jupyter_events-0.9.0/.pre-commit-config.yaml0000644000000000000000000000450013615410400016006 0ustar00ci: autoupdate_schedule: monthly autoupdate_commit_msg: "chore: update pre-commit hooks" repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: check-case-conflict - id: check-ast - id: check-docstring-first - id: check-executables-have-shebangs - id: check-added-large-files - id: check-case-conflict - id: check-merge-conflict - id: check-json - id: check-toml - id: check-yaml - id: debug-statements - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema rev: 0.27.0 hooks: - id: check-github-workflows - repo: https://github.com/executablebooks/mdformat rev: 0.7.17 hooks: - id: mdformat additional_dependencies: [mdformat-gfm, mdformat-frontmatter, mdformat-footnote] - repo: https://github.com/pre-commit/mirrors-prettier rev: "v3.0.3" hooks: - id: prettier types_or: [yaml, html, json] - repo: https://github.com/adamchainz/blacken-docs rev: "1.16.0" hooks: - id: blacken-docs additional_dependencies: [black==23.7.0] exclude: docs/user_guide/application.md - repo: https://github.com/codespell-project/codespell rev: "v2.2.6" hooks: - id: codespell args: ["-L", "sur,nd"] - repo: https://github.com/pre-commit/mirrors-mypy rev: "v1.6.1" hooks: - id: mypy files: "^jupyter_events" stages: [manual] args: ["--install-types", "--non-interactive"] additional_dependencies: [ "traitlets>=5.13", "jupyter_core>=5.4", "pyyaml", "python-json-logger", "pytest>=7", "click", "rich", ] - repo: https://github.com/pre-commit/pygrep-hooks rev: "v1.10.0" hooks: - id: rst-backticks - id: rst-directive-colons - id: rst-inline-touching-normal - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.1.3 hooks: - id: ruff args: ["--fix", "--show-fixes"] - id: ruff-format - repo: https://github.com/scientific-python/cookie rev: "2023.10.27" hooks: - id: sp-repo-review additional_dependencies: ["repo-review[cli]"] jupyter_events-0.9.0/.readthedocs.yaml0000644000000000000000000000036113615410400014755 0ustar00version: 2 build: os: ubuntu-22.04 tools: python: "3.9" sphinx: configuration: docs/source/conf.py python: install: # install itself with pip install . - method: pip path: . extra_requirements: - docs jupyter_events-0.9.0/CHANGELOG.md0000644000000000000000000003675513615410400013357 0ustar00# Changelog All notable changes to this project will be documented in this file. ## 0.9.0 ([Full Changelog](https://github.com/jupyter/jupyter_events/compare/v0.8.0...228a04801224d127f4304e17398464d045794cf0)) ### Bugs fixed - Clean up linting and fix a bug that was found [#91](https://github.com/jupyter/jupyter_events/pull/91) ([@blink1073](https://github.com/blink1073)) ### Maintenance and upkeep improvements - Clean up linting and fix a bug that was found [#91](https://github.com/jupyter/jupyter_events/pull/91) ([@blink1073](https://github.com/blink1073)) - Adopt ruff format [#90](https://github.com/jupyter/jupyter_events/pull/90) ([@blink1073](https://github.com/blink1073)) - Normalize "jsonschema\[format-nongpl\]" in pyproject.toml [#86](https://github.com/jupyter/jupyter_events/pull/86) ([@frenzymadness](https://github.com/frenzymadness)) ### Contributors to this release ([GitHub contributors page for this release](https://github.com/jupyter/jupyter_events/graphs/contributors?from=2023-10-16&to=2023-11-06&type=c)) [@blink1073](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Ablink1073+updated%3A2023-10-16..2023-11-06&type=Issues) | [@frenzymadness](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Afrenzymadness+updated%3A2023-10-16..2023-11-06&type=Issues) ## 0.8.0 ([Full Changelog](https://github.com/jupyter/jupyter_events/compare/v0.7.0...e3edb6a868924d3f1b15eaf18d45be621ad77cef)) ### Bugs fixed - Allow for string annotations in listener signature [#88](https://github.com/jupyter/jupyter_events/pull/88) ([@blink1073](https://github.com/blink1073)) ### Maintenance and upkeep improvements - Adopt sp-repo-review [#89](https://github.com/jupyter/jupyter_events/pull/89) ([@blink1073](https://github.com/blink1073)) - Bump actions/checkout from 3 to 4 [#84](https://github.com/jupyter/jupyter_events/pull/84) ([@dependabot](https://github.com/dependabot)) - Add more PyPI URLs [#82](https://github.com/jupyter/jupyter_events/pull/82) ([@pydanny](https://github.com/pydanny)) ### Contributors to this release ([GitHub contributors page for this release](https://github.com/jupyter/jupyter_events/graphs/contributors?from=2023-07-31&to=2023-10-16&type=c)) [@blink1073](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Ablink1073+updated%3A2023-07-31..2023-10-16&type=Issues) | [@dependabot](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Adependabot+updated%3A2023-07-31..2023-10-16&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Apre-commit-ci+updated%3A2023-07-31..2023-10-16&type=Issues) | [@pydanny](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Apydanny+updated%3A2023-07-31..2023-10-16&type=Issues) | [@Zsailer](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3AZsailer+updated%3A2023-07-31..2023-10-16&type=Issues) ## 0.7.0 ([Full Changelog](https://github.com/jupyter/jupyter_events/compare/v0.6.3...56e7d2660b59632765a85859217cddc7304e82f8)) ### Enhancements made - allow a 'message' field in an event schema [#72](https://github.com/jupyter/jupyter_events/pull/72) ([@Zsailer](https://github.com/Zsailer)) ### Bugs fixed - Improve usability of jp_read_emitted_events fixture [#74](https://github.com/jupyter/jupyter_events/pull/74) ([@kevin-bates](https://github.com/kevin-bates)) ### Maintenance and upkeep improvements - Migrate RefResolver to referencing.Registry [#80](https://github.com/jupyter/jupyter_events/pull/80) ([@hbcarlos](https://github.com/hbcarlos)) - Use local coverage [#73](https://github.com/jupyter/jupyter_events/pull/73) ([@blink1073](https://github.com/blink1073)) - Clean up license [#67](https://github.com/jupyter/jupyter_events/pull/67) ([@dcsaba89](https://github.com/dcsaba89)) - Add more linting [#65](https://github.com/jupyter/jupyter_events/pull/65) ([@blink1073](https://github.com/blink1073)) ### Contributors to this release ([GitHub contributors page for this release](https://github.com/jupyter/jupyter_events/graphs/contributors?from=2023-01-12&to=2023-07-31&type=c)) [@blink1073](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Ablink1073+updated%3A2023-01-12..2023-07-31&type=Issues) | [@dcsaba89](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Adcsaba89+updated%3A2023-01-12..2023-07-31&type=Issues) | [@hbcarlos](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Ahbcarlos+updated%3A2023-01-12..2023-07-31&type=Issues) | [@kevin-bates](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Akevin-bates+updated%3A2023-01-12..2023-07-31&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Apre-commit-ci+updated%3A2023-01-12..2023-07-31&type=Issues) | [@Zsailer](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3AZsailer+updated%3A2023-01-12..2023-07-31&type=Issues) ## 0.6.3 ([Full Changelog](https://github.com/jupyter/jupyter_events/compare/v0.6.2...ac65980322317f1f30bc07150c9e14afaad03d40)) ### Maintenance and upkeep improvements - Clean up typing [#64](https://github.com/jupyter/jupyter_events/pull/64) ([@blink1073](https://github.com/blink1073)) ### Contributors to this release ([GitHub contributors page for this release](https://github.com/jupyter/jupyter_events/graphs/contributors?from=2023-01-10&to=2023-01-12&type=c)) [@blink1073](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Ablink1073+updated%3A2023-01-10..2023-01-12&type=Issues) ## 0.6.2 ([Full Changelog](https://github.com/jupyter/jupyter_events/compare/v0.6.1...a00859944090df5277f263fcfe72ae48b8cc2382)) ### Maintenance and upkeep improvements - Decrease pyyaml dependency floor to increase compatibility [#63](https://github.com/jupyter/jupyter_events/pull/63) ([@kevin-bates](https://github.com/kevin-bates)) ### Contributors to this release ([GitHub contributors page for this release](https://github.com/jupyter/jupyter_events/graphs/contributors?from=2023-01-10&to=2023-01-10&type=c)) [@kevin-bates](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Akevin-bates+updated%3A2023-01-10..2023-01-10&type=Issues) ## 0.6.1 ([Full Changelog](https://github.com/jupyter/jupyter_events/compare/v0.6.0...1aa57024d0a8c73b10d9944375f84c01ee9f5c33)) ### Maintenance and upkeep improvements - Remove artificial cap on jsonschema dependency [#61](https://github.com/jupyter/jupyter_events/pull/61) ([@kevin-bates](https://github.com/kevin-bates)) - Try dropping jsonschema dependency [#59](https://github.com/jupyter/jupyter_events/pull/59) ([@bretttully](https://github.com/bretttully)) ### Contributors to this release ([GitHub contributors page for this release](https://github.com/jupyter/jupyter_events/graphs/contributors?from=2023-01-09&to=2023-01-10&type=c)) [@blink1073](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Ablink1073+updated%3A2023-01-09..2023-01-10&type=Issues) | [@bretttully](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Abretttully+updated%3A2023-01-09..2023-01-10&type=Issues) | [@kevin-bates](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Akevin-bates+updated%3A2023-01-09..2023-01-10&type=Issues) ## 0.6.0 ([Full Changelog](https://github.com/jupyter/jupyter_events/compare/v0.5.0...83f01b142c3190074d9e6108155514ddc6237d2c)) ### Maintenance and upkeep improvements - Add typing file [#60](https://github.com/jupyter/jupyter_events/pull/60) ([@blink1073](https://github.com/blink1073)) - More lint cleanup [#56](https://github.com/jupyter/jupyter_events/pull/56) ([@blink1073](https://github.com/blink1073)) - Add more ci checks [#53](https://github.com/jupyter/jupyter_events/pull/53) ([@blink1073](https://github.com/blink1073)) - Allow releasing from repo [#52](https://github.com/jupyter/jupyter_events/pull/52) ([@blink1073](https://github.com/blink1073)) - Adopt ruff and address lint [#51](https://github.com/jupyter/jupyter_events/pull/51) ([@blink1073](https://github.com/blink1073)) - Use base setup dependency type [#47](https://github.com/jupyter/jupyter_events/pull/47) ([@blink1073](https://github.com/blink1073)) - Clean up CI [#45](https://github.com/jupyter/jupyter_events/pull/45) ([@blink1073](https://github.com/blink1073)) - CI Cleanup [#44](https://github.com/jupyter/jupyter_events/pull/44) ([@blink1073](https://github.com/blink1073)) - Bump actions/checkout from 2 to 3 [#42](https://github.com/jupyter/jupyter_events/pull/42) ([@dependabot](https://github.com/dependabot)) - Add dependabot [#41](https://github.com/jupyter/jupyter_events/pull/41) ([@blink1073](https://github.com/blink1073)) - Maintenance cleanup [#36](https://github.com/jupyter/jupyter_events/pull/36) ([@blink1073](https://github.com/blink1073)) - Clean up CI checks [#35](https://github.com/jupyter/jupyter_events/pull/35) ([@blink1073](https://github.com/blink1073)) - Clean up pyproject and ci [#33](https://github.com/jupyter/jupyter_events/pull/33) ([@blink1073](https://github.com/blink1073)) ### Documentation improvements - Fix listener example [#34](https://github.com/jupyter/jupyter_events/pull/34) ([@dlqqq](https://github.com/dlqqq)) ### Contributors to this release ([GitHub contributors page for this release](https://github.com/jupyter/jupyter_events/graphs/contributors?from=2022-09-12&to=2023-01-09&type=c)) [@blink1073](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Ablink1073+updated%3A2022-09-12..2023-01-09&type=Issues) | [@dependabot](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Adependabot+updated%3A2022-09-12..2023-01-09&type=Issues) | [@dlqqq](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Adlqqq+updated%3A2022-09-12..2023-01-09&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Apre-commit-ci+updated%3A2022-09-12..2023-01-09&type=Issues) ## 0.5.0 ([Full Changelog](https://github.com/jupyter/jupyter_events/compare/v0.4.0...af1db6f5b9052e54d5a65797b67bff17b80e7eec)) ### Enhancements made - Add pytest plugin for testing events in other libraries [#23](https://github.com/jupyter/jupyter_events/pull/23) ([@Zsailer](https://github.com/Zsailer)) - improve error messages for absent/invalid schema path [#22](https://github.com/jupyter/jupyter_events/pull/22) ([@dlqqq](https://github.com/dlqqq)) ### Bugs fixed - specify utf-8 encoding for loading/dumping yaml [#29](https://github.com/jupyter/jupyter_events/pull/29) ([@bollwyvl](https://github.com/bollwyvl)) - use safe loaders and dumpers in yaml lib [#28](https://github.com/jupyter/jupyter_events/pull/28) ([@dlqqq](https://github.com/dlqqq)) ### Maintenance and upkeep improvements - add jsonschema\[format-nongpl\], core event schema [#31](https://github.com/jupyter/jupyter_events/pull/31) ([@bollwyvl](https://github.com/bollwyvl)) - Add CLI tests, return codes, version [#30](https://github.com/jupyter/jupyter_events/pull/30) ([@bollwyvl](https://github.com/bollwyvl)) - Set version floor on jsonchema [#25](https://github.com/jupyter/jupyter_events/pull/25) ([@kevin-bates](https://github.com/kevin-bates)) - Testing that the `.emit` call is a no-op when no listeners are registered. [#24](https://github.com/jupyter/jupyter_events/pull/24) ([@Zsailer](https://github.com/Zsailer)) ### Documentation improvements - docs: wrap shell command in quotes [#21](https://github.com/jupyter/jupyter_events/pull/21) ([@dlqqq](https://github.com/dlqqq)) ### Contributors to this release ([GitHub contributors page for this release](https://github.com/jupyter/jupyter_events/graphs/contributors?from=2022-08-29&to=2022-09-12&type=c)) [@bollwyvl](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Abollwyvl+updated%3A2022-08-29..2022-09-12&type=Issues) | [@dlqqq](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Adlqqq+updated%3A2022-08-29..2022-09-12&type=Issues) | [@kevin-bates](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Akevin-bates+updated%3A2022-08-29..2022-09-12&type=Issues) | [@Zsailer](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3AZsailer+updated%3A2022-08-29..2022-09-12&type=Issues) ## 0.4.0 ([Full Changelog](https://github.com/jupyter/jupyter_events/compare/v0.3.0...6d22b7dd73b1a04baf26a68539743d8a66599469)) ### Enhancements made - Add method to remove listener [#18](https://github.com/jupyter/jupyter_events/pull/18) ([@Zsailer](https://github.com/Zsailer)) - Add a simple CLI tool for validating event schemas [#9](https://github.com/jupyter/jupyter_events/pull/9) ([@Zsailer](https://github.com/Zsailer)) ### Bugs fixed - Fix minor bugs in Listeners API [#19](https://github.com/jupyter/jupyter_events/pull/19) ([@Zsailer](https://github.com/Zsailer)) ### Contributors to this release ([GitHub contributors page for this release](https://github.com/jupyter/jupyter_events/graphs/contributors?from=2022-08-24&to=2022-08-29&type=c)) [@Zsailer](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3AZsailer+updated%3A2022-08-24..2022-08-29&type=Issues) ## 0.3.0 ([Full Changelog](https://github.com/jupyter/jupyter_events/compare/v0.2.0...94646036f0ab4b3397e261422fd3041c0d7501e9)) ### Enhancements made - Remove (duplicate) version argument from API [#16](https://github.com/jupyter/jupyter_events/pull/16) ([@Zsailer](https://github.com/Zsailer)) - Add a "modifiers" hook to allow extension authors to mutate/redact event data [#12](https://github.com/jupyter/jupyter_events/pull/12) ([@Zsailer](https://github.com/Zsailer)) - Add Listeners API [#10](https://github.com/jupyter/jupyter_events/pull/10) ([@Zsailer](https://github.com/Zsailer)) ### Bugs fixed - Reading strings as file path is unsafe [#15](https://github.com/jupyter/jupyter_events/pull/15) ([@Zsailer](https://github.com/Zsailer)) ### Maintenance and upkeep improvements - Fix check_release build [#14](https://github.com/jupyter/jupyter_events/pull/14) ([@blink1073](https://github.com/blink1073)) ### Documentation improvements - Add JupyterLite example to the documentation [#6](https://github.com/jupyter/jupyter_events/pull/6) ([@Zsailer](https://github.com/Zsailer)) ### Contributors to this release ([GitHub contributors page for this release](https://github.com/jupyter/jupyter_events/graphs/contributors?from=2022-08-11&to=2022-08-24&type=c)) [@blink1073](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Ablink1073+updated%3A2022-08-11..2022-08-24&type=Issues) | [@Zsailer](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3AZsailer+updated%3A2022-08-11..2022-08-24&type=Issues) ## 0.2.0 ([Full Changelog](https://github.com/jupyter/jupyter_events/compare/v0.1.0...88acd8ec613fe7d2aa6fcaf07158275989dc5dfd)) ### Enhancements made - Add new EventSchema and SchemaRegistry API [#4](https://github.com/jupyter/jupyter_events/pull/4) ([@Zsailer](https://github.com/Zsailer)) - Add redactionPolicies field to Jupyter Event schemas [#2](https://github.com/jupyter/jupyter_events/pull/2) ([@Zsailer](https://github.com/Zsailer)) ### Contributors to this release ([GitHub contributors page for this release](https://github.com/jupyter/jupyter_events/graphs/contributors?from=2022-05-31&to=2022-08-11&type=c)) [@kevin-bates](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Akevin-bates+updated%3A2022-05-31..2022-08-11&type=Issues) | [@Zsailer](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3AZsailer+updated%3A2022-05-31..2022-08-11&type=Issues) ## 0.1.0 jupyter_events-0.9.0/RELEASE.md0000644000000000000000000000155013615410400013131 0ustar00# Making a Jupyter Events Release ## Using `jupyter_releaser` The recommended way to make a release is to use [`jupyter_releaser`](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html). Note that we must use manual versions since Jupyter Releaser does not yet support "next" or "patch" when dev versions are used. ## Manual Release To create a manual release, perform the following steps: ### Set up ```bash pip install hatch twine git pull origin $(git branch --show-current) git clean -dffx ``` ### Update the version and apply the tag ```bash echo "Enter new version" read new_version hatch version ${new_version} git tag -a ${new_version} -m "Release ${new_version}" ``` ### Build the artifacts ```bash rm -rf dist hatch build ``` ### Publish the artifacts to pypi ```bash twine check dist/* twine upload dist/* ``` jupyter_events-0.9.0/.github/dependabot.yml0000644000000000000000000000031513615410400015715 0ustar00version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" - package-ecosystem: "pip" directory: "/" schedule: interval: "weekly" jupyter_events-0.9.0/.github/workflows/enforce-label.yml0000644000000000000000000000050013615410400020337 0ustar00name: Enforce PR label on: pull_request: types: [labeled, unlabeled, opened, edited, synchronize] jobs: enforce-label: runs-on: ubuntu-latest permissions: pull-requests: write steps: - name: enforce-triage-label uses: jupyterlab/maintainer-tools/.github/actions/enforce-label@v1 jupyter_events-0.9.0/.github/workflows/prep-release.yml0000644000000000000000000000265713615410400020244 0ustar00name: "Step 1: Prep Release" on: workflow_dispatch: inputs: version_spec: description: "New Version Specifier" default: "next" required: false branch: description: "The branch to target" required: false post_version_spec: description: "Post Version Specifier" required: false since: description: "Use PRs with activity since this date or git reference" required: false since_last_stable: description: "Use PRs with activity since the last stable git tag" required: false type: boolean jobs: prep_release: runs-on: ubuntu-latest steps: - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - name: Prep Release id: prep-release uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2 with: token: ${{ secrets.ADMIN_GITHUB_TOKEN }} version_spec: ${{ github.event.inputs.version_spec }} post_version_spec: ${{ github.event.inputs.post_version_spec }} target: ${{ github.event.inputs.target }} branch: ${{ github.event.inputs.branch }} since: ${{ github.event.inputs.since }} since_last_stable: ${{ github.event.inputs.since_last_stable }} - name: "** Next Step **" run: | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}" jupyter_events-0.9.0/.github/workflows/publish-release.yml0000644000000000000000000000347613615410400020744 0ustar00name: "Step 2: Publish Release" on: workflow_dispatch: inputs: branch: description: "The target branch" required: false release_url: description: "The URL of the draft GitHub release" required: false steps_to_skip: description: "Comma separated list of steps to skip" required: false jobs: publish_release: runs-on: ubuntu-latest steps: - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - name: Populate Release id: populate-release uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2 with: token: ${{ secrets.ADMIN_GITHUB_TOKEN }} target: ${{ github.event.inputs.target }} branch: ${{ github.event.inputs.branch }} release_url: ${{ github.event.inputs.release_url }} steps_to_skip: ${{ github.event.inputs.steps_to_skip }} - name: Finalize Release id: finalize-release env: PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} PYPI_TOKEN_MAP: ${{ secrets.PYPI_TOKEN_MAP }} TWINE_USERNAME: __token__ NPM_TOKEN: ${{ secrets.NPM_TOKEN }} uses: jupyter-server/jupyter-releaser/.github/actions/finalize-release@v2 with: token: ${{ secrets.ADMIN_GITHUB_TOKEN }} target: ${{ github.event.inputs.target }} release_url: ${{ steps.populate-release.outputs.release_url }} - name: "** Next Step **" if: ${{ success() }} run: | echo "Verify the final release" echo ${{ steps.finalize-release.outputs.release_url }} - name: "** Failure Message **" if: ${{ failure() }} run: | echo "Failed to Publish the Draft Release Url:" echo ${{ steps.populate-release.outputs.release_url }} jupyter_events-0.9.0/.github/workflows/python-tests.yml0000644000000000000000000001151613615410400020333 0ustar00name: Jupyter Events Tests on: push: branches: ["main"] pull_request: schedule: - cron: "0 8 * * *" defaults: run: shell: bash -eux {0} jobs: build: runs-on: ${{ matrix.os }} timeout-minutes: 20 strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] python-version: ["3.8", "3.12"] include: - os: windows-latest python-version: "3.9" - os: ubuntu-latest python-version: "pypy-3.8" - os: ubuntu-latest python-version: "3.10" - os: macos-latest python-version: "3.11" steps: - uses: actions/checkout@v4 - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - name: Run Tests run: hatch run cov:test - uses: jupyterlab/maintainer-tools/.github/actions/upload-coverage@v1 coverage: runs-on: ubuntu-latest needs: - build steps: - uses: actions/checkout@v4 - uses: jupyterlab/maintainer-tools/.github/actions/report-coverage@v1 docs: runs-on: windows-latest steps: - uses: actions/checkout@v4 - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - run: hatch run docs:build test_lint: name: Test Lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - name: Run Linters run: | hatch run typing:test hatch run lint:build pipx run interrogate . pipx run doc8 --max-line-length=200 jupyter_server_downstream: runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1 with: package_name: jupyter_server test_minimum_versions: name: Test Minimum Versions timeout-minutes: 20 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Base Setup uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 with: dependency_type: minimum - name: Install with minimum versions and optional deps run: | pip install -e .[test] pip install jsonschema[format-nongpl,format_nongpl] - name: List installed packages run: | pip freeze pip check - name: Run the unit tests run: | pytest -vv -W default || pytest -vv -W default --lf test_prereleases: name: Test Prereleases runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Checkout uses: actions/checkout@v4 - name: Base Setup uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 with: dependency_type: pre - name: Run the tests run: | hatch run test:nowarn || hatch run test:nowarn --lf make_sdist: name: Make SDist runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - uses: jupyterlab/maintainer-tools/.github/actions/make-sdist@v1 test_sdist: runs-on: ubuntu-latest needs: [make_sdist] name: Install from SDist and Test timeout-minutes: 20 steps: - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - uses: jupyterlab/maintainer-tools/.github/actions/test-sdist@v1 with: test_command: pytest -vv || pytest -vv --lf check_links: runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkout@v4 - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - uses: jupyterlab/maintainer-tools/.github/actions/check-links@v1 check_release: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Base Setup uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - name: Install Dependencies run: | pip install -e . - name: Check Release uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2 with: version_spec: 100.100.100 token: ${{ secrets.GITHUB_TOKEN }} tests_check: # This job does nothing and is only used for the branch protection if: always() needs: - coverage - docs - test_lint - check_release - test_minimum_versions - test_prereleases - jupyter_server_downstream - test_sdist - check_links runs-on: ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} jupyter_events-0.9.0/docs/Makefile0000644000000000000000000000117213615410400014117 0ustar00# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) jupyter_events-0.9.0/docs/conf.py0000644000000000000000000000605213615410400013760 0ustar00# Configuration file for the Sphinx documentation builder. # # This file only contains a selection of the most common options. For a full # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # import os # import sys # sys.path.insert(0, os.path.abspath('.')) # -- Project information ----------------------------------------------------- from typing import List project = "jupyter_events" copyright = "2019, Project Jupyter" # noqa author = "Project Jupyter" # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions: List = ["myst_parser", "jupyterlite_sphinx"] try: import enchant # type:ignore # noqa extensions += ["sphinxcontrib.spelling"] except ImportError: pass # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] source_suffix = [".rst", ".md"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = [ "_build", "_contents", "Thumbs.db", ".DS_Store", "demo/demo-notebook.ipynb", ] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "pydata_sphinx_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] html_logo = "_static/jupyter_logo.png" master_doc = "index" # Configure jupyterlite to import jupyter_events package jupyterlite_contents = ["demo/demo-notebook.ipynb"] html_theme_options = { "logo": { "text": "Jupyter Events", }, "navigation_with_keys": False, "icon_links": [ { # Label for this link "name": "GitHub", # URL where the link will redirect "url": "https://github.com/jupyter/jupyter_events", # required # Icon class (if "type": "fontawesome"), or path to local image (if "type": "local") "icon": "fab fa-github-square", # The type of image to be used (see below for details) "type": "fontawesome", }, { "name": "jupyter.org", "url": "https://jupyter.org", "icon": "_static/jupyter_logo.png", "type": "local", }, ], } jupyter_events-0.9.0/docs/index.md0000644000000000000000000000126613615410400014114 0ustar00# Jupyter Events _An event system for Jupyter Applications and extensions._ Jupyter Events enables Jupyter Python Applications (e.g. Jupyter Server, JupyterLab Server, JupyterHub, etc.) to emit **events**—structured data describing things happening inside the application. Other software (e.g. client applications like JupyterLab) can _listen_ and respond to these events. ## Install Install Jupyter Events directly from PyPI: ``` pip install jupyter_events ``` or conda-forge: ``` conda install -c conda-forge jupyter_events ``` ## Contents ```{toctree} --- maxdepth: 2 --- user_guide/index demo/index ``` ## Indices and tables - {ref}`genindex` - {ref}`modindex` - {ref}`search` jupyter_events-0.9.0/docs/make.bat0000644000000000000000000000143313615410400014064 0ustar00@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd jupyter_events-0.9.0/docs/_static/jupyter_logo.png0000644000000000000000000006456513615410400017354 0ustar00PNG  IHDRX 7gAMA a@IDATxx=Id);$4aSf?W)@x$*Mb@-{ eʦ-# eeN FI,ْIqd[t'}<{O>(@ P(@ P(@ PBiaLP@)%O]GI)G?]V{QF}2mڴDTX$A Pp8utt(?S9r$R6T=eP~6WS(5Ȋcq* Bz4 t0XZN (@ C][&_+%@Z\ϥԮzkg͚yW z=Ξ={`J]M P&]ؔӲx\ ŖL]4ZRdS1#c-Kd.(@g#R r#cOJ]M P̗qz[Y@K+u6(@pm0*tٸ y1!UeJRA`C6?4LpUeJRA3X~,O,Mer v+?c]V,YB Pi:;;`/ʒ\(+VT UI P\?چl\q޲cfBiRAs*%FXdKKS(@,7aWd ,M@d # *LlnE,PeړrQA+J`i*Ӥ(`Jؒ2QV UI P=ٸ'd܊2XZ4)@ egM =L@I n# =. Ä/)@ \QTӬd k"(P)ղB<=+`i*Ӥ(`wc9UVVf |E P5@Mpc"uؙSpC' mma[<[V2] P65 ٸ$ mUeJRAs>hC6fUA,el/"K.eYf}hZ=b—$IM֔Uߚtץ`i.Ӧ(`.!ц⛒`- )P 7@t#I%O'=II υ)I*]%<wм R ,M4=!eUUs+r)Sx7AڍϷo!?(N@w%mЕ!烔|si<aC)dBIj-_,"yWW-Ʃb 7^={o}E{yRN nX;ImoBrLBy饍'Y0Kc4̮:qC'AjLa'r %>A%վJqvHT{j@\ci+fqh].oݴ۠膵ajD Z[5cq{Y$b5lX?E|7SQ /-Uc3psR8\zٵ,`y^ r+^(W=|eh1_zc1 -9q<| %|5}Պ@yZ ]¬%lt}F7l]0#T*.!q"G2ߓ[R ;8x=)햊w9ӧO2L>L^Z:tȩ555"`_"Ͽ ~ ^?7w*C̣y \=РG]Vm^?ږø>g#cՉrc{ۦ3uJg}i*M/W_q$P'#P~(hsS0P@lJ!ʋ(IEf,/SeϷ9sO 59~z!v/@%20RxjZvf;V55*_\yh~%G;:[d&[0G8+u&FHnV|P=R#+O|T}=7"8t4v4/^SWxs^%fg6˹ "ee՗^z)g1RME>l?RR ],̊IS h^]U>sIsA L@!xⴊ χfW4 Bb]7ma+oebv0c []5b B,Թe+/5gVŬf.%@/Dw8~XlDz5xmL K?s?z ^pq.*J}[IOwP&2w `ƒG ^JҚ! < 32; Yv[m3#5+0RΘ4딑RH͸ѺC=kc#c;&i׻_3fgႲ:p*&~*W@@/n%g[9X\-D"q*uk46{خ7T;}.ebVD|>&hEѺq4r@`;YIAp#HG[tHRy.Eji}K%TYoϺr.ٮr׽\Wf nΌ<i3>+:V<,ꃍ_+Jpҵ,]ut_ۜNԗD*O~rtc,wo&O jcj3fZH,w(\qLYcjEX[}>7tu3"@ Ht.U]cpj!Y6 8vuV ،8Hq^rKH ƣ"j~mܮ*l,q͊䃥39V7nV*YDǖUPLqu4[jpёPM7,%{EenIu,yOV@KsRgp|ıL~`ɋ]&LKtq;Kn(>~L&>YM%\4ה iE輶Q(b,)3z}"OGM5?h@`i_Ø$ڪE>r >5A%qYɢPb9gkf\u*Jl,1h FyDm\[Oc=*Q$0HL6GjO. KuYz-.,? N( PÕH|6^;ak8G``)/$F8s{xp0/vvqb(MM"sc7ȕqm,-JePx.|xNL1jgyH9JJ1%fz6V3mo,pe2FSbϸ= FW✷=|*IX:Xzm, n(|_eT#}wӺhM!ѸXxtXV (@'ĔTie  ݘPB?*`MHqo0d"_›Mz=q| }G_$rԴ3;:R*X;)u//)!`p ͇#cLm/mr} ,#Z~\]#ReJ75 lqU\ڲ4,к|IyW\qʼvҐ&aRK@ :+԰LJE},i~/ ջDE ә,T#M#u O0,:}.l 7ZJ \1=a 4.8Ɛ /<¬4(PFm]<~BW`U3XB\ZfԴ]~nT͔AѺG1$|>G+xH-4&/qo%Y~%ZQwl ^L2NGDd. 98\J P {PU_#5UdH';wyGJ݃e} ]m?gL֕~'FЅIS%. x6#K֋:,7 #LHSъ7,s/Pn hmtNJ{X z9ȫjyn%{ ]_y?FJ&dLF츍S֙$(@Kа8oYȍ:X{Baߤd)D?O6׼ⵓ$U8_w"r? P /|٣oK5\B\BG69K#.hDvO&|f/Ҷ|Ҳk_TR v|(@ . F$/{߆kz ,pMvspb!=75B_ZV{]߁p+a4ylh2Jw뼂Ahe^y.eذܤ}ܹsovSׁ1j.Ng<$/&C P _w\IA,Ĕ`tuuub'!`ˆEP )FkO!ud{v4~7: `2 shŬRۙ,#^p[:v@ǎv$5(8x!൫j_OӤ焊_в`ٳNhEёT*4͏yqt|:vϦM蹏^ᑘ@sMeY)@:Fʞ7⩕5-X[lg WnԳ(UJPB@.,(M,<ԱIJ%w(PpL`0/rTځ`rnjՉDYL6EpW P@)8 ;izӂ1X^OOq(hcB'izflG"D  < a-FO]R۷=<{z< FXŗ.s%Z+0ZJ PiJ(V.㊭FcbTX +Ė븂@9 5.ډRDDKrm1Dݢ5Ljrn,XFjk:>xӥZ\/yP鞅)ȣO9,TxJE<ֶ}J34N6%/J۽|"Ui_3u*2>Ne'Lހ sm) 2cc%۲T3np‡2Gp|N+/{"#gRKy $"hNEǞ1YRM}a1C%,㵓$DB4n z_8ϕi-&d(`1/bTwC-O[CO"m,`j W!|"` ~/9H|2sIPiedϿhubĨ[8PC륹J*Xv{c#71_8(k_yXV2V."{ݶuIK9"h]Q}Dlsڶ%,uU`f_9 8<*QOIM<T'%SIuZ:`˖s 04{[l%,(!P^#ሂ  +G<]7xua`!\#56.E< :G5ha 6gmJv%,vļ/qu<&k75 d"u/;EW ~ ZLg>XaQ].En|@r~<<$5nSM޸aJZV˅fF۝ou4n$ @  PfS DWR*tBpGQ!{X eQ˶? ](_KuK 6XFkR"?d믁|Hhmc锸0qFW'4ƄZKe_ cw*5f_TڝLK;"~w[px@ִ]Oď{#p̄)κ/O;.Xh,* l M5v{`V*; K 0F ߏࢿ鵦_Mi1er26(R|w{bgXL0~A)VgQN~`LXN>ZE,# z8SV\qMSEb(@sb5FdnE160:y͢ L8Tjo¥sIQ:(P$_>wbrۜPcENS˶GPBgU.t֭^vfڗS%! w8 hyj]EqQY'њʳ0ԟUD:4DYqc @1uѢIPeC˗N;u'*4LkJ*vf"m(@tEE+#[11)m~g_pw=jߋ[P =߭4M 6\y*,{a ;/B+3RfJ6OVahq^E+MKWl\½rH"%|q+)#x9U\9TŢTMRR],:-a4Fٶڕ7U \1K1ׄ6׉'m{1# P]cUX7VTX )@ P`@{mՑh\f]+LyGH{CjY>ۊ@i&&;{2Pn8K PƖj8"GۧWbEt9vғ&IJom4R@a~4Ri,U1-K=l0;PﭡS( `z ^\f"͝Mݣ5UϬ׎=1zj_*iYxN%!2% Pf7,]S꿘ކ4M̭x,EAk^8%VE Pv URFĀ!fi`H{oYvӄWGvg~s*Z*/ɓQJ)*l\iFZ(xt/P.ϡ9n:B42s PX'`n+guͬ 6jHbzͦJ=>=;)^iu|M Pi}@pD%%a19Biܯ-K%ZvIG =+_Sp.x^˷E+\k@;12\O_e5][_17fr/_(@}{Em9RaLK( aƅl1Gp\n'(@ 8_O%&HksgI*yZJlK  )=yQo}| Q1{߆k(@ PMH!L+Fu,]KƠhR0.h~mV Դma1?JT.Ms=X&勠R;~ǁ<<.(PJK> m9voă;3uMlQe߬'Ɏ@ -ʻ3ٞP@ ȳy5)gʈ~4nБГ+ckA_(i*f-]߶\O P'llI(̎~k/ pnK5grP}kkAwۖ)@ PtJ xLRPb|l ;ODyhAp}>J PpiDok[_ Zt5w@_ζ`gMgB*0Q sK(@ !`tb҂WBy&- ~BCGߟʤP܆(`@eWx~bHW yd4fbYD+jcϒP ^*`f(@S~0 P*]?" , mS\4SؽM7ӂq; P -"6+L$X}OS?ǙPv ڷ=<{iz4u,{ ͤ@܆(`#Vݨ#VӃe4:ǘԶ(@ P0%&-XsF e(P*?\< (@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ P(@ PUW5U\L Pp(2Bim!Ekv?nO P[ x|MP'G_\O P(@[]`z42ԔS)qwf+ST%twRܞIޖˊ^!ⱶOȤ ܆(`@{mՑJk-.$OK%% 0X&OO82p Pt2VQKeWRt2|J' iSWNN#h̕K)t㽣,XV|?UG]3s Pڪ ]yLD*f-YiޖK9wSv~}5-, P*!MR n>o^g`9`vt{9PF*of SvҐj,h;vO_w"X,2<鞡-̟y )@ P 60gi[SyL[,C[U >Y0WM]u`yu|M PU "6ix֘i.>Y,>JӖBC OH )@ PY^70M{;msЫףꙹ}?۞4N¦[e(@LuURBPbL> l?t[fK 4-]{Qbq2m(Ћ@[}Ĥҍ^gR^/?mídYlK4r7y U'eӬ" P@Zr &Eqd`!F&?%Nݖ,F)F-mѣWwpt͸(%>j7 DWM+m h*M(ˆUz*x턭7,_ P@OHmzR uSܠkem F*;43c*rl+)@ P*Fkq\쯶;Ov}5X}ujLuU(@ Plj*2k\˫Uixc$U3ePDD0ӻR2\G1[CF PVzƒ5q#&y3Ը||ck(lz}-N_^Ms.{Lw~ܖC >2O,&PyJ#ۃeW~aj:0_jvҘvF(P 5 d5*? Ez+H/HH.{+]JO{\(P|m5~%R'Q7+/&/ FA|޲F% 25sIP5-S9RS9DPp@}(cg\jM49cǂK^?yqo?=5(nݣ-|O͵&ipaK9 ,3[[wSD=S񎷢uw(@ pXTuOQyQj,46ڽ(Wxj]vshݸx3鍎/_PpqhguD``uݗ߸LW3*7яߝo\cFzLThMf<@_% 066+x4 0 5\L#uU}(@ PzSAW"8F 7#G(HgH݈_Y;CYt(@ .QdByOC׽ 6:gChYѼ? rzBY1/(@ L Rlt) IDAT7DR@yT1ʞ@iYk7 ,dČAup8-(PWנsM;A+6tTTM;8K*,e,IR&ƥ}VJlJSȗ4Z: alac#KF+S*5"k\MKaJ] R?qs'MALiw׊e H1,7T"0LTвh,F]NQԺ@^ZF5UBxncD;̈y1m P `F1: ՁgO92X${\+~۟jlĽ|P@M-o.>-?"s\jjm.fcez{^ aۺ4y2Br{ P*Г7"HN@/C2Wv`iT/r!_+焚Z@Q hҼ (vVR`rW_,{EDG+,Y1{=g>1V#h.6;mG PՇTj1uf7Wpt4֍R7ñq.IH,=ef/l|E P0sq{B^һcqٛv`ĤFwvll/WX1y(@ OfK= y&6,?n!Z1-5KDi俟>;_V_}l1 PPs&jz*.O.d*B`q+\JrnY|Cx>!|բ,(@ zѺSЫ7 ߽[03˓p(ke׼kD+B@xxӭ2 PnU\ISoغw^i䱳kQʟޟG}Mqf󾙦2A PFx턭*07+lKآ \, JKSS] yy0rW Pv،ɤDž5鳽d@UjZfv_ Ow,C?0KerN\Ųs+_ w(`ђLd=ƶdV )2yyYῙB.R[uF:}4U+k:tV\F P@|Fe%Z8[==;5 ݼer]r{'^;Zr쪐;ݳ\N(r *1&n AoBˎ oB \!nP#u$zNƿ}ZV5lh2z-+@zkλyHq&߲"ti`RӢAhw=bU9q MQ@cVt)@p9͘J.5r-JigZsS͋>XeaXYVaɇB[YӦ@) Ga--J!]s[qҭsqR2Q #˲}Gjˏ~8X'`ēbF; o3lWy8UIK0t+r/u1ڊwD񂔃R%ƍxP|"A:.)ŔrѭSxL4qM=wr4F*)_eW  gLL& ֮޿1.)˟kgnQR8xt>s6rvfн:4֑;T`ZJAu8gK.Xvx/~3@lǗƿqUsh>_?M̎[9_@TD;{q#8nc W[=gYr`7~)Fbxg"pQ s/Z3[[]^#б";:#~Qe%D`YUadpC,ĦK2Xq K~M% F}H7m*‹>*ʳ0{NSVUSqn~ 9.+ ``nNH6%, HIxA C\ =\{ΐ[.L%w5gF{bdWh;(%,:]uc>l]2r J, m$m*)RZxGj ;὆FiuY-p˭~˭lJ>X`ʋul7 8DZ3 -ޅ﹩,*B׵1a<{f<"hrQ=0_A-: jL?5gY``YִOYL#HWVxR8TKBXrMӂEy ,nxT!l̻i?B4q]I/enO5,-7tMFPb5tTqOC- )qv%eexI|hO"A]B@ ҃Z&ORR(}阔e"NqݮZg 8r<ս> 5\5UQDv=VKp)K{JȕWVgeYX".㊭۬]G HtB sңR#| Wta˹@"xL}0X9Ps|ƦYE)>+t4{B<'j?rm)Td\YR֑RGBB0(IW+g0%ewJ%qy/@%-S P^A0] ckZZ@erjMhk#Uv- Յ6DiɡJC5!(E` CN**ep}TY,k,x/k'Io{}lUWո:cz ֽX׵L%6G\fa[D!xa22l[u>%պ^c=vm+ozv{APR:}`ϛ">Ʉ"gS(P 12]2dklM(@ zH^m,3|KwMߩ/k wfF- 晊W|}=,8䑚I87u b7nJ PcqPͅ5,ʋ݉mΚQ(@WRZʁҀgjZ'\w{;ss Pn`-71;Vi? 7LR9&(@ 8V]T@eٟP/?~M;|$嘃(=| Zd,^i Ӹ\/]!ʳp}eLe01gIqw Pǝ$/3NM,aT5l]7| " 3 2eҤdL w$d2)I&C Pr|g%5M;oj,imvy2NXw2 pntk> ڪ#u{Yd)@0*,-sCM-KwK{{URR3 9!H /=s!\;K ?JN<:̶t`r-Fϕg?Q9{]N%WNE&0hN@URg7xGn8K{ :fd{rd.(-$AҪyq֖8kFB/c3ډQ} HZ\&K7-j7ibtnaQ6LE-bljy+Zc,m_E N M Q=0AwAqwpXﱖ/DHP5So?Asi3- hR6V|?tzhŊ`L… Oۦݒ*."i m'/70(7e|㩟Jp P>׵mfaB0q2$`YJG;Ϻ}ѱ LStx+ BZ"`i kq'u ]Nā"yo{Uqٛ% o"/MH Rrf_nK `T'RKVqlx )e*gy`㫟+]y͢:_|ڕ8w.5dE*a=vAJg1WPhEFTk{mE2K>(лe6\@4\S0839NSFJYfJLJ*X^wu>M6BRHqUN;"ƀx})NHڟb@P 2ʧ4˖f77E,nBz2FUMx}0OJ[xPhyG؍HF@'ޚp$m?`\Y1zZ䭁r23ĸc Uڛ'ErqdFp |H_=|Tr R2y h1۾*Z"/>/=Aٸx%1ђp}[wvvK|~#_yː!CnC3bgŖW%( MNNxPlK %|‹>2?] 7tSe"8 ZپƯ[EN=f3Xz"J?snZ@F/K=?P^](` 7ܰ.E<AQGF6gL6mTn͖*GTq*Tzr >aO*X2W=K X&`W-oaX2!hQfz@tPwDӕ8o܃@G4qlo=R{̿ݤx/+ p.{+L<,wR*V?O" .@1ct^=@RИˤrpt[~С:Eͽ-; [wotWRWȔMKyA>fk)2 ,`$o^q/fs駟o!K#`܃3֑ 3w&ུUM&у.Z2? `cs6P WGkv>P͑[N2eo5 @$<2Y 5&DݷS} cB^up-)`t}^Nv&9/9gL;1 0n;:͝I)!& OFY&C1n3G&arD -F")Ųܷsdm"`yw|y-܂O9(W9}Ǹ^ ncD''scy˱hq[a.LEBiK` ok (q#%qZ&|ˊyCN?ht@l);:J%cѝ; NG2*tQB}>5*)<ڻ_^^ @F[iG˽a/c֬^S%k̜q 1SR!Tpѹx\ A8{owYG{ѪfZR>gMjמqg$,A!nߨ$r|+} =Í|҃T)4 TBiM(?FqZ AK2,.w=wI @HvC Lk(Tx+`XsݰۺߐGNL3tW^,HbFP4.dh DN4RJO_q4#R,5 =K[ kiEYJo/R,gՉ0 @K,t m2`bl[J5( (:W" 5+RðE[NP H\JTϒbiPjB/>biD `Xcg~Y57L07;ޅYJT*5ۜ3 ©f\ V+39&i@`6{5ζs( @V,0tqϹE@ zkrcŔذg>=L' K?BIXشk>u "2Cʯh6@ X-j <,1U@|T# X-~*mJ[vf/+2B#`XzUÈ$a"p˄EE8 @};W!&zԁ! Tzr -. ͡H` 1 DR,kڟƜkv/K]dL bK7'c~q̏}iZ b}m$,^e?X?}bOj `hhG\ @"-2RɔQ7nͱR\۳ }HaHҷʦ:Okz2}1 ]ȋO[q.3& ~3w6@KKBr:c)5+ݹN @b)&Y?רHˁssS~yC"![ՐANvv G/++8>@`vX_SI0/nPΧ,+Od+3A%{9BWLByVѬҗ_"@ {(>  >"wetZ_VL[  pѶw0ݞݚˆG_Kzh6@ $X)Fxf}˗/^#4gW @,~^v*LLLlZދ!w՛; \ ŲcanհRt́)W8?F U/~eǟWܨ]Arp6RH)'"CfHElg^捺 iSO3C v1cdA mMAӟqV^P2DΞ@(RY,0{T4<[Vt<=ijC0mD2$b>l}iCkQ=m<|5/A  Djw}fCu%S 9cA/: fv@ +3 , schema_id='myapplication.org/example-event', version=1, data={'name': 'My Event'})\n", "hello world\n", "EventListenerData(event_logger=, schema_id='myapplication.org/example-event', version=1, data={'name': 'My Event'})\n" ] }, { "data": { "text/plain": [ "{'__timestamp__': '2022-08-11T22:46:28.947996Z',\n", " '__schema__': 'myapplication.org/example-event',\n", " '__schema_version__': 1,\n", " '__metadata_version__': 1,\n", " 'name': 'My Event'}" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "logger.emit(schema_id=\"myapplication.org/example-event\", version=1, data={\"name\": \"My Event\"})" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3.8.10 ('jupyter_events')", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.10" }, "orig_nbformat": 4, "vscode": { "interpreter": { "hash": "fa70b7d208e0e2ef401b5613e3a2c366a3ff98da2f39442a36f3be51bccaa21d" } } }, "nbformat": 4, "nbformat_minor": 2 } jupyter_events-0.9.0/docs/demo/index.md0000644000000000000000000000026413615410400015035 0ustar00# Try it out Try out the Jupyter Events Library in the example notebook below (powered by JupyterLite): ```{retrolite} demo-notebook.ipynb --- width: 100% height: 1200px --- ``` jupyter_events-0.9.0/docs/user_guide/application.md0000644000000000000000000000377113615410400017446 0ustar00# Adding `EventLogger` to a Jupyter application To begin using Jupyter Events in your Python application, create an instance of the `EventLogger` object in your application. ```python from jupyter_core.application import JupyterApp from jupyter_events import EventLogger from jupyter_events import Event class MyApplication(JupyterApp): classes = [EventLogger, ...] eventlogger = Instance(EventLogger) def initialize(self, *args, **kwargs): self.eventlogger = EventLogger(parent=self) ... ``` Register an event schema with the logger. ```python schema = """ $id: http://myapplication.org/my-method version: 1 title: My Method Executed description: My method was executed one time. properties: msg: title: Message type: string """ self.eventlogger.register_event_schema(schema=schema) ``` Call `.emit(...)` within the application to emit an instance of the event. ```python def my_method(self): # Do something ... # Emit event telling listeners that this event happened. self.eventlogger.emit( schema_id="myapplication.org/my-method", data={"msg": "Hello, world!"} ) # Do something else... ... ``` Great! Now your application is logging events from within. Deployers of your application can configure the system to listen to this event using Jupyter's configuration system. This usually means reading a `jupyter_config.py` file like this: ```python # A Jupyter from logging import StreamHandler handler = StreamHandler() c.EventLogger.handlers = [handler] ``` Now when we run our application and call the method, the event will be emitted to the console: ``` app = MyApplication.launch_instance(config_file="jupyter_config.py") app.my_method() ``` ``` {'__timestamp__': '2022-08-09T17:15:27.458048Z', '__schema__': 'myapplication.org/my-method', '__schema_version__': 1, '__metadata_version__': 1, 'msg': 'Hello, world!'} ``` jupyter_events-0.9.0/docs/user_guide/configure.md0000644000000000000000000000102713615410400017114 0ustar00# Configure applications to emit events Jupyter applications can be configured to emit events by registering logging `Handler`s with an Application's `EventLogger` object. This is usually done using a Jupyter configuration file, e.g. `jupyter_config.py`: ```python from logging import FileHandler # Log events to a local file on disk. handler = FileHandler("events.txt") # Explicitly list the types of events # to record and what properties or what categories # of data to begin collecting. c.EventLogger.handlers = [handler] ``` jupyter_events-0.9.0/docs/user_guide/defining-schema.md0000644000000000000000000001004313615410400020152 0ustar00(defining-schema)= # Defining an event schema All Jupyter Events schemas are valid [JSON schema](https://json-schema.org/) and can be written in valid YAML or JSON. More specifically, these schemas are validated against Jupyter Event's "meta"-JSON schema, [here](https://github.com/jupyter/jupyter_events/tree/main/jupyter_events/schemas/event-metaschema.yml). A common pattern is to define these schemas in separate files and register them with an `EventLogger` using the `.register_event_schema(...)` method: ```python schema_filepath = Path("/path/to/schema.yaml") logger = EventLogger() logger.register_event_schema(schema_file) ``` Note that a file path passed to `register_event_schema()` **must** be a Pathlib object. This is required for `register_event_schema()` to distinguish between file paths and schemas specified in a Python string. At a minimum, a valid Jupyter event schema requires the following keys: - `$id` : a URI to identify (and possibly locate) the schema. - `version` : the schema version. - `properties` : attributes of the event being emitted. Beyond these required items, any valid JSON should be possible. Here is a simple example of a valid JSON schema for an event. ```yaml $id: https://event.jupyter.org/example-event version: 1 title: My Event description: | Some information about my event type: object properties: thing: title: Thing description: A random thing. user: title: User name description: Name of user who initiated event required: - thing - user ``` ## Checking if a schema is valid When authoring a schema, how do you check if you schema is following the expected form? Jupyter Events offers a simple command line tool to validate your schema against its Jupyter Events metaschema. First, install the CLI: ``` pip install "jupyter_events[cli]" ``` Then, run the CLI against your schema: ``` jupyter-events validate path/to/my_schema.json ``` The output will look like this, if it passes: ``` ──────────────────────────────────── Validating the following schema ──────────────────────────────────── { "$id": "http://event.jupyter.org/test", "version": 1, "title": "Simple Test Schema", "description": "A simple schema for testing\n", "type": "object", "properties": { "prop": { "title": "Test Property", "description": "Test property.", "type": "string" } } } ──────────────────────────────────────────────── Results ──────────────────────────────────────────────── ✔ Nice work! This schema is valid. ``` or this if fails: ``` ──────────────────────────────────── Validating the following schema ──────────────────────────────────── { "$id": "http://event.jupyter.org/test", "version": 1, "title": "Simple Test Schema", "description": "A simple schema for testing\n", "type": "object", "properties": { "__badName": { "title": "Test Property", "description": "Test property.", "type": "string" } } } ──────────────────────────────────────────────── Results ──────────────────────────────────────────────── ❌ The schema failed to validate. We found the following error with your schema: '__badName' is an invalid property name because it starts with `__`. Properties starting with 'dunder' are reserved as special meta-fields for Jupyter Events to use. ``` jupyter_events-0.9.0/docs/user_guide/event-schemas.md0000644000000000000000000000352413615410400017701 0ustar00# What is an event schema? A Jupyter event schema defines the _shape_ and _type_ of an emitted event instance. This is a key piece of Jupyter Events. It tells the event listeners what they should expect when an event occurs. In the {ref}`first-event`, you saw how to register a schema with the `EventLogger`. In the next section, {ref}`defining-schema`, you will learn how to define a new schema. _So what exactly happens when we register a schema?_ ```python from jupyter_events.logger import EventLogger schema = """ $id: http://myapplication.org/example-event version: 1 title: Example Event description: An interesting event to collect properties: name: title: Name of Event type: string """ logger = EventLogger() logger.register_event_schema(schema) ``` First, the schema is validated against [Jupyter Event's metaschema](https://github.com/jupyter/jupyter_events/tree/main/jupyter_events/schemas/event-metaschema.yml). This ensures that your schema adheres minimally to Jupyter Event's expected form (read about how to define a schema [here](../user_guide/defining-schema.md)). Second, a `jsonschema.Validator` is created and cached for each one of your event schemas in a "schema registry" object. ```python print(logger.schemas) ``` ``` Validator class: Draft7Validator Schema: { "$id": "myapplication.org/example-event", "version": 1, "title": "Example Event", "description": "An interesting event to collect", "properties": { "name": { "title": "Name of Event", "type": "string" } } } ``` The registry's validators will be used to check incoming events to ensure all outgoing, emitted events are registered and follow the expected form. Lastly, if an incoming event is not found in the registry, it does not get emitted. This ensures that we only collect data that we explicitly register with the logger. jupyter_events-0.9.0/docs/user_guide/first-event.md0000644000000000000000000000331313615410400017401 0ustar00(first-event)= # Logging your first event! The `EventLogger` is the main object in Jupyter Events. ```python from jupyter_events.logger import EventLogger logger = EventLogger() ``` To begin emitting events from a Python application, you need to tell the `EventLogger` what events you'd like to emit. To do this, we should register our event's schema (more on this later) with the logger. ```python schema = """ $id: http://myapplication.org/example-event version: 1 title: Example Event description: An interesting event to collect properties: name: title: Name of Event type: string """ logger.register_event_schema(schema) ``` Now that the logger knows about the event, it needs to know _where_ to send it. To do this, we register a logging _Handler_ —borrowed from Python's standard [`logging`](https://docs.python.org/3/library/logging.html) library—to route the events to the proper place. ```python # We will import one of the handlers from Python's logging library from logging import StreamHandler handler = StreamHandler() logger.register_handler(handler) ``` The logger knows about the event and where to send it; all that's left is to emit an instance of the event! To to do this, call the `.emit(...)` method and set the (required) `schema_id` and `data` arguments. ```python from jupyter_events import Event logger.emit( schema_id="http://myapplication.org/example-event", data={"name": "My Event"} ) ``` On emission, the following data will get printed to your console by the `StreamHandler` instance: ``` {'__timestamp__': '2022-08-09T17:15:27.458048Z', '__schema__': 'myapplication.org/example-event', '__schema_version__': 1, '__metadata_version__': 1, 'name': 'My Event'} ``` jupyter_events-0.9.0/docs/user_guide/index.md0000644000000000000000000000020713615410400016241 0ustar00# User Guide ```{toctree} --- maxdepth: 2 --- first-event event-schemas defining-schema configure application modifiers listeners ``` jupyter_events-0.9.0/docs/user_guide/listeners.md0000644000000000000000000000135013615410400017142 0ustar00# Adding event listeners Event listeners are asynchronous callback functions/methods that are triggered when an event is emitted. Listeners can be used by extension authors to trigger custom logic every time an event occurs. ## Basic usage Define a listener (async) function: ```python from jupyter_events.logger import EventLogger async def my_listener(logger: EventLogger, schema_id: str, data: dict) -> None: print("hello, from my listener") ``` Hook this listener to a specific event type: ```python event_logger.add_listener( schema_id="http://event.jupyter.org/my-event", listener=my_listener ) ``` Now, every time a `"http://event.jupyter.org/test"` event is emitted from the EventLogger, this listener will be called. jupyter_events-0.9.0/docs/user_guide/modifiers.md0000644000000000000000000000201013615410400017105 0ustar00# Modifying events in an application using `jupyter_events` If you're deploying a configurable application that uses Jupyter Events to emit events, you can extend the application's event logger to modify/mutate/redact incoming events before they are emitted. This is particularly useful if you need to mask, salt, or remove sensitive data from incoming event. To modify events, define a callable (function or method) that modifies the event data dictionary. This callable **must** follow an exact signature (type annotations required): ```python def my_modifier(schema_id: str, data: dict) -> dict: ... ``` The return value is the mutated event data (dict). This data will be validated and emitted _after_ it is modified, so it still must follow the event's schema. Next, add this modifier to the event logger using the `.add_modifier` method: ```python logger = EventLogger() logger.add_modifier(my_modifier) ``` This method enforces the signature above and will raise a `ModifierError` if the signature does not match. jupyter_events-0.9.0/jupyter_events/__init__.py0000644000000000000000000000033613615410400016727 0ustar00# flake8: noqa from ._version import __version__ from .logger import EVENTS_METADATA_VERSION, EventLogger from .schema import EventSchema __all__ = ["__version__", "EVENTS_METADATA_VERSION", "EventLogger", "EventSchema"] jupyter_events-0.9.0/jupyter_events/_version.py0000644000000000000000000000120313615410400017006 0ustar00""" store the current version info of jupyter-events. """ import re from typing import List # Version string must appear intact for hatch versioning __version__ = "0.9.0" # Build up version_info tuple for backwards compatibility pattern = r"(?P\d+).(?P\d+).(?P\d+)(?P.*)" match = re.match(pattern, __version__) assert match is not None # noqa parts: List[object] = [int(match[part]) for part in ["major", "minor", "patch"]] if match["rest"]: parts.append(match["rest"]) version_info = tuple(parts) kernel_protocol_version_info = (5, 3) kernel_protocol_version = "{}.{}".format(*kernel_protocol_version_info) jupyter_events-0.9.0/jupyter_events/cli.py0000644000000000000000000000576413615410400015751 0ustar00"""The cli for jupyter events.""" import json import pathlib import platform import click from jsonschema import ValidationError from rich.console import Console from rich.json import JSON from rich.markup import escape from rich.padding import Padding from rich.style import Style from jupyter_events.schema import EventSchema, EventSchemaFileAbsent, EventSchemaLoadingError WIN = platform.system() == "Windows" class RC: """Return code enum.""" OK = 0 INVALID = 1 UNPARSABLE = 2 NOT_FOUND = 3 class EMOJI: """Terminal emoji enum""" X = "XX" if WIN else "\u274c" OK = "OK" if WIN else "\u2714" console = Console() error_console = Console(stderr=True) @click.group() @click.version_option() def main() -> None: """A simple CLI tool to quickly validate JSON schemas against Jupyter Event's custom validator. You can see Jupyter Event's meta-schema here: https://raw.githubusercontent.com/jupyter/jupyter_events/main/jupyter_events/schemas/event-metaschema.yml """ pass @click.command() @click.argument("schema") @click.pass_context def validate(ctx: click.Context, schema: str) -> int: """Validate a SCHEMA against Jupyter Event's meta schema. SCHEMA can be a JSON/YAML string or filepath to a schema. """ console.rule("Validating the following schema", style=Style(color="blue")) _schema = None try: # attempt to read schema as a serialized string _schema = EventSchema._load_schema(schema) except EventSchemaLoadingError: # pass here to avoid printing traceback of this exception if next block # excepts pass # if not a serialized schema string, try to interpret it as a path to schema file if _schema is None: schema_path = pathlib.Path(schema) try: _schema = EventSchema._load_schema(schema_path) except (EventSchemaLoadingError, EventSchemaFileAbsent) as e: # no need for full tracestack for user error exceptions. just print # the error message and return error_console.print(f"[bold red]ERROR[/]: {e}") return ctx.exit(RC.UNPARSABLE) # Print what was found. schema_json = JSON(json.dumps(_schema)) console.print(Padding(schema_json, (1, 0, 1, 4))) # Now validate this schema against the meta-schema. try: EventSchema(_schema) console.rule("Results", style=Style(color="green")) out = Padding(f"[green]{EMOJI.OK}[white] Nice work! This schema is valid.", (1, 0, 1, 0)) console.print(out) return ctx.exit(RC.OK) except ValidationError as err: error_console.rule("Results", style=Style(color="red")) error_console.print(f"[red]{EMOJI.X} [white]The schema failed to validate.") error_console.print("\nWe found the following error with your schema:") out = escape(str(err)) # type:ignore[assignment] error_console.print(Padding(out, (1, 0, 1, 4))) return ctx.exit(RC.INVALID) main.add_command(validate) jupyter_events-0.9.0/jupyter_events/logger.py0000644000000000000000000003532713615410400016457 0ustar00""" Emit structured, discrete events when various actions happen. """ from __future__ import annotations import asyncio import copy import json import logging import typing as t import warnings from datetime import datetime, timezone from jsonschema import ValidationError from pythonjsonlogger import jsonlogger from traitlets import Dict, Instance, Set, default from traitlets.config import Config, LoggingConfigurable from .schema import SchemaType from .schema_registry import SchemaRegistry from .traits import Handlers from .validators import JUPYTER_EVENTS_CORE_VALIDATOR # Increment this version when the metadata included with each event # changes. EVENTS_METADATA_VERSION = 1 class SchemaNotRegistered(Warning): """A warning to raise when an event is given to the logger but its schema has not be registered with the EventLogger """ class ModifierError(Exception): """An exception to raise when a modifier does not show the proper signature. """ class CoreMetadataError(Exception): """An exception raised when event core metadata is not valid.""" # Only show this warning on the first instance # of each event type that fails to emit. warnings.simplefilter("once", SchemaNotRegistered) class ListenerError(Exception): """An exception to raise when a listener does not show the proper signature. """ class EventLogger(LoggingConfigurable): """ An Event logger for emitting structured events. Event schemas must be registered with the EventLogger using the `register_schema` or `register_schema_file` methods. Every schema will be validated against Jupyter Event's metaschema. """ handlers = Handlers( default_value=None, allow_none=True, help="""A list of logging.Handler instances to send events to. When set to None (the default), all events are discarded. """, ).tag(config=True) schemas = Instance( SchemaRegistry, help="""The SchemaRegistry for caching validated schemas and their jsonschema validators. """, ) _modifiers = Dict({}, help="A mapping of schemas to their list of modifiers.") _modified_listeners = Dict({}, help="A mapping of schemas to the listeners of modified events.") _unmodified_listeners = Dict( {}, help="A mapping of schemas to the listeners of unmodified/raw events." ) _active_listeners: set[asyncio.Task[t.Any]] = Set() # type:ignore[assignment] async def gather_listeners(self) -> list[t.Any]: """Gather all of the active listeners.""" return await asyncio.gather( # type:ignore[no-any-return] *self._active_listeners, return_exceptions=True ) @default("schemas") def _default_schemas(self) -> SchemaRegistry: return SchemaRegistry() def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: """Initialize the logger.""" # We need to initialize the configurable before # adding the logging handlers. super().__init__(*args, **kwargs) # Use a unique name for the logger so that multiple instances of EventLog do not write # to each other's handlers. log_name = __name__ + "." + str(id(self)) self._logger = logging.getLogger(log_name) # We don't want events to show up in the default logs self._logger.propagate = False # We will use log.info to emit self._logger.setLevel(logging.INFO) # Add each handler to the logger and format the handlers. if self.handlers: for handler in self.handlers: self.register_handler(handler) def _load_config( self, cfg: Config, section_names: list[str] | None = None, traits: list[str] | None = None, # type:ignore[override] ) -> None: """Load EventLogger traits from a Config object, patching the handlers trait in the Config object to avoid deepcopy errors. """ my_cfg = self._find_my_config(cfg) handlers: list[logging.Handler] = my_cfg.pop("handlers", []) # Turn handlers list into a pickeable function def get_handlers() -> list[logging.Handler]: return handlers my_cfg["handlers"] = get_handlers # Build a new eventlog config object. eventlogger_cfg = Config({"EventLogger": my_cfg}) super()._load_config(eventlogger_cfg, section_names=None, traits=None) def register_event_schema(self, schema: SchemaType) -> None: """Register this schema with the schema registry. Get this registered schema using the EventLogger.schema.get() method. """ event_schema = self.schemas.register(schema) # type:ignore[arg-type] key = event_schema.id self._modifiers[key] = set() self._modified_listeners[key] = set() self._unmodified_listeners[key] = set() def register_handler(self, handler: logging.Handler) -> None: """Register a new logging handler to the Event Logger. All outgoing messages will be formatted as a JSON string. """ def _handle_message_field(record: t.Any, **kwargs: t.Any) -> str: """Python's logger always emits the "message" field with the value as "null" unless it's present in the schema/data. Message happens to be a common field for event logs, so special case it here and only emit it if "message" is found the in the schema's property list. """ schema = self.schemas.get(record["__schema__"]) if "message" not in schema.properties: del record["message"] return json.dumps(record, **kwargs) formatter = jsonlogger.JsonFormatter( # type:ignore [no-untyped-call] json_serializer=_handle_message_field, ) handler.setFormatter(formatter) self._logger.addHandler(handler) if handler not in self.handlers: self.handlers.append(handler) def remove_handler(self, handler: logging.Handler) -> None: """Remove a logging handler from the logger and list of handlers.""" self._logger.removeHandler(handler) if handler in self.handlers: self.handlers.remove(handler) def add_modifier( self, *, schema_id: str | None = None, modifier: t.Callable[[str, dict[str, t.Any]], dict[str, t.Any]], ) -> None: """Add a modifier (callable) to a registered event. Parameters ---------- modifier: Callable A callable function/method that executes when the named event occurs. This method enforces a string signature for modifiers: (schema_id: str, data: dict) -> dict: """ # Ensure that this is a callable function/method if not callable(modifier): msg = "`modifier` must be a callable" # type:ignore[unreachable] raise TypeError(msg) # If the schema ID and version is given, only add # this modifier to that schema if schema_id: self._modifiers[schema_id].add(modifier) return for id_ in self._modifiers: if schema_id is None or id_ == schema_id: self._modifiers[id_].add(modifier) def remove_modifier( self, *, schema_id: str | None = None, modifier: t.Callable[[str, dict[str, t.Any]], dict[str, t.Any]], ) -> None: """Remove a modifier from an event or all events. Parameters ---------- schema_id: str If given, remove this modifier only for a specific event type. modifier: Callable[[str, dict], dict] The modifier to remove. """ # If schema_id is given remove the modifier from this schema. if schema_id: self._modifiers[schema_id].discard(modifier) # If no schema_id is given, remove the modifier from all events. else: for schema_id in self.schemas.schema_ids: # Remove the modifier if it is found in the list. self._modifiers[schema_id].discard(modifier) self._modifiers[schema_id].discard(modifier) def add_listener( self, *, modified: bool = True, schema_id: str | None = None, listener: t.Callable[[EventLogger, str, dict[str, t.Any]], t.Coroutine[t.Any, t.Any, None]], ) -> None: """Add a listener (callable) to a registered event. Parameters ---------- modified: bool If True (default), listens to the data after it has been mutated/modified by the list of modifiers. schema_id: str $id of the schema listener: Callable A callable function/method that executes when the named event occurs. """ if not callable(listener): msg = "`listener` must be a callable" # type:ignore[unreachable] raise TypeError(msg) # If the schema ID and version is given, only add # this modifier to that schema if schema_id: if modified: self._modified_listeners[schema_id].add(listener) return self._unmodified_listeners[schema_id].add(listener) for id_ in self.schemas.schema_ids: if schema_id is None or id_ == schema_id: if modified: self._modified_listeners[id_].add(listener) else: self._unmodified_listeners[id_].add(listener) def remove_listener( self, *, schema_id: str | None = None, listener: t.Callable[[EventLogger, str, dict[str, t.Any]], t.Coroutine[t.Any, t.Any, None]], ) -> None: """Remove a listener from an event or all events. Parameters ---------- schema_id: str If given, remove this modifier only for a specific event type. listener: Callable[[EventLogger, str, dict], dict] The modifier to remove. """ # If schema_id is given remove the listener from this schema. if schema_id: self._modified_listeners[schema_id].discard(listener) self._unmodified_listeners[schema_id].discard(listener) # If no schema_id is given, remove the listener from all events. else: for schema_id in self.schemas.schema_ids: # Remove the listener if it is found in the list. self._modified_listeners[schema_id].discard(listener) self._unmodified_listeners[schema_id].discard(listener) def emit( self, *, schema_id: str, data: dict[str, t.Any], timestamp_override: datetime | None = None ) -> dict[str, t.Any] | None: """ Record given event with schema has occurred. Parameters ---------- schema_id: str $id of the schema data: dict The event to record timestamp_override: datetime, optional Optionally override the event timestamp. By default it is set to the current timestamp. Returns ------- dict The recorded event data """ # If no handlers are routing these events, there's no need to proceed. if ( not self.handlers and not self._modified_listeners[schema_id] and not self._unmodified_listeners[schema_id] ): return None # If the schema hasn't been registered, raise a warning to make sure # this was intended. if schema_id not in self.schemas: warnings.warn( f"{schema_id} has not been registered yet. If " "this was not intentional, please register the schema using the " "`register_event_schema` method.", SchemaNotRegistered, stacklevel=2, ) return None schema = self.schemas.get(schema_id) # Deep copy the data and modify the copy. modified_data = copy.deepcopy(data) for modifier in self._modifiers[schema.id]: modified_data = modifier(schema_id=schema_id, data=modified_data) if self._unmodified_listeners[schema.id]: # Process this event, i.e. validate and modify (in place) self.schemas.validate_event(schema_id, data) # Validate the modified data. self.schemas.validate_event(schema_id, modified_data) # Generate the empty event capsule. timestamp = ( datetime.now(tz=timezone.utc) if timestamp_override is None else timestamp_override ) capsule = { "__timestamp__": timestamp.isoformat() + "Z", "__schema__": schema_id, "__schema_version__": schema.version, "__metadata_version__": EVENTS_METADATA_VERSION, } try: JUPYTER_EVENTS_CORE_VALIDATOR.validate(capsule) except ValidationError as err: raise CoreMetadataError from err capsule.update(modified_data) self._logger.info(capsule) # callback for removing from finished listeners # from active listeners set. def _listener_task_done(task: asyncio.Task[t.Any]) -> None: # If an exception happens, log it to the main # applications logger err = task.exception() if err: self.log.error(err) self._active_listeners.discard(task) # Loop over listeners and execute them. for listener in self._modified_listeners[schema_id]: # Schedule this listener as a task and add # it to the list of active listeners task = asyncio.create_task( listener( logger=self, schema_id=schema_id, data=modified_data, ) ) self._active_listeners.add(task) # Adds the task and cleans it up later if needed. task.add_done_callback(_listener_task_done) for listener in self._unmodified_listeners[schema_id]: task = asyncio.create_task(listener(logger=self, schema_id=schema_id, data=data)) self._active_listeners.add(task) # Remove task from active listeners once its finished. def _listener_task_done(task: asyncio.Task[t.Any]) -> None: # If an exception happens, log it to the main # applications logger err = task.exception() if err: self.log.error(err) self._active_listeners.discard(task) # Adds the task and cleans it up later if needed. task.add_done_callback(_listener_task_done) return capsule jupyter_events-0.9.0/jupyter_events/py.typed0000644000000000000000000000000013615410400016301 0ustar00jupyter_events-0.9.0/jupyter_events/pytest_plugin.py0000644000000000000000000000336213615410400020100 0ustar00"""Fixtures for use with jupyter events.""" from __future__ import annotations import io import json import logging from typing import Any, Callable import pytest from jupyter_events import EventLogger @pytest.fixture def jp_event_sink() -> io.StringIO: """A stream for capture events.""" return io.StringIO() @pytest.fixture def jp_event_handler(jp_event_sink: io.StringIO) -> logging.Handler: """A logging handler that captures any events emitted by the event handler""" return logging.StreamHandler(jp_event_sink) @pytest.fixture def jp_read_emitted_events( jp_event_handler: logging.Handler, jp_event_sink: io.StringIO ) -> Callable[..., list[str] | None]: """Reads list of events since last time it was called.""" def _read() -> list[str] | None: jp_event_handler.flush() event_buf = jp_event_sink.getvalue().strip() output = [json.loads(item) for item in event_buf.split("\n")] if event_buf else None # Clear the sink. jp_event_sink.truncate(0) jp_event_sink.seek(0) return output return _read @pytest.fixture def jp_event_schemas() -> list[Any]: """A list of schema references. Each item should be one of the following: - string of serialized JSON/YAML content representing a schema - a pathlib.Path object pointing to a schema file on disk - a dictionary with the schema data. """ return [] @pytest.fixture def jp_event_logger(jp_event_handler: logging.Handler, jp_event_schemas: list[Any]) -> EventLogger: """A pre-configured event logger for tests.""" logger = EventLogger() for schema in jp_event_schemas: logger.register_event_schema(schema) logger.register_handler(handler=jp_event_handler) return logger jupyter_events-0.9.0/jupyter_events/schema.py0000644000000000000000000001246413615410400016435 0ustar00"""Event schema objects.""" from __future__ import annotations import json from pathlib import Path, PurePath from typing import Any, Union from jsonschema import FormatChecker, validators from referencing import Registry from referencing.jsonschema import DRAFT7 try: from jsonschema.protocols import Validator except ImportError: Validator = Any # type:ignore[assignment, misc] from . import yaml from .validators import draft7_format_checker, validate_schema class EventSchemaUnrecognized(Exception): # noqa """An error for an unrecognized event schema.""" pass class EventSchemaLoadingError(Exception): """An error for an event schema loading error.""" pass class EventSchemaFileAbsent(Exception): # noqa """An error for an absent event schema file.""" pass SchemaType = Union[dict, str, PurePath] class EventSchema: """A validated schema that can be used. On instantiation, validate the schema against Jupyter Event's metaschema. Parameters ---------- schema: dict or str JSON schema to validate against Jupyter Events. validator_class: jsonschema.validators The validator class from jsonschema used to validate instances of this event schema. The schema itself will be validated against Jupyter Event's metaschema to ensure that any schema registered here follows the expected form of Jupyter Events. registry: Registry for nested JSON schema references. """ def __init__( self, schema: SchemaType, validator_class: type[Validator] = validators.Draft7Validator, # type:ignore[assignment] format_checker: FormatChecker = draft7_format_checker, registry: Registry[Any] | None = None, ): """Initialize an event schema.""" _schema = self._load_schema(schema) # Validate the schema against Jupyter Events metaschema. validate_schema(_schema) if registry is None: registry = DRAFT7.create_resource(_schema) @ Registry() # Create a validator for this schema self._validator = validator_class(_schema, registry=registry, format_checker=format_checker) # type: ignore[call-arg] self._schema = _schema def __repr__(self) -> str: """A string repr for an event schema.""" return json.dumps(self._schema, indent=2) @staticmethod def _ensure_yaml_loaded(schema: SchemaType, was_str: bool = False) -> None: """Ensures schema was correctly loaded into a dictionary. Raises EventSchemaLoadingError otherwise.""" if isinstance(schema, dict): return error_msg = "Could not deserialize schema into a dictionary." def intended_as_path(schema: str) -> bool: path = Path(schema) return path.match("*.yml") or path.match("*.yaml") or path.match("*.json") # detect whether the user specified a string but intended a PurePath to # generate a more helpful error message if was_str and intended_as_path(schema): # type:ignore[arg-type] error_msg += " Paths to schema files must be explicitly wrapped in a Pathlib object." else: error_msg += " Double check the schema and ensure it is in the proper form." raise EventSchemaLoadingError(error_msg) @staticmethod def _load_schema(schema: SchemaType) -> dict[str, Any]: """Load a JSON schema from different sources/data types. `schema` could be a dictionary or serialized string representing the schema itself or a Pathlib object representing a schema file on disk. Returns a dictionary with schema data. """ # if schema is already a dictionary, return it if isinstance(schema, dict): return schema # if schema is PurePath, ensure file exists at path and then load from file if isinstance(schema, PurePath): if not Path(schema).exists(): msg = f'Schema file not present at path "{schema}".' raise EventSchemaFileAbsent(msg) loaded_schema = yaml.load(schema) EventSchema._ensure_yaml_loaded(loaded_schema) return loaded_schema # type:ignore[no-any-return] # finally, if schema is string, attempt to deserialize and return the output if isinstance(schema, str): # note the diff b/w load v.s. loads loaded_schema = yaml.loads(schema) EventSchema._ensure_yaml_loaded(loaded_schema, was_str=True) return loaded_schema # type:ignore[no-any-return] msg = f"Expected a dictionary, string, or PurePath, but instead received {schema.__class__.__name__}." # type:ignore[unreachable] raise EventSchemaUnrecognized(msg) @property def id(self) -> str: # noqa """Schema $id field.""" return self._schema["$id"] # type:ignore[no-any-return] @property def version(self) -> int: """Schema's version.""" return self._schema["version"] # type:ignore[no-any-return] @property def properties(self) -> dict[str, Any]: return self._schema["properties"] # type:ignore[no-any-return] def validate(self, data: dict[str, Any]) -> None: """Validate an incoming instance of this event schema.""" self._validator.validate(data) jupyter_events-0.9.0/jupyter_events/schema_registry.py0000644000000000000000000000533013615410400020357 0ustar00""""An event schema registry.""" from __future__ import annotations from typing import Any from .schema import EventSchema class SchemaRegistryException(Exception): # noqa: N818 """Exception class for Jupyter Events Schema Registry Errors.""" class SchemaRegistry: """A convenient API for storing and searching a group of schemas.""" def __init__(self, schemas: dict[str, EventSchema] | None = None): """Initialize the registry.""" self._schemas: dict[str, EventSchema] = schemas or {} def __contains__(self, key: str) -> bool: """Syntax sugar to check if a schema is found in the registry""" return key in self._schemas def __repr__(self) -> str: """The str repr of the registry.""" return ",\n".join([str(s) for s in self._schemas.values()]) def _add(self, schema_obj: EventSchema) -> None: if schema_obj.id in self._schemas: msg = ( f"The schema, {schema_obj.id}, is already " "registered. Try removing it and registering it again." ) raise SchemaRegistryException(msg) self._schemas[schema_obj.id] = schema_obj @property def schema_ids(self) -> list[str]: return list(self._schemas.keys()) def register(self, schema: dict[str, Any] | (str | EventSchema)) -> EventSchema: """Add a valid schema to the registry. All schemas are validated against the Jupyter Events meta-schema found here: """ if not isinstance(schema, EventSchema): schema = EventSchema(schema) self._add(schema) return schema def get(self, id_: str) -> EventSchema: """Fetch a given schema. If the schema is not found, this will raise a KeyError. """ try: return self._schemas[id_] except KeyError: msg = ( f"The requested schema, {id_}, was not found in the " "schema registry. Are you sure it was previously registered?" ) raise KeyError(msg) from None def remove(self, id_: str) -> None: """Remove a given schema. If the schema is not found, this will raise a KeyError. """ try: del self._schemas[id_] except KeyError: msg = ( f"The requested schema, {id_}, was not found in the " "schema registry. Are you sure it was previously registered?" ) raise KeyError(msg) from None def validate_event(self, id_: str, data: dict[str, Any]) -> None: """Validate an event against a schema within this registry. """ schema = self.get(id_) schema.validate(data) jupyter_events-0.9.0/jupyter_events/traits.py0000644000000000000000000000337613615410400016505 0ustar00"""Trait types for events.""" from __future__ import annotations import logging import typing as t from traitlets import TraitError, TraitType baseclass = TraitType if t.TYPE_CHECKING: baseclass = TraitType[t.Any, t.Any] # type:ignore[misc] class Handlers(baseclass): # type:ignore[type-arg] """A trait that takes a list of logging handlers and converts it to a callable that returns that list (thus, making this trait pickleable). """ info_text = "a list of logging handlers" def validate_elements(self, obj: t.Any, value: t.Any) -> None: """Validate the elements of an object.""" if len(value) > 0: # Check that all elements are logging handlers. for el in value: if isinstance(el, logging.Handler) is False: self.element_error(obj) def element_error(self, obj: t.Any) -> None: """Raise an error for bad elements.""" msg = f"Elements in the '{self.name}' trait of an {obj.__class__.__name__} instance must be Python `logging` handler instances." raise TraitError(msg) def validate(self, obj: t.Any, value: t.Any) -> t.Any: """Validate an object.""" # If given a callable, call it and set the # value of this trait to the returned list. # Verify that the callable returns a list # of logging handler instances. if callable(value): out = value() self.validate_elements(obj, out) return out # If a list, check it's elements to verify # that each element is a logging handler instance. elif isinstance(value, list): self.validate_elements(obj, value) return value else: self.error(obj, value) jupyter_events-0.9.0/jupyter_events/validators.py0000644000000000000000000000451113615410400017337 0ustar00"""Event validators.""" from __future__ import annotations import pathlib from typing import Any import jsonschema from jsonschema import Draft7Validator, ValidationError from referencing import Registry from referencing.jsonschema import DRAFT7 from . import yaml draft7_format_checker = ( Draft7Validator.FORMAT_CHECKER if hasattr(Draft7Validator, "FORMAT_CHECKER") else jsonschema.draft7_format_checker ) METASCHEMA_PATH = pathlib.Path(__file__).parent.joinpath("schemas") EVENT_METASCHEMA_FILEPATH = METASCHEMA_PATH.joinpath("event-metaschema.yml") EVENT_METASCHEMA = yaml.load(EVENT_METASCHEMA_FILEPATH) EVENT_CORE_SCHEMA_FILEPATH = METASCHEMA_PATH.joinpath("event-core-schema.yml") EVENT_CORE_SCHEMA = yaml.load(EVENT_CORE_SCHEMA_FILEPATH) PROPERTY_METASCHEMA_FILEPATH = METASCHEMA_PATH.joinpath("property-metaschema.yml") PROPERTY_METASCHEMA = yaml.load(PROPERTY_METASCHEMA_FILEPATH) SCHEMA_STORE = { EVENT_METASCHEMA["$id"]: EVENT_METASCHEMA, PROPERTY_METASCHEMA["$id"]: PROPERTY_METASCHEMA, EVENT_CORE_SCHEMA["$id"]: EVENT_CORE_SCHEMA, } resources = [ DRAFT7.create_resource(each) for each in (EVENT_METASCHEMA, PROPERTY_METASCHEMA, EVENT_CORE_SCHEMA) ] METASCHEMA_REGISTRY: Registry[Any] = resources @ Registry() JUPYTER_EVENTS_SCHEMA_VALIDATOR = Draft7Validator( schema=EVENT_METASCHEMA, registry=METASCHEMA_REGISTRY, format_checker=draft7_format_checker, ) JUPYTER_EVENTS_CORE_VALIDATOR = Draft7Validator( schema=EVENT_CORE_SCHEMA, registry=METASCHEMA_REGISTRY, format_checker=draft7_format_checker, ) def validate_schema(schema: dict[str, Any]) -> None: """Validate a schema dict.""" try: # Validate the schema against Jupyter Events metaschema. JUPYTER_EVENTS_SCHEMA_VALIDATOR.validate(schema) except ValidationError as err: reserved_property_msg = " does not match '^(?!__.*)'" if reserved_property_msg in str(err): idx = str(err).find(reserved_property_msg) bad_property = str(err)[:idx].strip() msg = ( f"{bad_property} is an invalid property name because it " "starts with `__`. Properties starting with 'dunder' " "are reserved as special meta-fields for Jupyter Events to use." ) raise ValidationError(msg) from err raise err jupyter_events-0.9.0/jupyter_events/yaml.py0000644000000000000000000000200713615410400016127 0ustar00"""Yaml utilities.""" from __future__ import annotations from pathlib import Path, PurePath from typing import Any from yaml import dump as ydump from yaml import load as yload try: from yaml import CSafeDumper as SafeDumper from yaml import CSafeLoader as SafeLoader except ImportError: # pragma: no cover from yaml import SafeDumper, SafeLoader # type:ignore[assignment] def loads(stream: Any) -> Any: """Load yaml from a stream.""" return yload(stream, Loader=SafeLoader) def dumps(stream: Any) -> str: """Parse the first YAML document in a stream as an object.""" return ydump(stream, Dumper=SafeDumper) def load(fpath: str | PurePath) -> Any: """Load yaml from a file.""" # coerce PurePath into Path, then read its contents data = Path(str(fpath)).read_text(encoding="utf-8") return loads(data) def dump(data: Any, outpath: str | PurePath) -> None: """Parse the a YAML document in a file as an object.""" Path(outpath).write_text(dumps(data), encoding="utf-8") jupyter_events-0.9.0/jupyter_events/schemas/event-core-schema.yml0000644000000000000000000000110413615410400022263 0ustar00$schema: http://json-schema.org/draft-07/schema $id: http://event.jupyter.org/event-schema version: 1 title: Event Schema description: | A schema for validating any Jupyter Event. type: object properties: __metadata_version__: title: Metadata Version type: number const: 1 __schema_version__: title: Schema Version type: integer __schema__: title: Schema ID type: string __timestamp__: title: Event Timestamp type: string format: datetime required: - __metadata_version__ - __schema__ - __schema_version__ - __timestamp__ jupyter_events-0.9.0/jupyter_events/schemas/event-metaschema.yml0000644000000000000000000000116213615410400022210 0ustar00$schema: http://json-schema.org/draft-07/schema $id: http://event.jupyter.org/event-metaschema version: 1 title: Event Metaschema description: | A meta schema for validating that all registered Jupyter Event schemas are appropriately defined. type: object properties: version: type: integer title: type: string description: type: string properties: type: object additionalProperties: $ref: http://event.jupyter.org/property-metaschema propertyNames: pattern: ^(?!__.*) patternProperties: "\\$id": type: string format: uri required: - $id - version - properties jupyter_events-0.9.0/jupyter_events/schemas/property-metaschema.yml0000644000000000000000000000116013615410400022751 0ustar00$schema: http://json-schema.org/draft-07/schema $id: http://event.jupyter.org/property-metaschema version: 1 title: Property Metaschema description: | A metaschema for validating properties within an event schema properties: title: type: string description: type: string properties: type: object additionalProperties: $ref: http://event.jupyter.org/property-metaschema propertyNames: pattern: ^(?!__.*) items: $ref: http://event.jupyter.org/property-metaschema additionalProperties: $ref: http://event.jupyter.org/property-metaschema propertyNames: pattern: ^(?!__.*) jupyter_events-0.9.0/tests/__init__.py0000644000000000000000000000000013615410400014767 0ustar00jupyter_events-0.9.0/tests/conftest.py0000644000000000000000000000006213615410400015065 0ustar00pytest_plugins = ["jupyter_events.pytest_plugin"] jupyter_events-0.9.0/tests/test_cli.py0000644000000000000000000000365313615410400015057 0ustar00import os import pytest import jupyter_events from jupyter_events.cli import RC from .utils import SCHEMA_PATH NAME = "jupyter-events" VALIDATE = NAME, "validate" @pytest.fixture def cli(script_runner): def run_cli(*args, **kwargs): env = dict(os.environ) env.update(kwargs.pop("env", {})) env["PYTHONIOENCODING"] = "utf-8" kwargs["env"] = env return script_runner.run([NAME, *list(map(str, args))], **kwargs) return run_cli def test_cli_version(cli): ret = cli("--version") assert ret.success assert ret.stdout.strip() == f"{NAME}, version {jupyter_events.__version__}" def test_cli_help(cli): ret = cli("--help") assert ret.success assert f"Usage: {NAME}" in ret.stdout.strip() def test_cli_good(cli): """jupyter events validate path/to/my_schema.json""" ret = cli("validate", SCHEMA_PATH / "good/array.yaml") assert ret.success assert not ret.stderr.strip() assert "This schema is valid" in ret.stdout def test_cli_good_raw(cli): """jupyter events validate path/to/my_schema.json""" ret = cli("validate", (SCHEMA_PATH / "good/array.yaml").read_text(encoding="utf-8")) assert ret.success assert not ret.stderr.strip() assert "This schema is valid" in ret.stdout def test_cli_missing(cli): ret = cli("validate", SCHEMA_PATH / "bad/doesnt-exist.yaml") assert not ret.success assert ret.returncode == RC.UNPARSABLE assert "Schema file not present" in ret.stderr.strip() def test_cli_malformed(cli): ret = cli("validate", SCHEMA_PATH / "bad/invalid.yaml") assert not ret.success assert ret.returncode == RC.UNPARSABLE assert "Could not deserialize" in ret.stderr.strip() def test_cli_invalid(cli): ret = cli("validate", SCHEMA_PATH / "bad/reserved-property.yaml") assert not ret.success assert ret.returncode == RC.INVALID assert "The schema failed to validate" in ret.stderr.strip() jupyter_events-0.9.0/tests/test_listeners.py0000644000000000000000000001123513615410400016313 0ustar00import io import logging import pytest from jupyter_events.logger import EventLogger from jupyter_events.schema import EventSchema from .utils import SCHEMA_PATH @pytest.fixture def schema(): # Read schema from path. schema_path = SCHEMA_PATH / "good" / "basic.yaml" return EventSchema(schema=schema_path) @pytest.fixture def jp_event_schemas(schema): return [schema] async def test_listener_function(jp_event_logger, schema): event_logger = jp_event_logger listener_was_called = False async def my_listener(logger: EventLogger, schema_id: str, data: dict) -> None: nonlocal listener_was_called listener_was_called = True # Add the modifier event_logger.add_listener(schema_id=schema.id, listener=my_listener) event_logger.emit(schema_id=schema.id, data={"prop": "hello, world"}) await event_logger.gather_listeners() assert listener_was_called # Check that the active listeners are cleaned up. assert len(event_logger._active_listeners) == 0 async def test_listener_function_str_annotations(jp_event_logger, schema): event_logger = jp_event_logger listener_was_called = False async def my_listener(logger: "EventLogger", schema_id: "str", data: "dict") -> "None": nonlocal listener_was_called listener_was_called = True # Add the modifier event_logger.add_listener(schema_id=schema.id, listener=my_listener) event_logger.emit(schema_id=schema.id, data={"prop": "hello, world"}) await event_logger.gather_listeners() assert listener_was_called # Check that the active listeners are cleaned up. assert len(event_logger._active_listeners) == 0 async def test_remove_listener_function(jp_event_logger, schema): event_logger = jp_event_logger listener_was_called = False async def my_listener(logger: EventLogger, schema_id: str, data: dict) -> None: nonlocal listener_was_called listener_was_called = True # Add the modifier event_logger.add_listener(schema_id=schema.id, listener=my_listener) event_logger.emit(schema_id=schema.id, data={"prop": "hello, world"}) await event_logger.gather_listeners() assert listener_was_called # Check that the active listeners are cleaned up. assert len(event_logger._active_listeners) == 0 event_logger.remove_listener(listener=my_listener) assert len(event_logger._modified_listeners[schema.id]) == 0 assert len(event_logger._unmodified_listeners[schema.id]) == 0 async def test_listener_that_raises_exception(jp_event_logger, schema): event_logger = jp_event_logger # Get an application logger that will show the exception app_log = event_logger.log log_stream = io.StringIO() h = logging.StreamHandler(log_stream) app_log.addHandler(h) async def listener_raise_exception(logger: EventLogger, schema_id: str, data: dict) -> None: raise Exception("This failed") # noqa event_logger.add_listener(schema_id=schema.id, listener=listener_raise_exception) event_logger.emit(schema_id=schema.id, data={"prop": "hello, world"}) await event_logger.gather_listeners() # Check that the exception was printed to the logs h.flush() log_output = log_stream.getvalue() assert "This failed" in log_output # Check that the active listeners are cleaned up. assert len(event_logger._active_listeners) == 0 async def test_bad_listener_does_not_break_good_listener(jp_event_logger, schema): event_logger = jp_event_logger # Get an application logger that will show the exception app_log = event_logger.log log_stream = io.StringIO() h = logging.StreamHandler(log_stream) app_log.addHandler(h) listener_was_called = False async def listener_raise_exception(logger: EventLogger, schema_id: str, data: dict) -> None: raise Exception("This failed") # noqa async def my_listener(logger: EventLogger, schema_id: str, data: dict) -> None: nonlocal listener_was_called listener_was_called = True # Add a bad listener and a good listener and ensure that # emitting still works and the bad listener's exception is is logged. event_logger.add_listener(schema_id=schema.id, listener=listener_raise_exception) event_logger.add_listener(schema_id=schema.id, listener=my_listener) event_logger.emit(schema_id=schema.id, data={"prop": "hello, world"}) await event_logger.gather_listeners() # Check that the exception was printed to the logs h.flush() log_output = log_stream.getvalue() assert "This failed" in log_output assert listener_was_called # Check that the active listeners are cleaned up. assert len(event_logger._active_listeners) == 0 jupyter_events-0.9.0/tests/test_logger.py0000644000000000000000000003413613615410400015567 0ustar00import io import json import logging import sys from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock import jsonschema import pytest from jsonschema.exceptions import ValidationError from traitlets import TraitError from traitlets.config.loader import PyFileConfigLoader from jupyter_events import yaml from jupyter_events.logger import EventLogger from jupyter_events.schema_registry import SchemaRegistryException GOOD_CONFIG = """ import logging c.EventLogger.handlers = [ logging.StreamHandler() ] """ BAD_CONFIG = """ import logging c.EventLogger.handlers = [ 0 ] """ def get_config_from_file(path, content): # Write config file filename = "config.py" config_file = path / filename config_file.write_text(content) # Load written file. loader = PyFileConfigLoader(filename, path=str(path)) cfg = loader.load_config() return cfg def test_good_config_file(tmp_path): cfg = get_config_from_file(tmp_path, GOOD_CONFIG) # Pass config to EventLogger e = EventLogger(config=cfg) assert len(e.handlers) > 0 assert isinstance(e.handlers[0], logging.Handler) def test_bad_config_file(tmp_path): cfg = get_config_from_file(tmp_path, BAD_CONFIG) with pytest.raises(TraitError): EventLogger(config=cfg) def test_register_invalid_schema(): """ Invalid JSON Schemas should fail registration """ el = EventLogger() with pytest.raises(ValidationError): el.register_event_schema( { # Totally invalid "properties": True } ) def test_missing_required_properties(): """ id and $version are required properties in our schemas. They aren't required by JSON Schema itself """ el = EventLogger() with pytest.raises(ValidationError): el.register_event_schema({"properties": {}}) with pytest.raises(ValidationError): el.register_event_schema( { "$id": "something", "$version": 1, # This should been 'version' } ) def test_timestamp_override(): """ Simple test for overriding timestamp """ schema = { "$id": "http://test/test", "version": 1, "properties": { "something": { "type": "string", "title": "test", }, }, } output = io.StringIO() handler = logging.StreamHandler(output) el = EventLogger(handlers=[handler]) el.register_event_schema(schema) timestamp_override = datetime.now(tz=timezone.utc) - timedelta(days=1) el.emit( schema_id="http://test/test", data={"something": "blah"}, timestamp_override=timestamp_override, ) handler.flush() event_capsule = json.loads(output.getvalue()) assert event_capsule["__timestamp__"] == timestamp_override.isoformat() + "Z" def test_emit(): """ Simple test for emitting valid events """ schema = { "$id": "http://test/test", "version": 1, "properties": { "something": { "type": "string", "title": "test", } }, } output = io.StringIO() handler = logging.StreamHandler(output) el = EventLogger(handlers=[handler]) el.register_event_schema(schema) el.emit( schema_id="http://test/test", data={ "something": "blah", }, ) handler.flush() event_capsule = json.loads(output.getvalue()) assert "__timestamp__" in event_capsule # Remove timestamp from capsule when checking equality, since it is gonna vary del event_capsule["__timestamp__"] expected = { "__schema__": "http://test/test", "__schema_version__": 1, "__metadata_version__": 1, "something": "blah", } if sys.version_info >= (3, 12): expected["taskName"] = None assert event_capsule == expected def test_message_field(): """ Simple test for emitting an event with the literal property "message". """ schema = { "$id": "http://test/test", "version": 1, "properties": { "something": { "type": "string", "title": "test", }, "message": { "type": "string", "title": "test", }, }, } output = io.StringIO() handler = logging.StreamHandler(output) el = EventLogger(handlers=[handler]) el.register_event_schema(schema) el.emit( schema_id="http://test/test", data={"something": "blah", "message": "a message was seen"}, ) handler.flush() event_capsule = json.loads(output.getvalue()) assert "__timestamp__" in event_capsule # Remove timestamp from capsule when checking equality, since it is gonna vary del event_capsule["__timestamp__"] expected = { "__schema__": "http://test/test", "__schema_version__": 1, "__metadata_version__": 1, "something": "blah", "message": "a message was seen", } if sys.version_info >= (3, 12): expected["taskName"] = None assert event_capsule == expected def test_nested_message_field(): """ Simple test for emitting an event with the literal property "message". """ schema = { "$id": "http://test/test", "version": 1, "properties": { "thing": { "type": "object", "title": "thing", "properties": { "message": { "type": "string", "title": "message", }, }, }, }, } output = io.StringIO() handler = logging.StreamHandler(output) el = EventLogger(handlers=[handler]) el.register_event_schema(schema) el.emit( schema_id="http://test/test", data={"thing": {"message": "a nested message was seen"}}, ) handler.flush() event_capsule = json.loads(output.getvalue()) assert "__timestamp__" in event_capsule # Remove timestamp from capsule when checking equality, since it is gonna vary del event_capsule["__timestamp__"] expected = { "__schema__": "http://test/test", "__schema_version__": 1, "__metadata_version__": 1, "thing": {"message": "a nested message was seen"}, } if sys.version_info >= (3, 12): expected["taskName"] = None assert event_capsule == expected def test_register_event_schema(tmp_path): """ Register schema from a file """ schema = { "$id": "http://test/test", "version": 1, "type": "object", "properties": { "something": { "type": "string", "title": "test", }, }, } el = EventLogger() schema_file = tmp_path.joinpath("schema.yml") yaml.dump(schema, schema_file) el.register_event_schema(schema_file) assert "http://test/test" in el.schemas def test_register_event_schema_object(tmp_path): """ Register schema from a file """ schema = { "$id": "http://test/test", "version": 1, "type": "object", "properties": { "something": { "type": "string", "title": "test", }, }, } el = EventLogger() schema_file = tmp_path.joinpath("schema.yml") yaml.dump(schema, schema_file) el.register_event_schema(schema_file) assert "http://test/test" in el.schemas def test_emit_badschema(): """ Fail fast when an event doesn't conform to its schema """ schema = { "$id": "http://test/test", "version": 1, "type": "object", "properties": { "something": { "type": "string", "title": "test", }, "status": { "enum": ["success", "failure"], "title": "test 2", }, }, } el = EventLogger(handlers=[logging.NullHandler()]) el.register_event_schema(schema) with pytest.raises(jsonschema.ValidationError) as excinfo: el.emit(schema_id="http://test/test", data={"something": "blah", "status": "hi"}) assert "'hi' is not one of" in str(excinfo.value) def test_emit_badschema_format(): """ Fail fast when an event doesn't conform to a specific format """ schema = { "$id": "http://test/test", "version": 1, "type": "object", "properties": { "something": {"type": "string", "title": "test", "format": "date-time"}, }, } el = EventLogger(handlers=[logging.NullHandler()]) el.register_event_schema(schema) with pytest.raises(jsonschema.ValidationError) as excinfo: el.emit(schema_id="http://test/test", data={"something": "chucknorris"}) assert "'chucknorris' is not a 'date-time'" in str(excinfo.value) def test_unique_logger_instances(): schema0 = { "$id": "http://test/test0", "version": 1, "type": "object", "properties": { "something": { "type": "string", "title": "test", }, }, } schema1 = { "$id": "http://test/test1", "version": 1, "type": "object", "properties": { "something": { "type": "string", "title": "test", }, }, } output0 = io.StringIO() output1 = io.StringIO() handler0 = logging.StreamHandler(output0) handler1 = logging.StreamHandler(output1) el0 = EventLogger(handlers=[handler0]) el0.register_event_schema(schema0) el1 = EventLogger(handlers=[handler1]) el1.register_event_schema(schema1) el0.emit( schema_id="http://test/test0", data={ "something": "blah", }, ) el1.emit( schema_id="http://test/test1", data={ "something": "blah", }, ) handler0.flush() handler1.flush() event_capsule0 = json.loads(output0.getvalue()) assert "__timestamp__" in event_capsule0 # Remove timestamp from capsule when checking equality, since it is gonna vary del event_capsule0["__timestamp__"] expected = { "__schema__": "http://test/test0", "__schema_version__": 1, "__metadata_version__": 1, "something": "blah", } if sys.version_info >= (3, 12): expected["taskName"] = None assert event_capsule0 == expected event_capsule1 = json.loads(output1.getvalue()) assert "__timestamp__" in event_capsule1 # Remove timestamp from capsule when checking equality, since it is gonna vary del event_capsule1["__timestamp__"] expected = { "__schema__": "http://test/test1", "__schema_version__": 1, "__metadata_version__": 1, "something": "blah", } if sys.version_info >= (3, 12): expected["taskName"] = None assert event_capsule1 == expected def test_register_duplicate_schemas(): schema0 = { "$id": "http://test/test", "version": 1, "type": "object", "properties": { "something": { "type": "string", "title": "test", }, }, } schema1 = { "$id": "http://test/test", "version": 1, "type": "object", "properties": { "something": { "type": "string", "title": "test", }, }, } el = EventLogger() el.register_event_schema(schema0) with pytest.raises(SchemaRegistryException): el.register_event_schema(schema1) async def test_noop_emit(): """Tests that the emit method returns immediately if no handlers are listeners are mapped to the incoming event. This is important for performance. """ el = EventLogger() # The `emit` method calls `validate_event` if # it doesn't return immediately. We'll use the # MagicMock here to see if/when this method is called # to ensure `emit` is returning when it should. el.schemas.validate_event = MagicMock(name="validate_event") # type:ignore[method-assign] schema_id1 = "http://test/test" schema1 = { "$id": schema_id1, "version": 1, "type": "object", "properties": { "something": { "type": "string", "title": "test", }, }, } schema_id2 = "http://test/test2" schema2 = { "$id": schema_id2, "version": 1, "type": "object", "properties": { "something_elss": { "type": "string", "title": "test", }, }, } el.register_event_schema(schema1) el.register_event_schema(schema2) # No handlers or listeners are registered # So the validate_event method should not # be called. el.emit(schema_id=schema_id1, data={"something": "hello"}) el.schemas.validate_event.assert_not_called() # Register a handler and check that .emit # validates the method. handler = logging.StreamHandler() el.register_handler(handler) el.emit(schema_id=schema_id1, data={"something": "hello"}) el.schemas.validate_event.assert_called_once() # Reset el.remove_handler(handler) el.schemas.validate_event.reset_mock() assert el.schemas.validate_event.call_count == 0 # Create a listener and check that emit works async def listener(logger: EventLogger, schema_id: str, data: dict) -> None: return None el.add_listener(schema_id=schema_id1, listener=listener) el.emit(schema_id=schema_id1, data={"something": "hello"}) el.schemas.validate_event.assert_called_once() el.schemas.validate_event.reset_mock() assert el.schemas.validate_event.call_count == 0 # Emit a different event with no listeners or # handlers and make sure it returns immediately. el.emit(schema_id=schema_id2, data={"something_else": "hello again"}) el.schemas.validate_event.assert_not_called() jupyter_events-0.9.0/tests/test_modifiers.py0000644000000000000000000000431313615410400016263 0ustar00import pytest from jupyter_events.schema import EventSchema from .utils import SCHEMA_PATH @pytest.fixture def schema(): # Read schema from path. schema_path = SCHEMA_PATH / "good" / "user.yaml" return EventSchema(schema=schema_path) @pytest.fixture def jp_event_schemas(schema): return [schema] def test_modifier_function(schema, jp_event_logger, jp_read_emitted_events): event_logger = jp_event_logger def redactor(schema_id: str, data: dict) -> dict: if "username" in data: data["username"] = "" return data # Add the modifier event_logger.add_modifier(modifier=redactor) event_logger.emit(schema_id=schema.id, data={"username": "jovyan"}) output = jp_read_emitted_events()[0] assert "username" in output assert output["username"] == "" def test_modifier_method(schema, jp_event_logger, jp_read_emitted_events): event_logger = jp_event_logger class Redactor: def redact(self, schema_id: str, data: dict) -> dict: if "username" in data: data["username"] = "" return data redactor = Redactor() # Add the modifier event_logger.add_modifier(modifier=redactor.redact) event_logger.emit(schema_id=schema.id, data={"username": "jovyan"}) output = jp_read_emitted_events()[0] assert "username" in output assert output["username"] == "" def test_remove_modifier(schema, jp_event_logger, jp_read_emitted_events): event_logger = jp_event_logger def redactor(schema_id: str, data: dict) -> dict: if "username" in data: data["username"] = "" return data # Add the modifier event_logger.add_modifier(modifier=redactor) assert len(event_logger._modifiers) == 1 event_logger.emit(schema_id=schema.id, data={"username": "jovyan"}) output = jp_read_emitted_events()[0] assert "username" in output assert output["username"] == "" event_logger.remove_modifier(modifier=redactor) event_logger.emit(schema_id=schema.id, data={"username": "jovyan"}) output = jp_read_emitted_events()[0] assert "username" in output assert output["username"] == "jovyan" jupyter_events-0.9.0/tests/test_schema.py0000644000000000000000000000600013615410400015535 0ustar00import os from pathlib import Path import pytest from jsonschema.exceptions import ValidationError from jupyter_events import yaml from jupyter_events.schema import ( EventSchema, EventSchemaFileAbsent, EventSchemaLoadingError, EventSchemaUnrecognized, ) from jupyter_events.validators import validate_schema from .utils import SCHEMA_PATH BAD_SCHEMAS = [ ["reserved-property.yaml", "Properties starting with 'dunder'"], ["nested-reserved-property.yaml", "Properties starting with 'dunder'"], ["bad-id.yaml", "'not-a-uri' is not a 'uri'"], ] GOOD_SCHEMAS = ["array.yaml", "nested-array.yaml", "basic.yaml"] @pytest.mark.parametrize("schema_file,validation_error_msg", BAD_SCHEMAS) def test_bad_validations(schema_file, validation_error_msg): """ Validation fails because the schema is missing a redactionPolicies field. """ # Read the schema file with open(SCHEMA_PATH / "bad" / schema_file) as f: schema = yaml.loads(f) # Assert that the schema files for a known reason. with pytest.raises(ValidationError) as err: validate_schema(schema) assert validation_error_msg in err.value.message def test_file_absent(): """Validation fails because file does not exist at path.""" with pytest.raises(EventSchemaFileAbsent): EventSchema(Path("asdf.txt")) def test_string_intended_as_path(): """Ensure EventSchema returns a helpful error message if user passes a string intended as a Path.""" expected_msg_contents = "Paths to schema files must be explicitly wrapped in a Pathlib object." str_path = os.path.join(SCHEMA_PATH, "good", "some_schema.yaml") with pytest.raises(EventSchemaLoadingError) as e: EventSchema(str_path) assert expected_msg_contents in str(e) def test_unrecognized_type(): """Validation fails because file is not of valid type.""" with pytest.raises(EventSchemaUnrecognized): EventSchema(9001) # type:ignore[arg-type] def test_invalid_yaml(): """Validation fails because deserialized schema is not a dictionary.""" path = SCHEMA_PATH / "bad" / "invalid.yaml" with pytest.raises(EventSchemaLoadingError): EventSchema(path) def test_valid_json(): """Ensure EventSchema accepts JSON files.""" path = SCHEMA_PATH / "good" / "basic.json" EventSchema(path) @pytest.mark.parametrize("schema_file", GOOD_SCHEMAS) def test_good_validations(schema_file): """Ensure validation passes for good schemas.""" # Read the schema file with open(SCHEMA_PATH / "good" / schema_file) as f: schema = yaml.loads(f) # assert that no exception gets raised validate_schema(schema) @pytest.mark.parametrize( "schema", [ # Non existent paths "non-existent-file.yml", "non/existent/path", "non/existent/path/file.yaml", # Valid yaml string, but not a valid object "random string", ], ) def test_loading_string_error(schema): with pytest.raises(EventSchemaLoadingError): EventSchema(schema) jupyter_events-0.9.0/tests/test_traits.py0000644000000000000000000000121713615410400015610 0ustar00import logging import pytest from traitlets import HasTraits, TraitError from jupyter_events.traits import Handlers class HasHandlers(HasTraits): handlers = Handlers(None, allow_none=True) def test_good_handlers_value(): handlers = [logging.NullHandler(), logging.NullHandler()] obj = HasHandlers(handlers=handlers) assert obj.handlers == handlers def test_bad_handlers_values(): handlers = [0, 1] with pytest.raises(TraitError): HasHandlers(handlers=handlers) def test_mixed_handlers_values(): handlers = [logging.NullHandler(), 1] with pytest.raises(TraitError): HasHandlers(handlers=handlers) jupyter_events-0.9.0/tests/utils.py0000644000000000000000000000135613615410400014407 0ustar00import io import json import logging import pathlib from copy import deepcopy from jupyter_events.logger import EventLogger SCHEMA_PATH = pathlib.Path(__file__).parent / "schemas" def get_event_data(event, schema, schema_id, version, unredacted_policies): sink = io.StringIO() # Create a handler that captures+records events with allowed tags. handler = logging.StreamHandler(sink) e = EventLogger(handlers=[handler], unredacted_policies=unredacted_policies) e.register_event_schema(schema) # Record event and read output e.emit(schema_id=schema_id, data=deepcopy(event)) recorded_event = json.loads(sink.getvalue()) return {key: value for key, value in recorded_event.items() if not key.startswith("__")} jupyter_events-0.9.0/tests/schemas/bad/bad-id.yaml0000644000000000000000000000031413615410400017043 0ustar00$id: not-a-uri version: 1 title: Schema with a Bad URI ID description: | A schema with a bad id type: object properties: bad: title: Test Property description: Test property. type: string jupyter_events-0.9.0/tests/schemas/bad/invalid.yaml0000644000000000000000000000002113615410400017344 0ustar00418 i'm a teapot jupyter_events-0.9.0/tests/schemas/bad/nested-reserved-property.yaml0000644000000000000000000000126713615410400022714 0ustar00$id: http://event.jupyter.org/test version: 1 title: Schema with Array description: | A schema for an array of objects. type: object properties: users: title: Test User Array description: | Test User array. type: array items: type: object title: User properties: name: type: string title: Name hobbies: type: array title: Hobbies items: type: object title: Hobby properties: __badName: title: Sport Name type: string position: title: Position type: string jupyter_events-0.9.0/tests/schemas/bad/reserved-property.yaml0000644000000000000000000000034513615410400021430 0ustar00$id: http://event.jupyter.org/test version: 1 title: Simple Test Schema description: | A simple schema for testing type: object properties: __badName: title: Test Property description: Test property. type: string jupyter_events-0.9.0/tests/schemas/good/array.yaml0000644000000000000000000000065213615410400017250 0ustar00$id: http://event.jupyter.org/test version: 1 title: Schema with Array description: | A schema for an array of objects. type: object properties: users: title: Test User Array description: | Test User array. type: array items: type: object title: User properties: email: type: string title: Email id: type: string title: Name jupyter_events-0.9.0/tests/schemas/good/basic.json0000644000000000000000000000045313615410400017221 0ustar00{ "$id": "http://event.jupyter.org/test", "version": 1, "title": "Simple Test Schema", "description": "A simple schema for testing", "type": "object", "properties": { "prop": { "title": "Test Property", "description": "Test property.", "type": "string" } } } jupyter_events-0.9.0/tests/schemas/good/basic.yaml0000644000000000000000000000034013615410400017205 0ustar00$id: http://event.jupyter.org/test version: 1 title: Simple Test Schema description: | A simple schema for testing type: object properties: prop: title: Test Property description: Test property. type: string jupyter_events-0.9.0/tests/schemas/good/nested-array.yaml0000644000000000000000000000126313615410400020527 0ustar00$id: http://event.jupyter.org/test version: 1 title: Schema with Array description: | A schema for an array of objects. type: object properties: users: title: Test User Array description: | Test User array. type: array items: type: object title: User properties: name: type: string title: Name hobbies: type: array title: Hobbies items: type: object title: Hobby properties: sport: title: Sport Name type: string position: title: Position type: string jupyter_events-0.9.0/tests/schemas/good/user.yaml0000644000000000000000000000027613615410400017112 0ustar00$id: http://event.jupyter.org/user version: 1 title: User description: | A User model. type: object properties: username: title: Username description: Username. type: string jupyter_events-0.9.0/.gitignore0000644000000000000000000000233113615410400013515 0ustar00# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .DS_Store .vscode/ # pycharm .idea/ jupyter_events-0.9.0/LICENSE0000644000000000000000000000277613615410400012547 0ustar00BSD 3-Clause License Copyright (c) 2022-, Jupyter Development Team All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. jupyter_events-0.9.0/README.md0000644000000000000000000000430513615410400013007 0ustar00# Jupyter Events [![Build Status](https://github.com/jupyter/jupyter_events/actions/workflows/python-tests.yml/badge.svg?query=branch%3Amain++)](https://github.com/jupyter/jupyter_events/actions/workflows/python-tests.yml/badge.svg?query=branch%3Amain++) [![Documentation Status](https://readthedocs.org/projects/jupyter-events/badge/?version=latest)](http://jupyter-events.readthedocs.io/en/latest/?badge=latest) _An event system for Jupyter Applications and extensions._ Jupyter Events enables Jupyter Python Applications (e.g. Jupyter Server, JupyterLab Server, JupyterHub, etc.) to emit **events**—structured data describing things happening inside the application. Other software (e.g. client applications like JupyterLab) can _listen_ and respond to these events. ## Install Install Jupyter Events directly from PyPI: ``` pip install jupyter_events ``` or conda-forge: ``` conda install -c conda-forge jupyter_events ``` ## Documentation Documentation is available at [jupyter-events.readthedocs.io](https://jupyter-events.readthedocs.io). ## About the Jupyter Development Team The Jupyter Development Team is the set of all contributors to the Jupyter project. This includes all of the Jupyter subprojects. The core team that coordinates development on GitHub can be found here: https://github.com/jupyter/. ## Our Copyright Policy Jupyter uses a shared copyright model. Each contributor maintains copyright over their contributions to Jupyter. But, it is important to note that these contributions are typically only changes to the repositories. Thus, the Jupyter source code, in its entirety is not the copyright of any single person or institution. Instead, it is the collective copyright of the entire Jupyter Development Team. If individual contributors want to maintain a record of what changes/contributions they have specific copyright on, they should indicate their copyright in the commit message of the change, when they commit the change to one of the Jupyter repositories. With this in mind, the following banner should be used in any source code file to indicate the copyright and license terms: ``` # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. ``` jupyter_events-0.9.0/pyproject.toml0000644000000000000000000001245013615410400014444 0ustar00[build-system] requires = ["hatchling>=1.5"] build-backend = "hatchling.build" [project] name = "jupyter-events" description = "Jupyter Event System library" readme = "README.md" requires-python = ">=3.8" authors = [ { name = "Jupyter Development Team", email = "jupyter@googlegroups.com" }, ] keywords = [ "Jupyter", "JupyterLab", ] classifiers = [ "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Intended Audience :: System Administrators", "License :: OSI Approved :: BSD License", "Programming Language :: Python", "Programming Language :: Python :: 3", ] dependencies = [ "referencing", "jsonschema[format-nongpl]>=4.18.0", "python-json-logger>=2.0.4", "pyyaml>=5.3", "traitlets>=5.3", # The following are necessary to address an issue where pyproject.toml normalizes extra dependencies # such that 'format_nongpl' is normalized to 'format-nongpl' which prevents these two validators from # from being installed when jsonschema is <= 4.9 because jsonschema uses 'format_nongpl' in those releases. "rfc3339-validator", "rfc3986-validator>=0.1.1", ] dynamic = [ "version", ] [project.license] file = 'LICENSE' [project.urls] Homepage = "http://jupyter.org" documentation = "https://jupyter-events.readthedocs.io/" repository = "https://github.com/jupyter/jupyter_events.git" changelog = "https://github.com/jupyter/jupyter_events/blob/main/CHANGELOG.md" [project.scripts] jupyter-events = "jupyter_events.cli:main" [project.optional-dependencies] docs = [ "jupyterlite-sphinx", "myst_parser", "pydata_sphinx_theme", "sphinxcontrib-spelling", ] test = [ "pre-commit", "pytest-asyncio>=0.19.0", "pytest-console-scripts", "pytest>=7.0", # [cli] "click", "rich", ] cli = [ "click", "rich" ] [tool.hatch.version] path = "jupyter_events/_version.py" [tool.hatch.envs.docs] features = ["docs"] [tool.hatch.envs.docs.scripts] build = "make -C docs html SPHINXOPTS='-W'" [tool.hatch.envs.test] features = ["test"] [tool.hatch.envs.test.scripts] test = "python -m pytest -vv {args}" nowarn = "test -W default {args}" [tool.hatch.envs.cov] features = ["test"] dependencies = ["coverage[toml]", "pytest-cov"] [tool.hatch.envs.cov.scripts] test = "python -m pytest -vv --cov jupyter_events --cov-branch --cov-report term-missing:skip-covered {args}" nowarn = "test -W default {args}" [tool.hatch.envs.lint] detached = true dependencies = ["pre-commit"] [tool.hatch.envs.lint.scripts] build = "pre-commit run --all-files ruff" [tool.hatch.envs.typing] dependencies = [ "pre-commit"] detached = true [tool.hatch.envs.typing.scripts] test = "pre-commit run --all-files --hook-stage manual mypy" [tool.pytest.ini_options] minversion = "6.0" xfail_strict = true log_cli_level = "info" addopts = [ "-ra", "--durations=10", "--color=yes", "--doctest-modules", "--showlocals", "--strict-markers", "--strict-config" ] testpaths = [ "tests/" ] asyncio_mode = "auto" script_launch_mode = "subprocess" filterwarnings= [ # Fail on warnings "error", # Upstream warnings from python-dateutil "module:datetime.datetime.utc:DeprecationWarning", # Ignore importwarning on pypy for yaml "module:can't resolve package from __spec__ or __package__:ImportWarning", ] [tool.coverage.report] exclude_lines = [ "pragma: no cover", "def __repr__", "if self.debug:", "if settings.DEBUG", "raise AssertionError", "raise NotImplementedError", "if 0:", "if __name__ == .__main__.:", "class .*\bProtocol\\):", "@(abc\\.)?abstractmethod", ] [tool.coverage.run] relative_files = true source = ["jupyter_events"] [tool.mypy] files = "jupyter_events" python_version = "3.8" strict = true show_error_codes = true enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] warn_unreachable = true [tool.ruff] target-version = "py38" line-length = 100 [tool.ruff.lint] select = [ "A", "B", "C", "DTZ", "E", "EM", "F", "FBT", "I", "ICN", "N", "PLC", "PLE", "PLR", "PLW", "Q", "RUF", "S", "SIM", "T", "TID", "UP", "W", "YTT", ] ignore = [ # Q000 Single quotes found but double quotes preferred "Q000", # FBT001 Boolean positional arg in function definition "FBT001", "FBT002", "FBT003", # E501 Line too long (158 > 100 characters) "E501", # SIM105 Use `contextlib.suppress(...)` "SIM105", ] unfixable = [ # Don't touch print statements "T201", # Don't touch noqa lines "RUF100", ] [tool.ruff.lint.per-file-ignores] # B011 Do not call assert False since python -O removes these calls # F841 local variable 'foo' is assigned to but never used # C408 Unnecessary `dict` call # E402 Module level import not at top of file # T201 `print` found # B007 Loop control variable `i` not used within the loop body. # N802 Function name `assertIn` should be lowercase # F841 Local variable `t` is assigned to but never used # S101 Use of `assert` detected "tests/*" = ["B011", "F841", "C408", "E402", "T201", "B007", "N802", "F841", "S101"] # C901 Function is too complex "jupyter_events/logger.py" = ["C901"] # `emit` is too complex (12 > 10) [tool.interrogate] ignore-init-module=true ignore-private=true ignore-semiprivate=true ignore-property-decorators=true ignore-nested-functions=true ignore-nested-classes=true fail-under=100 exclude = ["docs", "tests"] [tool.repo-review] ignore = ["PY007", "GH102"] jupyter_events-0.9.0/PKG-INFO0000644000000000000000000001312113615410400012621 0ustar00Metadata-Version: 2.1 Name: jupyter-events Version: 0.9.0 Summary: Jupyter Event System library Project-URL: Homepage, http://jupyter.org Project-URL: documentation, https://jupyter-events.readthedocs.io/ Project-URL: repository, https://github.com/jupyter/jupyter_events.git Project-URL: changelog, https://github.com/jupyter/jupyter_events/blob/main/CHANGELOG.md Author-email: Jupyter Development Team License: BSD 3-Clause License Copyright (c) 2022-, Jupyter Development Team All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. License-File: LICENSE Keywords: Jupyter,JupyterLab Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Science/Research Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Requires-Python: >=3.8 Requires-Dist: jsonschema[format-nongpl]>=4.18.0 Requires-Dist: python-json-logger>=2.0.4 Requires-Dist: pyyaml>=5.3 Requires-Dist: referencing Requires-Dist: rfc3339-validator Requires-Dist: rfc3986-validator>=0.1.1 Requires-Dist: traitlets>=5.3 Provides-Extra: cli Requires-Dist: click; extra == 'cli' Requires-Dist: rich; extra == 'cli' Provides-Extra: docs Requires-Dist: jupyterlite-sphinx; extra == 'docs' Requires-Dist: myst-parser; extra == 'docs' Requires-Dist: pydata-sphinx-theme; extra == 'docs' Requires-Dist: sphinxcontrib-spelling; extra == 'docs' Provides-Extra: test Requires-Dist: click; extra == 'test' Requires-Dist: pre-commit; extra == 'test' Requires-Dist: pytest-asyncio>=0.19.0; extra == 'test' Requires-Dist: pytest-console-scripts; extra == 'test' Requires-Dist: pytest>=7.0; extra == 'test' Requires-Dist: rich; extra == 'test' Description-Content-Type: text/markdown # Jupyter Events [![Build Status](https://github.com/jupyter/jupyter_events/actions/workflows/python-tests.yml/badge.svg?query=branch%3Amain++)](https://github.com/jupyter/jupyter_events/actions/workflows/python-tests.yml/badge.svg?query=branch%3Amain++) [![Documentation Status](https://readthedocs.org/projects/jupyter-events/badge/?version=latest)](http://jupyter-events.readthedocs.io/en/latest/?badge=latest) _An event system for Jupyter Applications and extensions._ Jupyter Events enables Jupyter Python Applications (e.g. Jupyter Server, JupyterLab Server, JupyterHub, etc.) to emit **events**—structured data describing things happening inside the application. Other software (e.g. client applications like JupyterLab) can _listen_ and respond to these events. ## Install Install Jupyter Events directly from PyPI: ``` pip install jupyter_events ``` or conda-forge: ``` conda install -c conda-forge jupyter_events ``` ## Documentation Documentation is available at [jupyter-events.readthedocs.io](https://jupyter-events.readthedocs.io). ## About the Jupyter Development Team The Jupyter Development Team is the set of all contributors to the Jupyter project. This includes all of the Jupyter subprojects. The core team that coordinates development on GitHub can be found here: https://github.com/jupyter/. ## Our Copyright Policy Jupyter uses a shared copyright model. Each contributor maintains copyright over their contributions to Jupyter. But, it is important to note that these contributions are typically only changes to the repositories. Thus, the Jupyter source code, in its entirety is not the copyright of any single person or institution. Instead, it is the collective copyright of the entire Jupyter Development Team. If individual contributors want to maintain a record of what changes/contributions they have specific copyright on, they should indicate their copyright in the commit message of the change, when they commit the change to one of the Jupyter repositories. With this in mind, the following banner should be used in any source code file to indicate the copyright and license terms: ``` # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. ```