Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: jupyter/jupyter_events
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.6.3
Choose a base ref
...
head repository: jupyter/jupyter_events
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref

Commits on Jan 19, 2023

  1. [pre-commit.ci] pre-commit autoupdate (#57)

    * [pre-commit.ci] pre-commit autoupdate
    
    updates:
    - [github.com/charliermarsh/ruff-pre-commit: v0.0.194 → v0.0.207](astral-sh/ruff-pre-commit@v0.0.194...v0.0.207)
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    Co-authored-by: Steven Silvester <steven.silvester@ieee.org>
    Co-authored-by: Min RK <benjaminrk@gmail.com>
    3 people authored Jan 19, 2023
    Copy the full SHA
    05471c6 View commit details

Commits on Jan 29, 2023

  1. Add more linting (#65)

    blink1073 authored Jan 29, 2023
    Copy the full SHA
    27c87f5 View commit details

Commits on Feb 7, 2023

  1. [pre-commit.ci] pre-commit autoupdate (#66)

    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    Co-authored-by: Steven Silvester <steven.silvester@ieee.org>
    pre-commit-ci[bot] and blink1073 authored Feb 7, 2023
    Copy the full SHA
    c0ea8b0 View commit details

Commits on Feb 17, 2023

  1. Clean up license (#67)

    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    dcsaba89 and pre-commit-ci[bot] authored Feb 17, 2023
    Copy the full SHA
    d3a37e9 View commit details

Commits on Mar 7, 2023

  1. [pre-commit.ci] pre-commit autoupdate (#69)

    * [pre-commit.ci] pre-commit autoupdate
    
    updates:
    - [github.com/charliermarsh/ruff-pre-commit: v0.0.243 → v0.0.254](astral-sh/ruff-pre-commit@v0.0.243...v0.0.254)
    
    * Update ruff in pyproject.toml, remove blanket directive
    
    * Remove unnecessary lint directive
    
    ---------
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    Co-authored-by: Kevin Bates <kbates4@gmail.com>
    pre-commit-ci[bot] and kevin-bates authored Mar 7, 2023
    Copy the full SHA
    100a292 View commit details

Commits on Apr 4, 2023

  1. [pre-commit.ci] pre-commit autoupdate (#71)

    * [pre-commit.ci] pre-commit autoupdate
    
    updates:
    - [github.com/python-jsonschema/check-jsonschema: 0.21.0 → 0.22.0](python-jsonschema/check-jsonschema@0.21.0...0.22.0)
    - [github.com/psf/black: 23.1.0 → 23.3.0](psf/black@23.1.0...23.3.0)
    - [github.com/charliermarsh/ruff-pre-commit: v0.0.254 → v0.0.260](astral-sh/ruff-pre-commit@v0.0.254...v0.0.260)
    
    * [pre-commit.ci] auto fixes from pre-commit.com hooks
    
    for more information, see https://pre-commit.ci
    
    * Address remaining lint issues
    
    ---------
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    Co-authored-by: Kevin Bates <kbates4@gmail.com>
    pre-commit-ci[bot] and kevin-bates authored Apr 4, 2023
    Copy the full SHA
    00d2062 View commit details

Commits on Apr 11, 2023

  1. allow a 'message' field in an event schema (#72)

    * allow a 'message' field in an event schema
    * add unit test for nested message field; update private docstring
    Zsailer authored Apr 11, 2023
    Copy the full SHA
    ebbebf8 View commit details

Commits on Apr 13, 2023

  1. Use local coverage (#73)

    blink1073 authored Apr 13, 2023
    Copy the full SHA
    181371f View commit details

Commits on Apr 17, 2023

  1. Improve usability of jp_read_emitted_events fixture (#74)

    * Make fixture fp_read_emitted_events more friendly
    
    * Set handlers default value to None to match doc string
    kevin-bates authored Apr 17, 2023
    Copy the full SHA
    a0082af View commit details

Commits on May 6, 2023

  1. [pre-commit.ci] pre-commit autoupdate (#76)

    * [pre-commit.ci] pre-commit autoupdate
    
    updates:
    - [github.com/charliermarsh/ruff-pre-commit: v0.0.260 → v0.0.263](astral-sh/ruff-pre-commit@v0.0.260...v0.0.263)
    
    * lint
    
    ---------
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    Co-authored-by: Steven Silvester <steven.silvester@ieee.org>
    pre-commit-ci[bot] and blink1073 authored May 6, 2023
    Copy the full SHA
    c93e6a0 View commit details

Commits on Jun 6, 2023

  1. [pre-commit.ci] pre-commit autoupdate (#77)

    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    Co-authored-by: Steven Silvester <steven.silvester@ieee.org>
    pre-commit-ci[bot] and blink1073 authored Jun 6, 2023
    Copy the full SHA
    a5238e4 View commit details

Commits on Jul 10, 2023

  1. [pre-commit.ci] pre-commit autoupdate (#78)

    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    pre-commit-ci[bot] authored Jul 10, 2023
    Copy the full SHA
    0b5e0e8 View commit details

Commits on Jul 29, 2023

  1. Migrate RefResolver to referencing.Registry (#80)

    Co-authored-by: Julian Berman <Julian@GrayVines.com>
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    Co-authored-by: Steven Silvester <steven.silvester@ieee.org>
    4 people authored Jul 29, 2023
    Copy the full SHA
    56e7d26 View commit details

Commits on Jul 31, 2023

  1. Publish 0.7.0

    SHA256 hashes:
    
    jupyter_events-0.7.0-py3-none-any.whl: 4753da434c13a37c3f3c89b500afa0c0a6241633441421f6adafe2fb2e2b924e
    
    jupyter_events-0.7.0.tar.gz: 7be27f54b8388c03eefea123a4f79247c5b9381c49fb1cd48615ee191eb12615
    blink1073 committed Jul 31, 2023
    Copy the full SHA
    3c22b52 View commit details

Commits on Aug 1, 2023

  1. Add more PyPI URLs (#82)

    pydanny authored Aug 1, 2023
    Copy the full SHA
    16289eb View commit details
  2. [pre-commit.ci] pre-commit autoupdate (#83)

    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    Co-authored-by: Steven Silvester <steven.silvester@ieee.org>
    pre-commit-ci[bot] and blink1073 authored Aug 1, 2023
    Copy the full SHA
    d2690d5 View commit details

Commits on Sep 5, 2023

  1. Bump actions/checkout from 3 to 4 (#84)

    Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
    - [Release notes](https://github.com/actions/checkout/releases)
    - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
    - [Commits](actions/checkout@v3...v4)
    
    ---
    updated-dependencies:
    - dependency-name: actions/checkout
      dependency-type: direct:production
      update-type: version-update:semver-major
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Sep 5, 2023
    Copy the full SHA
    bf43903 View commit details
  2. [pre-commit.ci] pre-commit autoupdate (#85)

    * [pre-commit.ci] pre-commit autoupdate
    
    updates:
    - [github.com/python-jsonschema/check-jsonschema: 0.23.3 → 0.26.3](python-jsonschema/check-jsonschema@0.23.3...0.26.3)
    - [github.com/executablebooks/mdformat: 0.7.16 → 0.7.17](hukkin/mdformat@0.7.16...0.7.17)
    - [github.com/astral-sh/ruff-pre-commit: v0.0.281 → v0.0.287](astral-sh/ruff-pre-commit@v0.0.281...v0.0.287)
    
    * sync deps and lint
    
    ---------
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    Co-authored-by: Steven Silvester <steven.silvester@ieee.org>
    pre-commit-ci[bot] and blink1073 authored Sep 5, 2023
    Copy the full SHA
    cad58f1 View commit details

Commits on Oct 4, 2023

  1. [pre-commit.ci] pre-commit autoupdate (#87)

    * [pre-commit.ci] pre-commit autoupdate
    
    updates:
    - [github.com/python-jsonschema/check-jsonschema: 0.26.3 → 0.27.0](python-jsonschema/check-jsonschema@0.26.3...0.27.0)
    - [github.com/psf/black: 23.7.0 → 23.9.1](psf/black@23.7.0...23.9.1)
    - [github.com/astral-sh/ruff-pre-commit: v0.0.287 → v0.0.292](astral-sh/ruff-pre-commit@v0.0.287...v0.0.292)
    
    * typing
    
    ---------
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    Co-authored-by: Steven Silvester <steven.silvester@ieee.org>
    pre-commit-ci[bot] and blink1073 authored Oct 4, 2023
    Copy the full SHA
    9eaf857 View commit details

Commits on Oct 10, 2023

  1. Copy the full SHA
    eb711c3 View commit details

Commits on Oct 15, 2023

  1. Adopt sp-repo-review (#89)

    * adopt sp-repo-review
    
    * add readthedocs config
    
    * wip address typing
    
    * wip address typing
    
    * update tests
    
    * restore indents
    
    * handle warning and typings
    
    * fix types and do not run coverage on pypy
    blink1073 authored Oct 15, 2023
    Copy the full SHA
    e3edb6a View commit details

Commits on Oct 16, 2023

  1. Publish 0.8.0

    SHA256 hashes:
    
    jupyter_events-0.8.0-py3-none-any.whl: 81f07375c7673ff298bfb9302b4a981864ec64edaed75ca0fe6f850b9b045525
    
    jupyter_events-0.8.0.tar.gz: fda08f0defce5e16930542ce60634ba48e010830d50073c3dfd235759cee77bf
    blink1073 committed Oct 16, 2023
    Copy the full SHA
    9b99f67 View commit details

Commits on Oct 29, 2023

  1. Adopt ruff format (#90)

    blink1073 authored Oct 29, 2023
    Copy the full SHA
    63a1291 View commit details

Commits on Nov 5, 2023

  1. Copy the full SHA
    7126b45 View commit details

Commits on Nov 6, 2023

  1. Copy the full SHA
    228a048 View commit details
  2. Publish 0.9.0

    SHA256 hashes:
    
    jupyter_events-0.9.0-py3-none-any.whl: d853b3c10273ff9bc8bb8b30076d65e2c9685579db736873de6c2232dde148bf
    
    jupyter_events-0.9.0.tar.gz: 81ad2e4bc710881ec274d31c6c50669d71bbaa5dd9d01e600b56faa85700d399
    blink1073 committed Nov 6, 2023
    Copy the full SHA
    be7b834 View commit details

Commits on Nov 9, 2023

  1. chore: update pre-commit hooks (#92)

    * chore: update pre-commit hooks
    
    updates:
    - [github.com/python-jsonschema/check-jsonschema: 0.27.0 → 0.27.1](python-jsonschema/check-jsonschema@0.27.0...0.27.1)
    - [github.com/astral-sh/ruff-pre-commit: v0.1.3 → v0.1.4](astral-sh/ruff-pre-commit@v0.1.3...v0.1.4)
    
    * update ruff
    
    * [pre-commit.ci] auto fixes from pre-commit.com hooks
    
    for more information, see https://pre-commit.ci
    
    * update ruff config
    
    ---------
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    Co-authored-by: Steven Silvester <steven.silvester@ieee.org>
    pre-commit-ci[bot] and blink1073 authored Nov 9, 2023
    Copy the full SHA
    dd5dfff View commit details

Commits on Dec 1, 2023

  1. Copy the full SHA
    5c95f94 View commit details

Commits on Jan 2, 2024

  1. chore: update pre-commit hooks (#94)

    * chore: update pre-commit hooks
    
    updates:
    - [github.com/python-jsonschema/check-jsonschema: 0.27.2 → 0.27.3](python-jsonschema/check-jsonschema@0.27.2...0.27.3)
    - [github.com/pre-commit/mirrors-prettier: v3.0.3 → v4.0.0-alpha.8](pre-commit/mirrors-prettier@v3.0.3...v4.0.0-alpha.8)
    - [github.com/pre-commit/mirrors-mypy: v1.7.1 → v1.8.0](pre-commit/mirrors-mypy@v1.7.1...v1.8.0)
    - [github.com/astral-sh/ruff-pre-commit: v0.1.6 → v0.1.9](astral-sh/ruff-pre-commit@v0.1.6...v0.1.9)
    - [github.com/scientific-python/cookie: 2023.11.17 → 2023.12.21](scientific-python/cookie@2023.11.17...2023.12.21)
    
    * update config
    
    * [pre-commit.ci] auto fixes from pre-commit.com hooks
    
    for more information, see https://pre-commit.ci
    
    ---------
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    Co-authored-by: Steven Silvester <steven.silvester@ieee.org>
    pre-commit-ci[bot] and blink1073 authored Jan 2, 2024
    Copy the full SHA
    e0f689b View commit details

Commits on Feb 18, 2024

  1. chore: update pre-commit hooks (#95)

    updates:
    - [github.com/python-jsonschema/check-jsonschema: 0.27.3 → 0.27.4](python-jsonschema/check-jsonschema@0.27.3...0.27.4)
    - [github.com/astral-sh/ruff-pre-commit: v0.1.9 → v0.2.0](astral-sh/ruff-pre-commit@v0.1.9...v0.2.0)
    - [github.com/scientific-python/cookie: 2023.12.21 → 2024.01.24](scientific-python/cookie@2023.12.21...2024.01.24)
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    pre-commit-ci[bot] authored Feb 18, 2024
    Copy the full SHA
    43f8a50 View commit details

Commits on Mar 12, 2024

  1. Copy the full SHA
    014a91c View commit details
  2. Publish 0.9.1

    SHA256 hashes:
    
    jupyter_events-0.9.1-py3-none-any.whl: e51f43d2c25c2ddf02d7f7a5045f71fc1d5cb5ad04ef6db20da961c077654b9b
    
    jupyter_events-0.9.1.tar.gz: a52e86f59eb317ee71ff2d7500c94b963b8a24f0b7a1517e2e653e24258e15c7
    blink1073 committed Mar 12, 2024
    Copy the full SHA
    8654e87 View commit details

Commits on Mar 18, 2024

  1. Enable adding listeners to event before the event is registered (#97)

    * Enable adding listeners to event before the event is registered
    
    * pre-commit issues
    
    * use pytest.warns when schema is not registered
    Zsailer authored Mar 18, 2024
    Copy the full SHA
    e7784fd View commit details
  2. Publish 0.10.0

    SHA256 hashes:
    
    jupyter_events-0.10.0-py3-none-any.whl: 4b72130875e59d57716d327ea70d3ebc3af1944d3717e5a498b8a06c6c159960
    
    jupyter_events-0.10.0.tar.gz: 670b8229d3cc882ec782144ed22e0d29e1c2d639263f92ca8383e66682845e22
    Zsailer committed Mar 18, 2024
    Copy the full SHA
    76433e7 View commit details

Commits on May 10, 2024

  1. Copy the full SHA
    5b3cdf2 View commit details

Commits on Oct 29, 2024

  1. Copy the full SHA
    08ff0b9 View commit details

Commits on Dec 17, 2024

  1. Fix typo (#102)

    * Fix typo
    
    * Again
    davidbrochart authored Dec 17, 2024
    Copy the full SHA
    bf1dc11 View commit details
  2. Fix DeprecationWarning with patched python-json-logger (#103)

    * Fix DeprecationWarning with patched python-json-logger
    
    Running tests with https://github.com/nhairs/python-json-logger 3.1.0
    produces:
    
      DeprecationWarning: pythonjsonlogger.jsonlogger has been moved to pythonjsonlogger.json
    
    It's easy enough to be compatible with both.
    
    * [pre-commit.ci] auto fixes from pre-commit.com hooks
    
    for more information, see https://pre-commit.ci
    
    * be explict about versions for later cleanup
    
    * fix
    
    * remove weird taskName
    
    * pre-commit
    
    * min version 3.9
    
    * min py version
    
    * fix lints
    
    * bump versions
    
    * min pyversion 3.9
    
    * min pyversion 3.9
    
    * dont build docs on window
    
    ---------
    
    Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
    Co-authored-by: M Bussonnier <bussonniermatthias@gmail.com>
    3 people authored Dec 17, 2024
    Copy the full SHA
    93d37c6 View commit details
  3. test on 3.13 (#106)

    Carreau authored Dec 17, 2024
    Copy the full SHA
    f613554 View commit details
  4. Switch schema version type to str (#104)

    * Switch schema `version` type to `str`
    
    Type coerce `version` to be a string
    
    * lints
    
    * pass interoogate
    
    ---------
    
    Co-authored-by: M Bussonnier <bussonniermatthias@gmail.com>
    afshin and Carreau authored Dec 17, 2024
    Copy the full SHA
    56da5c4 View commit details
  5. Fix typo and remove double check. (#107)

    There is no need to check that all tests are passing as they all are
    displayed in the github UI and required to merge.
    Carreau authored Dec 17, 2024
    Copy the full SHA
    f801261 View commit details
  6. Publish 0.11.0

    SHA256 hashes:
    
    jupyter_events-0.11.0-py3-none-any.whl: 36399b41ce1ca45fe8b8271067d6a140ffa54cec4028e95491c93b78a855cacf
    
    jupyter_events-0.11.0.tar.gz: c0bc56a37aac29c1fbc3bcfbddb8c8c49533f9cf11f1c4e6adadba936574ab90
    Carreau committed Dec 17, 2024
    Copy the full SHA
    9964fd8 View commit details

Commits on Dec 18, 2024

  1. Copy the full SHA
    21d4694 View commit details

Commits on Jan 6, 2025

  1. Copy the full SHA
    6704ea6 View commit details

Commits on Feb 3, 2025

  1. Publish 0.12.0

    SHA256 hashes:
    
    jupyter_events-0.12.0-py3-none-any.whl: 6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb
    
    jupyter_events-0.12.0.tar.gz: fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b
    Zsailer committed Feb 3, 2025
    Copy the full SHA
    0861062 View commit details

Commits on Mar 21, 2025

  1. Add security.md (#113)

    Carreau authored Mar 21, 2025
    Copy the full SHA
    1f0340a View commit details
Showing with 969 additions and 549 deletions.
  1. +8 −0 .github/dependabot.yml
  2. +8 −1 .github/workflows/prep-release.yml
  3. +34 −0 .github/workflows/publish-changelog.yml
  4. +12 −10 .github/workflows/publish-release.yml
  5. +29 −39 .github/workflows/python-tests.yml
  6. +3 −0 .gitignore
  7. +61 −9 .pre-commit-config.yaml
  8. +14 −0 .readthedocs.yaml
  9. +143 −2 CHANGELOG.md
  10. +0 −61 COPYING.md
  11. +30 −0 LICENSE
  12. +28 −1 README.md
  13. +10 −0 SECURITY.md
  14. +0 −9 codecov.yml
  15. +4 −4 docs/conf.py
  16. +30 −27 docs/demo/demo-notebook.ipynb
  17. +5 −6 docs/user_guide/application.md
  18. +1 −1 docs/user_guide/configure.md
  19. +4 −4 docs/user_guide/defining-schema.md
  20. +3 −3 docs/user_guide/event-schemas.md
  21. +4 −8 docs/user_guide/first-event.md
  22. +4 −1 docs/user_guide/listeners.md
  23. +5 −4 jupyter_events/_version.py
  24. +6 −5 jupyter_events/cli.py
  25. +99 −102 jupyter_events/logger.py
  26. +13 −8 jupyter_events/pytest_plugin.py
  27. +31 −27 jupyter_events/schema.py
  28. +17 −15 jupyter_events/schema_registry.py
  29. +2 −2 jupyter_events/schemas/event-core-schema.yml
  30. +2 −2 jupyter_events/schemas/event-metaschema.yml
  31. +1 −1 jupyter_events/schemas/property-metaschema.yml
  32. +14 −7 jupyter_events/traits.py
  33. +8 −0 jupyter_events/utils.py
  34. +29 −8 jupyter_events/validators.py
  35. +9 −7 jupyter_events/yaml.py
  36. +89 −80 pyproject.toml
  37. +2 −0 tests/conftest.py
  38. +1 −1 tests/schemas/bad/bad-id.yaml
  39. +1 −1 tests/schemas/bad/nested-reserved-property.yaml
  40. +1 −1 tests/schemas/good/array.yaml
  41. +1 −1 tests/schemas/good/basic.json
  42. +1 −1 tests/schemas/good/basic.yaml
  43. +1 −1 tests/schemas/good/nested-array.yaml
  44. +1 −1 tests/schemas/good/user.yaml
  45. +5 −3 tests/test_cli.py
  46. +57 −18 tests/test_listeners.py
  47. +127 −24 tests/test_logger.py
  48. +2 −40 tests/test_modifiers.py
  49. +5 −3 tests/test_schema.py
  50. +2 −0 tests/test_traits.py
  51. +2 −0 tests/utils.py
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -4,7 +4,15 @@ updates:
directory: "/"
schedule:
interval: "weekly"
groups:
actions:
patterns:
- "*"
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
groups:
actions:
patterns:
- "*"
9 changes: 8 additions & 1 deletion .github/workflows/prep-release.yml
Original file line number Diff line number Diff line change
@@ -12,6 +12,10 @@ on:
post_version_spec:
description: "Post Version Specifier"
required: false
silent:
description: "Set a placeholder in the changelog and don't publish the release."
required: false
type: boolean
since:
description: "Use PRs with activity since this date or git reference"
required: false
@@ -22,15 +26,18 @@ on:
jobs:
prep_release:
runs-on: ubuntu-latest
permissions:
contents: write
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 }}
token: ${{ secrets.GITHUB_TOKEN }}
version_spec: ${{ github.event.inputs.version_spec }}
silent: ${{ github.event.inputs.silent }}
post_version_spec: ${{ github.event.inputs.post_version_spec }}
target: ${{ github.event.inputs.target }}
branch: ${{ github.event.inputs.branch }}
34 changes: 34 additions & 0 deletions .github/workflows/publish-changelog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: "Publish Changelog"
on:
release:
types: [published]

workflow_dispatch:
inputs:
branch:
description: "The branch to target"
required: false

jobs:
publish_changelog:
runs-on: ubuntu-latest
environment: release
steps:
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1

- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}

- name: Publish changelog
id: publish-changelog
uses: jupyter-server/jupyter_releaser/.github/actions/publish-changelog@v2
with:
token: ${{ steps.app-token.outputs.token }}
branch: ${{ github.event.inputs.branch }}

- name: "** Next Step **"
run: |
echo "Merge the changelog update PR: ${{ steps.publish-changelog.outputs.pr_url }}"
22 changes: 12 additions & 10 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
@@ -15,30 +15,32 @@ on:
jobs:
publish_release:
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write
steps:
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1

- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}

- 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 }}
token: ${{ steps.app-token.outputs.token }}
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
uses: jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2
with:
token: ${{ secrets.ADMIN_GITHUB_TOKEN }}
target: ${{ github.event.inputs.target }}
token: ${{ steps.app-token.outputs.token }}
release_url: ${{ steps.populate-release.outputs.release_url }}

- name: "** Next Step **"
68 changes: 29 additions & 39 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
@@ -18,49 +18,58 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.7", "3.11"]
python-version: ["3.9", "3.12", "3.13"]
include:
- os: windows-latest
python-version: "3.9"
- os: ubuntu-latest
python-version: "pypy-3.8"
python-version: "pypy-3.9"
- os: ubuntu-latest
python-version: "3.10"
- os: macos-latest
python-version: "3.8"
python-version: "3.11"
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
- name: Run Tests
run: hatch run cov:test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
- 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
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
with:
python_version: 3.12
- run: hatch run docs:build

test_lint:
name: Test Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
- name: Run Linters
run: |
- name: Run Linters
run: |
hatch run typing:test
hatch run lint:style
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@v3
- uses: actions/checkout@v4
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
- uses: jupyterlab/maintainer-tools/.github/actions/downstream-test@v1
with:
@@ -71,12 +80,12 @@ jobs:
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Base Setup
uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
with:
dependency_type: minimum
- name: Install with miniumum versions and optional deps
python_version: 3.9
- name: Install with minimum versions and optional deps
run: |
pip install -e .[test]
pip install jsonschema[format-nongpl,format_nongpl]
@@ -94,7 +103,7 @@ jobs:
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Base Setup
uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
with:
@@ -108,7 +117,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
- uses: jupyterlab/maintainer-tools/.github/actions/make-sdist@v1

@@ -127,15 +136,15 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- 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@v3
uses: actions/checkout@v4
- name: Base Setup
uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1
- name: Install Dependencies
@@ -146,22 +155,3 @@ jobs:
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:
- build
- 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) }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -105,3 +105,6 @@ venv.bak/

.DS_Store
.vscode/

# pycharm
.idea/
70 changes: 61 additions & 9 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
ci:
autoupdate_schedule: monthly
autoupdate_commit_msg: "chore: update pre-commit hooks"

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v4.5.0
hooks:
- id: check-case-conflict
- id: check-ast
@@ -20,22 +21,73 @@ repos:
- id: trailing-whitespace

- repo: https://github.com/python-jsonschema/check-jsonschema
rev: 0.19.2
rev: 0.27.4
hooks:
- id: check-github-workflows

- repo: https://github.com/executablebooks/mdformat
rev: 0.7.16
rev: 0.7.17
hooks:
- id: mdformat
additional_dependencies:
[mdformat-gfm, mdformat-frontmatter, mdformat-footnote]

- repo: https://github.com/psf/black
rev: 22.12.0
- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v4.0.0-alpha.8"
hooks:
- id: black
- id: prettier
types_or: [yaml, html, json]

- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.194
- 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.8.0"
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.2.0
hooks:
- id: ruff
args: ["--fix"]
types_or: [python, jupyter]
args: ["--fix", "--show-fixes"]
- id: ruff-format
types_or: [python, jupyter]

- repo: https://github.com/scientific-python/cookie
rev: "2024.01.24"
hooks:
- id: sp-repo-review
additional_dependencies: ["repo-review[cli]"]
args: ["--ignore", "GH102"]
14 changes: 14 additions & 0 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version: 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
145 changes: 143 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -4,6 +4,149 @@ All notable changes to this project will be documented in this file.

<!-- <START NEW CHANGELOG ENTRY> -->

## 0.12.0

([Full Changelog](https://github.com/jupyter/jupyter_events/compare/v0.11.0...6704ea630522f44542d83608f750da0068e41443))

### Bugs fixed

- pop taskName for older version of python-json-logger [#110](https://github.com/jupyter/jupyter_events/pull/110) ([@Carreau](https://github.com/Carreau))

### Maintenance and upkeep improvements

- declare dependency on packaging [#109](https://github.com/jupyter/jupyter_events/pull/109) ([@bollwyvl](https://github.com/bollwyvl))

### Contributors to this release

([GitHub contributors page for this release](https://github.com/jupyter/jupyter_events/graphs/contributors?from=2024-12-17&to=2025-02-03&type=c))

[@bollwyvl](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Abollwyvl+updated%3A2024-12-17..2025-02-03&type=Issues) | [@Carreau](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3ACarreau+updated%3A2024-12-17..2025-02-03&type=Issues)

<!-- <END NEW CHANGELOG ENTRY> -->

## 0.11.0

([Full Changelog](https://github.com/jupyter/jupyter_events/compare/v0.10.0...f8012610f86408908004febed9e0e06ef71ca951))

### Bugs fixed

- Switch schema `version` type to `str` [#104](https://github.com/jupyter/jupyter_events/pull/104) ([@afshin](https://github.com/afshin))
- Fix DeprecationWarning with patched python-json-logger [#103](https://github.com/jupyter/jupyter_events/pull/103) ([@cjwatson](https://github.com/cjwatson))
- Prevent unintended `KeyError` when emitting an unregistered event schema [#101](https://github.com/jupyter/jupyter_events/pull/101) ([@afshin](https://github.com/afshin))

### Maintenance and upkeep improvements

- Fix typo and remove double check. [#107](https://github.com/jupyter/jupyter_events/pull/107) ([@Carreau](https://github.com/Carreau))
- test on 3.13 [#106](https://github.com/jupyter/jupyter_events/pull/106) ([@Carreau](https://github.com/Carreau))

### Documentation improvements

- Fix typo [#102](https://github.com/jupyter/jupyter_events/pull/102) ([@davidbrochart](https://github.com/davidbrochart))
- Update notebook to match current version of jupyter-events [#98](https://github.com/jupyter/jupyter_events/pull/98) ([@manics](https://github.com/manics))

### Contributors to this release

([GitHub contributors page for this release](https://github.com/jupyter/jupyter_events/graphs/contributors?from=2024-03-18&to=2024-12-17&type=c))

[@afshin](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Aafshin+updated%3A2024-03-18..2024-12-17&type=Issues) | [@Carreau](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3ACarreau+updated%3A2024-03-18..2024-12-17&type=Issues) | [@cjwatson](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Acjwatson+updated%3A2024-03-18..2024-12-17&type=Issues) | [@davidbrochart](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Adavidbrochart+updated%3A2024-03-18..2024-12-17&type=Issues) | [@manics](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Amanics+updated%3A2024-03-18..2024-12-17&type=Issues) | [@Zsailer](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3AZsailer+updated%3A2024-03-18..2024-12-17&type=Issues)

## 0.10.0

([Full Changelog](https://github.com/jupyter/jupyter_events/compare/v0.9.1...e7784fd09356ef074d69d1c2f192f1ad96f5f00c))

### Enhancements made

- Enable adding listeners to event before the event is registered [#97](https://github.com/jupyter/jupyter_events/pull/97) ([@Zsailer](https://github.com/Zsailer))

### Contributors to this release

([GitHub contributors page for this release](https://github.com/jupyter/jupyter_events/graphs/contributors?from=2024-03-12&to=2024-03-18&type=c))

[@Zsailer](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3AZsailer+updated%3A2024-03-12..2024-03-18&type=Issues)

## 0.9.1

([Full Changelog](https://github.com/jupyter/jupyter_events/compare/v0.9.0...014a91c793b12d008bb744614a280bc14b5be7eb))

### Maintenance and upkeep improvements

- Update Release Scripts [#96](https://github.com/jupyter/jupyter_events/pull/96) ([@blink1073](https://github.com/blink1073))
- chore: update pre-commit hooks [#95](https://github.com/jupyter/jupyter_events/pull/95) ([@pre-commit-ci](https://github.com/pre-commit-ci))
- chore: update pre-commit hooks [#94](https://github.com/jupyter/jupyter_events/pull/94) ([@pre-commit-ci](https://github.com/pre-commit-ci))
- Update ruff and typing [#93](https://github.com/jupyter/jupyter_events/pull/93) ([@blink1073](https://github.com/blink1073))
- chore: update pre-commit hooks [#92](https://github.com/jupyter/jupyter_events/pull/92) ([@pre-commit-ci](https://github.com/pre-commit-ci))

### Contributors to this release

([GitHub contributors page for this release](https://github.com/jupyter/jupyter_events/graphs/contributors?from=2023-11-06&to=2024-03-12&type=c))

[@blink1073](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Ablink1073+updated%3A2023-11-06..2024-03-12&type=Issues) | [@pre-commit-ci](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Apre-commit-ci+updated%3A2023-11-06..2024-03-12&type=Issues)

## 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))
@@ -18,8 +161,6 @@ All notable changes to this project will be documented in this file.

[@blink1073](https://github.com/search?q=repo%3Ajupyter%2Fjupyter_events+involves%3Ablink1073+updated%3A2023-01-10..2023-01-12&type=Issues)

<!-- <END NEW CHANGELOG ENTRY> -->

## 0.6.2

([Full Changelog](https://github.com/jupyter/jupyter_events/compare/v0.6.1...a00859944090df5277f263fcfe72ae48b8cc2382))
61 changes: 0 additions & 61 deletions COPYING.md

This file was deleted.

30 changes: 30 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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.
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# 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++)
[![codecov](https://codecov.io/gh/jupyter/jupyter_events/branch/main/graph/badge.svg?token=S9WiBg2iL0)](https://codecov.io/gh/jupyter/jupyter_events)
[![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._
@@ -25,3 +24,31 @@ 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.
```
10 changes: 10 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Security Policy

## Reporting a Vulnerability

All IPython and Jupyter security are handled via security@ipython.org.
You can find more information on the Jupyter website. https://jupyter.org/security

## Tidelift

You can also report security concerns for jupyter-eveents via the [Tidelift platform](https://tidelift.com/security).
9 changes: 0 additions & 9 deletions codecov.yml

This file was deleted.

8 changes: 4 additions & 4 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -16,8 +16,7 @@


# -- Project information -----------------------------------------------------

from typing import List
from __future__ import annotations

project = "jupyter_events"
copyright = "2019, Project Jupyter"
@@ -29,10 +28,10 @@
# 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"]
extensions: list = ["myst_parser", "jupyterlite_sphinx"]

try:
import enchant # type:ignore # noqa
import enchant # noqa: F401

extensions += ["sphinxcontrib.spelling"]
except ImportError:
@@ -78,6 +77,7 @@
"logo": {
"text": "Jupyter Events",
},
"navigation_with_keys": False,
"icon_links": [
{
# Label for this link
57 changes: 30 additions & 27 deletions docs/demo/demo-notebook.ipynb
Original file line number Diff line number Diff line change
@@ -13,6 +13,8 @@
"metadata": {},
"outputs": [],
"source": [
"from __future__ import annotations\n",
"\n",
"import piplite\n",
"\n",
"await piplite.install(\"jupyter_events\")"
@@ -45,16 +47,15 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": null,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Validator class: Draft7Validator\n",
"Schema: {\n",
" \"$id\": \"myapplication.org/example-event\",\n",
"{\n",
" \"$id\": \"http://myapplication.org/example-event\",\n",
" \"version\": 1,\n",
" \"title\": \"Example Event\",\n",
" \"description\": \"An interesting event to collect\",\n",
@@ -71,7 +72,7 @@
"source": [
"schema = \"\"\"\n",
"$id: http://myapplication.org/example-event\n",
"version: 1\n",
"version: \"1\"\n",
"title: Example Event\n",
"description: An interesting event to collect\n",
"properties:\n",
@@ -99,7 +100,7 @@
"metadata": {},
"outputs": [],
"source": [
"# We will import one of the handlers from Python's logging libray\n",
"# We will import one of the handlers from Python's logging library\n",
"from logging import StreamHandler\n",
"\n",
"handler = StreamHandler()\n",
@@ -123,14 +124,14 @@
"name": "stderr",
"output_type": "stream",
"text": [
"{\"__timestamp__\": \"2022-08-11T22:46:22.248281Z\", \"__schema__\": \"myapplication.org/example-event\", \"__schema_version__\": 1, \"__metadata_version__\": 1, \"name\": \"My Event\"}\n"
"{\"__timestamp__\": \"2024-05-04T23:22:40.338884+00:00Z\", \"__schema__\": \"http://myapplication.org/example-event\", \"__schema_version__\": 1, \"__metadata_version__\": 1, \"name\": \"My Event\"}\n"
]
},
{
"data": {
"text/plain": [
"{'__timestamp__': '2022-08-11T22:46:22.248281Z',\n",
" '__schema__': 'myapplication.org/example-event',\n",
"{'__timestamp__': '2024-05-04T23:22:40.338884+00:00Z',\n",
" '__schema__': 'http://myapplication.org/example-event',\n",
" '__schema_version__': 1,\n",
" '__metadata_version__': 1,\n",
" 'name': 'My Event'}"
@@ -142,7 +143,7 @@
}
],
"source": [
"logger.emit(schema_id=\"myapplication.org/example-event\", data={\"name\": \"My Event\"})"
"logger.emit(schema_id=\"http://myapplication.org/example-event\", data={\"name\": \"My Event\"})"
]
},
{
@@ -158,12 +159,14 @@
"metadata": {},
"outputs": [],
"source": [
"def my_listener(data):\n",
" print(\"hello, from my_listener!\")\n",
"async def my_listener(logger: EventLogger, schema_id: str, data: dict) -> None:\n",
" print(\"hello, from my_listener\")\n",
" print(logger)\n",
" print(schema_id)\n",
" print(data)\n",
"\n",
"\n",
"logger.add_listener(schema_id=\"myapplication.org/example-event\", version=1, listener=my_listener)"
"logger.add_listener(schema_id=\"http://myapplication.org/example-event\", listener=my_listener)"
]
},
{
@@ -182,24 +185,14 @@
"name": "stderr",
"output_type": "stream",
"text": [
"{\"__timestamp__\": \"2022-08-11T22:46:28.947996Z\", \"__schema__\": \"myapplication.org/example-event\", \"__schema_version__\": 1, \"__metadata_version__\": 1, \"name\": \"My Event\"}\n"
]
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"hello world\n",
"EventListenerData(event_logger=<jupyter_events.logger.EventLogger object at 0x7fd07085dfd0>, schema_id='myapplication.org/example-event', version=1, data={'name': 'My Event'})\n",
"hello world\n",
"EventListenerData(event_logger=<jupyter_events.logger.EventLogger object at 0x7fd07085dfd0>, schema_id='myapplication.org/example-event', version=1, data={'name': 'My Event'})\n"
"{\"__timestamp__\": \"2024-05-04T23:22:40.400243+00:00Z\", \"__schema__\": \"http://myapplication.org/example-event\", \"__schema_version__\": 1, \"__metadata_version__\": 1, \"name\": \"My Event\"}\n"
]
},
{
"data": {
"text/plain": [
"{'__timestamp__': '2022-08-11T22:46:28.947996Z',\n",
" '__schema__': 'myapplication.org/example-event',\n",
"{'__timestamp__': '2024-05-04T23:22:40.400243+00:00Z',\n",
" '__schema__': 'http://myapplication.org/example-event',\n",
" '__schema_version__': 1,\n",
" '__metadata_version__': 1,\n",
" 'name': 'My Event'}"
@@ -208,10 +201,20 @@
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"hello, from my_listener\n",
"<jupyter_events.logger.EventLogger object at 0x7f2fa6cd35b0>\n",
"http://myapplication.org/example-event\n",
"{'name': 'My Event'}\n"
]
}
],
"source": [
"logger.emit(schema_id=\"myapplication.org/example-event\", version=1, data={\"name\": \"My Event\"})"
"logger.emit(schema_id=\"http://myapplication.org/example-event\", data={\"name\": \"My Event\"})"
]
},
{
11 changes: 5 additions & 6 deletions docs/user_guide/application.md
Original file line number Diff line number Diff line change
@@ -9,7 +9,6 @@ from jupyter_events import Event


class MyApplication(JupyterApp):

classes = [EventLogger, ...]
eventlogger = Instance(EventLogger)

@@ -23,7 +22,7 @@ Register an event schema with the logger.
```python
schema = """
$id: http://myapplication.org/my-method
version: 1
version: "1"
title: My Method Executed
description: My method was executed one time.
properties:
@@ -32,9 +31,7 @@ Register an event schema with the logger.
type: string
"""

self.eventlogger.register_event_schema(
schema=schema
)
self.eventlogger.register_event_schema(schema=schema)
```

Call `.emit(...)` within the application to emit an instance of the event.
@@ -44,7 +41,9 @@ Call `.emit(...)` within the application to emit an instance of the event.
# Do something
...
# Emit event telling listeners that this event happened.
self.eventlogger.emit(schema_id="myapplication.org/my-method", data={"msg": "Hello, world!"})
self.eventlogger.emit(
schema_id="myapplication.org/my-method", data={"msg": "Hello, world!"}
)
# Do something else...
...
```
2 changes: 1 addition & 1 deletion docs/user_guide/configure.md
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ This is usually done using a Jupyter configuration file, e.g. `jupyter_config.py
from logging import FileHandler

# Log events to a local file on disk.
handler = FileHandler('events.txt')
handler = FileHandler("events.txt")

# Explicitly list the types of events
# to record and what properties or what categories
8 changes: 4 additions & 4 deletions docs/user_guide/defining-schema.md
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ A common pattern is to define these schemas in separate files and register them
schema_filepath = Path("/path/to/schema.yaml")

logger = EventLogger()
logger.register_event_schema(schema_file)
logger.register_event_schema(schema_filepath)
```

Note that a file path passed to `register_event_schema()` **must** be a Pathlib
@@ -27,7 +27,7 @@ Beyond these required items, any valid JSON should be possible. Here is a simple

```yaml
$id: https://event.jupyter.org/example-event
version: 1
version: "1"
title: My Event
description: |
Some information about my event
@@ -67,7 +67,7 @@ The output will look like this, if it passes:
{
"$id": "http://event.jupyter.org/test",
"version": 1,
"version": "1",
"title": "Simple Test Schema",
"description": "A simple schema for testing\n",
"type": "object",
@@ -92,7 +92,7 @@ or this if fails:
{
"$id": "http://event.jupyter.org/test",
"version": 1,
"version": "1",
"title": "Simple Test Schema",
"description": "A simple schema for testing\n",
"type": "object",
6 changes: 3 additions & 3 deletions docs/user_guide/event-schemas.md
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ from jupyter_events.logger import EventLogger

schema = """
$id: http://myapplication.org/example-event
version: 1
version: "1"
title: Example Event
description: An interesting event to collect
properties:
@@ -38,7 +38,7 @@ print(logger.schemas)
Validator class: Draft7Validator
Schema: {
"$id": "myapplication.org/example-event",
"version": 1,
"version": "1",
"title": "Example Event",
"description": "An interesting event to collect",
"properties": {
@@ -52,4 +52,4 @@ Schema: {

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 explicity register with the logger.
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.
12 changes: 4 additions & 8 deletions docs/user_guide/first-event.md
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ To begin emitting events from a Python application, you need to tell the `EventL
```python
schema = """
$id: http://myapplication.org/example-event
version: 1
version: "1"
title: Example Event
description: An interesting event to collect
properties:
@@ -31,25 +31,21 @@ 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 libray
# 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.
The logger knows about the event and where to send it; all that's left is to emit an instance of the event! 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"
}
)
schema_id="http://myapplication.org/example-event", data={"name": "My Event"}
)
```

5 changes: 4 additions & 1 deletion docs/user_guide/listeners.md
Original file line number Diff line number Diff line change
@@ -11,14 +11,17 @@ 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)
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.
9 changes: 5 additions & 4 deletions jupyter_events/_version.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
"""
store the current version info of jupyter-events.
"""
from __future__ import annotations

import re
from typing import List

# Version string must appear intact for hatch versioning
__version__ = "0.6.3"
__version__ = "0.12.0"

# Build up version_info tuple for backwards compatibility
pattern = r"(?P<major>\d+).(?P<minor>\d+).(?P<patch>\d+)(?P<rest>.*)"
match = re.match(pattern, __version__)
assert match is not None
parts: List[object] = [int(match[part]) for part in ["major", "minor", "patch"]]
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 = "%s.%s" % kernel_protocol_version_info
kernel_protocol_version = "{}.{}".format(*kernel_protocol_version_info)
11 changes: 6 additions & 5 deletions jupyter_events/cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""The cli for jupyter events."""
from __future__ import annotations

import json
import pathlib
import platform
@@ -21,7 +23,7 @@ class RC:

OK = 0
INVALID = 1
UNPARSEABLE = 2
UNPARSABLE = 2
NOT_FOUND = 3


@@ -38,15 +40,14 @@ class EMOJI:

@click.group()
@click.version_option()
def main():
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()
@@ -77,7 +78,7 @@ def validate(ctx: click.Context, schema: str) -> int:
# 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.UNPARSEABLE)
return ctx.exit(RC.UNPARSABLE)

# Print what was found.
schema_json = JSON(json.dumps(_schema))
@@ -93,7 +94,7 @@ def validate(ctx: click.Context, schema: str) -> int:
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
out = escape(str(err)) # type:ignore[assignment]
error_console.print(Padding(out, (1, 0, 1, 4)))
return ctx.exit(RC.INVALID)

201 changes: 99 additions & 102 deletions jupyter_events/logger.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
"""
Emit structured, discrete events when various actions happen.
"""
from __future__ import annotations

import asyncio
import copy
import inspect
import json
import logging
import typing as t
import warnings
from datetime import datetime
from typing import Any, Callable, Coroutine, Optional, Union
from datetime import datetime, timezone
from importlib.metadata import version

from jsonschema import ValidationError
from pythonjsonlogger import jsonlogger # type:ignore
from packaging.version import parse
from traitlets import Dict, Instance, Set, default
from traitlets.config import Config, LoggingConfigurable

@@ -20,6 +22,13 @@
from .traits import Handlers
from .validators import JUPYTER_EVENTS_CORE_VALIDATOR

# Check if the version is greater than 3.1.0
version_info = version("python-json-logger")
if parse(version_info) >= parse("3.1.0"):
from pythonjsonlogger.json import JsonFormatter
else:
from pythonjsonlogger.jsonlogger import JsonFormatter # type: ignore[attr-defined]

# Increment this version when the metadata included with each event
# changes.
EVENTS_METADATA_VERSION = 1
@@ -63,7 +72,7 @@ class EventLogger(LoggingConfigurable):
"""

handlers = Handlers(
default_value=[],
default_value=None,
allow_none=True,
help="""A list of logging.Handler instances to send events to.
@@ -86,17 +95,17 @@ class EventLogger(LoggingConfigurable):
{}, help="A mapping of schemas to the listeners of unmodified/raw events."
)

_active_listeners = Set()
_active_listeners: set[asyncio.Task[t.Any]] = Set() # type:ignore[assignment]

async def gather_listeners(self):
async def gather_listeners(self) -> list[t.Any]:
"""Gather all of the active listeners."""
return await asyncio.gather(*self._active_listeners, return_exceptions=True)

@default("schemas")
def _default_schemas(self) -> SchemaRegistry:
return SchemaRegistry()

def __init__(self, *args, **kwargs):
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
"""Initialize the logger."""
# We need to initialize the configurable before
# adding the logging handlers.
@@ -114,15 +123,20 @@ def __init__(self, *args, **kwargs):
for handler in self.handlers:
self.register_handler(handler)

def _load_config(self, cfg, section_names=None, traits=None):
def _load_config(
self,
cfg: Config,
section_names: list[str] | None = None, # noqa: ARG002
traits: list[str] | None = None, # type:ignore[override] # noqa: ARG002
) -> 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 = my_cfg.pop("handlers", [])
handlers: list[logging.Handler] = my_cfg.pop("handlers", [])

# Turn handlers list into a pickeable function
def get_handlers():
def get_handlers() -> list[logging.Handler]:
return handlers

my_cfg["handlers"] = get_handlers
@@ -136,29 +150,38 @@ def register_event_schema(self, schema: SchemaType) -> None:
Get this registered schema using the EventLogger.schema.get() method.
"""

event_schema = self.schemas.register(schema)
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()
# It's possible that listeners and modifiers have been added for this
# schema before the schema is registered.
if key not in self._modifiers:
self._modifiers[key] = set()
if key not in self._modified_listeners:
self._modified_listeners[key] = set()
if key not in self._unmodified_listeners:
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 _skip_message(record, **kwargs):
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.
"""
Remove 'message' from log record.
It is always emitted with 'null', and we do not want it,
since we are always emitting events only
"""
del record["message"]
schema = self.schemas.get(record["__schema__"])
if "message" not in schema.properties:
del record["message"]
return json.dumps(record, **kwargs)

formatter = jsonlogger.JsonFormatter(json_serializer=_skip_message)
formatter = JsonFormatter(
json_serializer=_handle_message_field,
)
handler.setFormatter(formatter)
self._logger.addHandler(handler)
if handler not in self.handlers:
@@ -173,8 +196,8 @@ def remove_handler(self, handler: logging.Handler) -> None:
def add_modifier(
self,
*,
schema_id: Union[str, None] = None,
modifier: Callable[[str, dict], dict],
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.
@@ -188,41 +211,27 @@ def add_modifier(
"""
# Ensure that this is a callable function/method
if not callable(modifier):
msg = "`modifier` must be a callable"
msg = "`modifier` must be a callable" # type:ignore[unreachable]
raise TypeError(msg)

# Now let's verify the function signature.
signature = inspect.signature(modifier)

def modifier_signature( # type:ignore[empty-body]
schema_id: str, data: dict
) -> dict:
"""Signature to enforce"""
...

expected_signature = inspect.signature(modifier_signature)
# Assert this signature or raise an exception
if signature == expected_signature:
# 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)
else:
msg = (
"Modifiers are required to follow an exact function/method "
"signature. The signature should look like:"
f"\n\n\tdef my_modifier{expected_signature}:\n\n"
"Check that you are using type annotations for each argument "
"and the return value."
)
raise ModifierError(msg)
# If the schema ID and version is given, only add
# this modifier to that schema
if schema_id:
# If the schema hasn't been added yet,
# start a placeholder set.
modifiers = self._modifiers.get(schema_id, set())
modifiers.add(modifier)
self._modifiers[schema_id] = modifiers
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: Optional[str] = None, modifier: Callable[[str, dict], dict]
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.
@@ -248,8 +257,8 @@ def add_listener(
self,
*,
modified: bool = True,
schema_id: Union[str, None] = None,
listener: Callable[["EventLogger", str, dict], Coroutine[Any, Any, None]],
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.
@@ -264,47 +273,35 @@ def add_listener(
A callable function/method that executes when the named event occurs.
"""
if not callable(listener):
msg = "`listener` must be a callable"
msg = "`listener` must be a callable" # type:ignore[unreachable]
raise TypeError(msg)

signature = inspect.signature(listener)

async def listener_signature(logger: EventLogger, schema_id: str, data: dict) -> None:
"""An interface for a listener."""
...

expected_signature = inspect.signature(listener_signature)
# Assert this signature or raise an exception
if signature == expected_signature:
# If the schema ID and version is given, only add
# this modifier to that schema
if schema_id:
# If the schema ID and version is given, only add
# this modifier to that schema
if schema_id:
if modified:
# If the schema hasn't been added yet,
# start a placeholder set.
listeners = self._modified_listeners.get(schema_id, set())
listeners.add(listener)
self._modified_listeners[schema_id] = listeners
return
listeners = self._unmodified_listeners.get(schema_id, set())
listeners.add(listener)
self._unmodified_listeners[schema_id] = listeners
return
for id_ in self.schemas.schema_ids:
if schema_id is None or id_ == 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[schema_id].add(listener)
else:
msg = (
"Listeners are required to follow an exact function/method "
"signature. The signature should look like:"
f"\n\n\tasync def my_listener{expected_signature}:\n\n"
"Check that you are using type annotations for each argument "
"and the return value."
)

raise ListenerError(msg)
self._modified_listeners[id_].add(listener)
else:
self._unmodified_listeners[id_].add(listener)

def remove_listener(
self,
*,
schema_id: Optional[str] = None,
listener: Callable[["EventLogger", str, dict], Coroutine[Any, Any, None]],
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.
@@ -328,8 +325,8 @@ def remove_listener(
self._unmodified_listeners[schema_id].discard(listener)

def emit(
self, *, schema_id: str, data: dict, timestamp_override: Optional[datetime] = None
) -> Optional[dict]:
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.
@@ -350,8 +347,8 @@ def emit(
# 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]
and not self._modified_listeners.get(schema_id)
and not self._unmodified_listeners.get(schema_id)
):
return None

@@ -363,6 +360,7 @@ def emit(
"this was not intentional, please register the schema using the "
"`register_event_schema` method.",
SchemaNotRegistered,
stacklevel=2,
)
return None

@@ -381,10 +379,9 @@ def emit(
self.schemas.validate_event(schema_id, modified_data)

# Generate the empty event capsule.
if timestamp_override is None:
timestamp = datetime.utcnow()
else:
timestamp = timestamp_override
timestamp = (
datetime.now(tz=timezone.utc) if timestamp_override is None else timestamp_override
)
capsule = {
"__timestamp__": timestamp.isoformat() + "Z",
"__schema__": schema_id,
@@ -402,7 +399,7 @@ def emit(

# callback for removing from finished listeners
# from active listeners set.
def _listener_task_done(task: asyncio.Task) -> None:
def _listener_task_done(task: asyncio.Task[t.Any]) -> None:
# If an exception happens, log it to the main
# applications logger
err = task.exception()
@@ -431,7 +428,7 @@ def _listener_task_done(task: asyncio.Task) -> None:
self._active_listeners.add(task)

# Remove task from active listeners once its finished.
def _listener_task_done(task: asyncio.Task) -> None:
def _listener_task_done(task: asyncio.Task[t.Any]) -> None:
# If an exception happens, log it to the main
# applications logger
err = task.exception()
21 changes: 13 additions & 8 deletions jupyter_events/pytest_plugin.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,38 @@
"""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():
def jp_event_sink() -> io.StringIO:
"""A stream for capture events."""
return io.StringIO()


@pytest.fixture
def jp_event_handler(jp_event_sink):
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, jp_event_sink):
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():
def _read() -> list[str] | None:
jp_event_handler.flush()
lines = jp_event_sink.getvalue().strip().split("\n")
output = [json.loads(item) for item in lines]
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)
@@ -37,7 +42,7 @@ def _read():


@pytest.fixture
def jp_event_schemas():
def jp_event_schemas() -> list[Any]:
"""A list of schema references.
Each item should be one of the following:
@@ -49,7 +54,7 @@ def jp_event_schemas():


@pytest.fixture
def jp_event_logger(jp_event_handler, jp_event_schemas):
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:
58 changes: 31 additions & 27 deletions jupyter_events/schema.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,36 @@
"""Event schema objects."""
from __future__ import annotations

import json
from pathlib import Path, PurePath
from typing import Optional, Type, Union
from typing import Any, Union

from jsonschema import FormatChecker, RefResolver, validators
from jsonschema import FormatChecker, validators
from referencing import Registry
from referencing.jsonschema import DRAFT7

try:
from jsonschema.protocols import Validator
except ImportError:
from typing import Any

Validator = Any # type:ignore
Validator = Any # type:ignore[assignment, misc]

from . import yaml
from .validators import draft7_format_checker, validate_schema


class EventSchemaUnrecognized(Exception): # noqa
class EventSchemaUnrecognized(Exception):
"""An error for an unrecognized event schema."""

pass


class EventSchemaLoadingError(Exception):
"""An error for an event schema loading error."""

pass


class EventSchemaFileAbsent(Exception): # noqa
class EventSchemaFileAbsent(Exception):
"""An error for an absent event schema file."""

pass


SchemaType = Union[dict, str, PurePath]
SchemaType = Union[dict[str, Any], str, PurePath]


class EventSchema:
@@ -55,26 +51,30 @@ class EventSchema:
any schema registered here follows the expected form
of Jupyter Events.
resolver:
RefResolver for nested JSON schema references.
registry:
Registry for nested JSON schema references.
"""

def __init__(
self,
schema: SchemaType,
validator_class: Type[Validator] = validators.Draft7Validator, # type:ignore[assignment]
validator_class: type[Validator] = validators.Draft7Validator, # type:ignore[assignment]
format_checker: FormatChecker = draft7_format_checker,
resolver: Optional[RefResolver] = None,
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, resolver=resolver, format_checker=format_checker)
self._validator = validator_class(_schema, registry=registry, format_checker=format_checker) # type: ignore[call-arg]
self._schema = _schema

def __repr__(self):
def __repr__(self) -> str:
"""A string repr for an event schema."""
return json.dumps(self._schema, indent=2)

@@ -101,7 +101,7 @@ def intended_as_path(schema: str) -> bool:
raise EventSchemaLoadingError(error_msg)

@staticmethod
def _load_schema(schema: SchemaType) -> dict:
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
@@ -122,28 +122,32 @@ def _load_schema(schema: SchemaType) -> dict:

loaded_schema = yaml.load(schema)
EventSchema._ensure_yaml_loaded(loaded_schema)
return 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
return loaded_schema # type:ignore[no-any-return]

msg = f"Expected a dictionary, string, or PurePath, but instead received {schema.__class__.__name__}."
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:
"""Schema $id field."""
return self._schema["$id"]
return self._schema["$id"] # type:ignore[no-any-return]

@property
def version(self) -> int:
"""Schema's version."""
return self._schema["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) -> None:
def validate(self, data: dict[str, Any]) -> None:
"""Validate an incoming instance of this event schema."""
self._validator.validate(data)
32 changes: 17 additions & 15 deletions jupyter_events/schema_registry.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
""""An event schema registry."""
from typing import Optional, Union
from __future__ import annotations

from typing import Any

from .schema import EventSchema


class SchemaRegistryException(Exception): # noqa[N818]
class SchemaRegistryException(Exception):
"""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: Optional[dict] = None):
def __init__(self, schemas: dict[str, EventSchema] | None = None):
"""Initialize the registry."""
self._schemas = schemas or {}
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"""
@@ -33,10 +35,10 @@ def _add(self, schema_obj: EventSchema) -> None:
self._schemas[schema_obj.id] = schema_obj

@property
def schema_ids(self):
return self._schemas.keys()
def schema_ids(self) -> list[str]:
return list(self._schemas.keys())

def register(self, schema: Union[dict, str, EventSchema]) -> EventSchema:
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
@@ -47,35 +49,35 @@ def register(self, schema: Union[dict, str, EventSchema]) -> EventSchema:
self._add(schema)
return schema

def get(self, id: str) -> EventSchema:
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]
return self._schemas[id_]
except KeyError:
msg = (
f"The requested schema, {id}, was not found in the "
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:
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]
del self._schemas[id_]
except KeyError:
msg = (
f"The requested schema, {id}, was not found in the "
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) -> 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 = self.get(id_)
schema.validate(data)
4 changes: 2 additions & 2 deletions jupyter_events/schemas/event-core-schema.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
$schema: http://json-schema.org/draft-07/schema
$id: http://event.jupyter.org/event-schema
version: 1
version: "1"
title: Event Schema
description: |
A schema for validating any Jupyter Event.
@@ -12,7 +12,7 @@ properties:
const: 1
__schema_version__:
title: Schema Version
type: integer
type: string
__schema__:
title: Schema ID
type: string
4 changes: 2 additions & 2 deletions jupyter_events/schemas/event-metaschema.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
$schema: http://json-schema.org/draft-07/schema
$id: http://event.jupyter.org/event-metaschema
version: 1
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
type: string
title:
type: string
description:
2 changes: 1 addition & 1 deletion jupyter_events/schemas/property-metaschema.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
$schema: http://json-schema.org/draft-07/schema
$id: http://event.jupyter.org/property-metaschema
version: 1
version: "1"
title: Property Metaschema
description: |
A metaschema for validating properties within
21 changes: 14 additions & 7 deletions jupyter_events/traits.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
"""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(TraitType):
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, value):
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):
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, value):
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.
@@ -37,8 +44,8 @@ def validate(self, obj, value):
return out
# If a list, check it's elements to verify
# that each element is a logging handler instance.
elif type(value) == list:
if isinstance(value, list):
self.validate_elements(obj, value)
return value
else:
self.error(obj, value)
self.error(obj, value)
return None # type:ignore[unreachable]
8 changes: 8 additions & 0 deletions jupyter_events/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""
Various utilities
"""
from __future__ import annotations


class JupyterEventsVersionWarning(UserWarning):
"""Emitted when an event schema version is an `int` when it should be `str`."""
37 changes: 29 additions & 8 deletions jupyter_events/validators.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
"""Event validators."""
from __future__ import annotations

import pathlib
import warnings
from typing import Any

import jsonschema
from jsonschema import Draft7Validator, RefResolver, ValidationError
from jsonschema import Draft7Validator, ValidationError
from referencing import Registry
from referencing.jsonschema import DRAFT7

from . import yaml
from .utils import JupyterEventsVersionWarning

draft7_format_checker = (
Draft7Validator.FORMAT_CHECKER
if hasattr(Draft7Validator, "FORMAT_CHECKER")
else jsonschema.draft7_format_checker
)

from . import yaml

METASCHEMA_PATH = pathlib.Path(__file__).parent.joinpath("schemas")

@@ -29,26 +37,39 @@
EVENT_CORE_SCHEMA["$id"]: EVENT_CORE_SCHEMA,
}

METASCHEMA_RESOLVER = RefResolver(
base_uri=EVENT_METASCHEMA["$id"], referrer=EVENT_METASCHEMA, store=SCHEMA_STORE
)
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,
resolver=METASCHEMA_RESOLVER,
registry=METASCHEMA_REGISTRY,
format_checker=draft7_format_checker,
)

JUPYTER_EVENTS_CORE_VALIDATOR = Draft7Validator(
schema=EVENT_CORE_SCHEMA,
resolver=METASCHEMA_RESOLVER,
registry=METASCHEMA_REGISTRY,
format_checker=draft7_format_checker,
)


def validate_schema(schema: dict) -> None:
def validate_schema(schema: dict[str, Any]) -> None:
"""Validate a schema dict."""
try:
# If the `version` attribute is an integer, coerce to string.
# TODO: remove this in a future version.
if "version" in schema and isinstance(schema["version"], int):
schema["version"] = str(schema["version"])
msg = (
"The `version` property of an event schema must be a string. "
"It has been type coerced, but in a future version of this "
"library, it will fail to validate. Please update schema: "
f"{schema['$id']}"
)
warnings.warn(JupyterEventsVersionWarning(msg), stacklevel=2)
# Validate the schema against Jupyter Events metaschema.
JUPYTER_EVENTS_SCHEMA_VALIDATOR.validate(schema)
except ValidationError as err:
16 changes: 9 additions & 7 deletions jupyter_events/yaml.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Yaml utilities."""
# mypy: ignore-errors
from pathlib import Path
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
@@ -9,26 +11,26 @@
from yaml import CSafeDumper as SafeDumper
from yaml import CSafeLoader as SafeLoader
except ImportError: # pragma: no cover
from yaml import SafeDumper, SafeLoader
from yaml import SafeDumper, SafeLoader # type:ignore[assignment]


def loads(stream):
def loads(stream: Any) -> Any:
"""Load yaml from a stream."""
return yload(stream, Loader=SafeLoader)


def dumps(stream):
def dumps(stream: Any) -> str:
"""Parse the first YAML document in a stream as an object."""
return ydump(stream, Dumper=SafeDumper)


def load(fpath):
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, outpath):
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")
169 changes: 89 additions & 80 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
name = "jupyter-events"
description = "Jupyter Event System library"
readme = "README.md"
requires-python = ">=3.7"
requires-python = ">=3.9"
authors = [
{ name = "Jupyter Development Team", email = "jupyter@googlegroups.com" },
]
@@ -23,10 +23,12 @@ classifiers = [
"Programming Language :: Python :: 3",
]
dependencies = [
"jsonschema[format-nongpl,format_nongpl]>=3.2.0",
"referencing",
"jsonschema[format-nongpl]>=4.18.0",
"python-json-logger>=2.0.4",
"pyyaml>=5.3",
"traitlets>=5.3",
"packaging",
# 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.
@@ -38,27 +40,29 @@ dynamic = [
]

[project.license]
file = 'COPYING.md'
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 = [
"sphinx>=8",
"jupyterlite-sphinx",
"myst_parser",
"pydata_sphinx_theme",
"pydata_sphinx_theme>=0.16",
"sphinxcontrib-spelling",
]
test = [
"coverage",
"pre-commit",
"pytest-asyncio>=0.19.0",
"pytest-console-scripts",
"pytest-cov",
"pytest>=7.0",
# [cli]
"click",
@@ -85,39 +89,49 @@ nowarn = "test -W default {args}"

[tool.hatch.envs.cov]
features = ["test"]
dependencies = ["coverage", "pytest-cov"]
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 --cov-fail-under 80 {args}"
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.typing]
features = ["test"]
dependencies = ["mypy>=0.990"]
[tool.hatch.envs.typing.scripts]
test = "mypy --install-types --non-interactive {args:.}"

[tool.hatch.envs.lint]
dependencies = ["black[jupyter]==22.12.0", "mdformat>0.7", "ruff==0.0.194"]
detached = true
dependencies = ["pre-commit"]
[tool.hatch.envs.lint.scripts]
style = [
"ruff {args:.}",
"black --check --diff {args:.}",
"mdformat --check {args:docs *.md}"
]
fmt = [
"black {args:.}",
"ruff --fix {args:.}",
"mdformat {args:docs *.md}"
build = [
"pre-commit run --all-files ruff",
"pre-commit run --all-files ruff-format"
]

[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]
addopts = "-raXs --durations 10 --color=yes --doctest-modules"
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",
"ignore::jupyter_events.utils.JupyterEventsVersionWarning",
]

[tool.coverage.report]
exclude_lines = [
@@ -133,74 +147,60 @@ exclude_lines = [
"@(abc\\.)?abstractmethod",
]

[tool.coverage.run]
relative_files = true
source = ["jupyter_events"]

[tool.mypy]
check_untyped_defs = true
disallow_incomplete_defs = true
no_implicit_optional = true
pretty = true
show_error_context = true
show_error_codes = true
strict_equality = true
warn_unused_configs = true
warn_unused_ignores = true
warn_redundant_casts = true
explicit_package_bases = true
namespace_packages = true

[tool.black]
line-length = 100
skip-string-normalization = true
target-version = ["py37"]
files = "jupyter_events"
python_version = "3.9"
strict = true
enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
warn_unreachable = true

[tool.ruff]
target-version = "py37"
line-length = 100
select = [
"A", "B", "C", "E", "EM", "F", "FBT", "I", "N", "Q", "RUF", "S", "T",
"UP", "W", "YTT",

[tool.ruff.lint]
extend-select = [
"B", # flake8-bugbear
"I", # isort
"ARG", # flake8-unused-arguments
"C4", # flake8-comprehensions
"EM", # flake8-errmsg
"ICN", # flake8-import-conventions
"G", # flake8-logging-format
"PGH", # pygrep-hooks
"PIE", # flake8-pie
"PL", # pylint
"PTH", # flake8-use-pathlib
"PT", # flake8-pytest-style
"RET", # flake8-return
"RUF", # Ruff-specific
"SIM", # flake8-simplify
"T20", # flake8-print
"UP", # pyupgrade
"YTT", # flake8-2020
"EXE", # flake8-executable
"PYI", # flake8-pyi
"S", # flake8-bandit
"G001", # .format and co in logging methods
]
ignore = [
# Allow non-abstract empty methods in abstract base classes
"B027",
# Ignore McCabe complexity
"C901",
# Allow boolean positional values in function calls, like `dict.get(... True)`
"FBT003",
# Use of `assert` detected
"S101",
# Line too long
"E501",
# Relative imports are banned
"TID252",
# Boolean ... in function definition
"FBT001", "FBT002",
# Module level import not at top of file
"E402",
# A001/A002/A003 .. is shadowing a python builtin
"A001", "A002", "A003",
# Possible hardcoded password
"S105", "S106",
# Q000 Single quotes found but double quotes preferred
"Q000",
# N806 Variable `B` in function should be lowercase
"N806",
# T201 `print` found
"T201",
# N802 Function name `CreateWellKnownSid` should be lowercase
"N802", "N803",
# C408 Unnecessary `dict` call (rewrite as a literal)
"C408",
# N801 Class name `directional_link` should use CapWords convention
"N801",
"E501", # E501 Line too long (158 > 100 characters)
"SIM105", # SIM105 Use `contextlib.suppress(...)`
"PLR", # Design related pylint codes
"S101", # Use of `assert` detected
]
unfixable = [
# Don't touch print statements
"T201",
# Don't touch noqa lines
"RUF100",
]
isort.required-imports = ["from __future__ import annotations"]

[tool.ruff.per-file-ignores]
[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
@@ -209,7 +209,16 @@ unfixable = [
# 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
"tests/*" = ["B011", "F841", "C408", "E402", "T201", "B007", "N802", "F841"]
# S101 Use of `assert` detected
"tests/*" = ["B011", "F841", "C408", "E402", "T201", "B007", "N802", "F841", "S101", "ARG", "PGH"]
# C901 Function is too complex
"jupyter_events/logger.py" = ["C901"] # `emit` is too complex (12 > 10)
"docs/demo/demo-notebook.ipynb" = ["PLE1142", "E402", "T201"]

[tool.ruff.lint.flake8-pytest-style]
fixture-parentheses = false
mark-parentheses = false
parametrize-names-type = "csv"

[tool.interrogate]
ignore-init-module=true
2 changes: 2 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from __future__ import annotations

pytest_plugins = ["jupyter_events.pytest_plugin"]
2 changes: 1 addition & 1 deletion tests/schemas/bad/bad-id.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
$id: not-a-uri
version: 1
version: "1"
title: Schema with a Bad URI ID
description: |
A schema with a bad id
2 changes: 1 addition & 1 deletion tests/schemas/bad/nested-reserved-property.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
$id: http://event.jupyter.org/test
version: 1
version: "1"
title: Schema with Array
description: |
A schema for an array of objects.
2 changes: 1 addition & 1 deletion tests/schemas/good/array.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
$id: http://event.jupyter.org/test
version: 1
version: "1"
title: Schema with Array
description: |
A schema for an array of objects.
2 changes: 1 addition & 1 deletion tests/schemas/good/basic.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$id": "http://event.jupyter.org/test",
"version": 1,
"version": "1",
"title": "Simple Test Schema",
"description": "A simple schema for testing",
"type": "object",
2 changes: 1 addition & 1 deletion tests/schemas/good/basic.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
$id: http://event.jupyter.org/test
version: 1
version: "1"
title: Simple Test Schema
description: |
A simple schema for testing
2 changes: 1 addition & 1 deletion tests/schemas/good/nested-array.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
$id: http://event.jupyter.org/test
version: 1
version: "1"
title: Schema with Array
description: |
A schema for an array of objects.
2 changes: 1 addition & 1 deletion tests/schemas/good/user.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
$id: http://event.jupyter.org/user
version: 1
version: "1"
title: User
description: |
A User model.
8 changes: 5 additions & 3 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import os

import pytest
@@ -18,7 +20,7 @@ def run_cli(*args, **kwargs):
env.update(kwargs.pop("env", {}))
env["PYTHONIOENCODING"] = "utf-8"
kwargs["env"] = env
return script_runner.run(NAME, *map(str, args), **kwargs)
return script_runner.run([NAME, *list(map(str, args))], **kwargs)

return run_cli

@@ -54,14 +56,14 @@ def test_cli_good_raw(cli):
def test_cli_missing(cli):
ret = cli("validate", SCHEMA_PATH / "bad/doesnt-exist.yaml")
assert not ret.success
assert ret.returncode == RC.UNPARSEABLE
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.UNPARSEABLE
assert ret.returncode == RC.UNPARSABLE
assert "Could not deserialize" in ret.stderr.strip()


75 changes: 57 additions & 18 deletions tests/test_listeners.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from __future__ import annotations

import io
import logging

import pytest

from jupyter_events.logger import EventLogger, ListenerError
from jupyter_events.logger import EventLogger, SchemaNotRegistered
from jupyter_events.schema import EventSchema

from .utils import SCHEMA_PATH
@@ -38,7 +40,7 @@ async def my_listener(logger: EventLogger, schema_id: str, data: dict) -> None:
assert len(event_logger._active_listeners) == 0


async def test_remove_listener_function(jp_event_logger, schema):
async def test_listener_function_str_annotations(jp_event_logger, schema):
event_logger = jp_event_logger
listener_was_called = False

@@ -51,30 +53,29 @@ async def my_listener(logger: EventLogger, schema_id: str, data: dict) -> None:
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_bad_listener_function_signature(jp_event_logger, schema):
async def test_remove_listener_function(jp_event_logger, schema):
event_logger = jp_event_logger
listener_was_called = False

async def listener_with_extra_args(
logger: EventLogger, schema_id: str, data: dict, unknown_arg: dict
) -> None:
pass
async def my_listener(logger: EventLogger, schema_id: str, data: dict) -> None:
nonlocal listener_was_called
listener_was_called = True

with pytest.raises(ListenerError):
event_logger.add_listener(
schema_id=schema.id,
listener=listener_with_extra_args,
)
# 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

# Ensure no modifier was added.
# 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


@@ -137,3 +138,41 @@ async def my_listener(logger: EventLogger, schema_id: str, data: dict) -> None:
assert listener_was_called
# Check that the active listeners are cleaned up.
assert len(event_logger._active_listeners) == 0


@pytest.mark.parametrize(
# Make sure no schemas are added at the start of this test.
"jp_event_schemas",
[
# Empty events list.
[]
],
)
async def test_listener_added_before_schemas_passes(jp_event_logger, schema):
# Ensure there are no schemas listed.
assert len(jp_event_logger.schemas.schema_ids) == 0

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 listener without any schemas
jp_event_logger.add_listener(schema_id=schema.id, listener=my_listener)

# Proof that emitting the event won't success
with pytest.warns(SchemaNotRegistered):
jp_event_logger.emit(schema_id=schema.id, data={"prop": "hello, world"})

assert not listener_was_called

# Now register the event and emit.
jp_event_logger.register_event_schema(schema)

# Try emitting the event again and ensure the listener saw it.
jp_event_logger.emit(schema_id=schema.id, data={"prop": "hello, world"})
await jp_event_logger.gather_listeners()
assert listener_was_called
# Check that the active listeners are cleaned up.
assert len(jp_event_logger._active_listeners) == 0
151 changes: 127 additions & 24 deletions tests/test_logger.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from __future__ import annotations

import io
import json
import logging
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from unittest.mock import MagicMock

import jsonschema
@@ -39,8 +41,7 @@ def get_config_from_file(path, content):

# Load written file.
loader = PyFileConfigLoader(filename, path=str(path))
cfg = loader.load_config()
return cfg
return loader.load_config()


def test_good_config_file(tmp_path):
@@ -99,7 +100,7 @@ def test_timestamp_override():
"""
schema = {
"$id": "http://test/test",
"version": 1,
"version": "1",
"properties": {
"something": {
"type": "string",
@@ -113,7 +114,7 @@ def test_timestamp_override():
el = EventLogger(handlers=[handler])
el.register_event_schema(schema)

timestamp_override = datetime.utcnow() - timedelta(days=1)
timestamp_override = datetime.now(tz=timezone.utc) - timedelta(days=1)

el.emit(
schema_id="http://test/test",
@@ -122,6 +123,7 @@ def test_timestamp_override():
)
handler.flush()
event_capsule = json.loads(output.getvalue())
event_capsule.pop("taskName", None)
assert event_capsule["__timestamp__"] == timestamp_override.isoformat() + "Z"


@@ -131,12 +133,12 @@ def test_emit():
"""
schema = {
"$id": "http://test/test",
"version": 1,
"version": "1",
"properties": {
"something": {
"type": "string",
"title": "test",
},
}
},
}

@@ -154,16 +156,113 @@ def test_emit():
handler.flush()

event_capsule = json.loads(output.getvalue())
event_capsule.pop("taskName", None)

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",
}
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())
event_capsule.pop("taskName", None)

assert "__timestamp__" in event_capsule
# Remove timestamp from capsule when checking equality, since it is gonna vary
del event_capsule["__timestamp__"]
assert event_capsule == {
expected = {
"__schema__": "http://test/test",
"__schema_version__": 1,
"__schema_version__": "1",
"__metadata_version__": 1,
"something": "blah",
"message": "a message was seen",
}
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())
event_capsule.pop("taskName", None)

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"},
}
assert event_capsule == expected


def test_register_event_schema(tmp_path):
@@ -172,7 +271,7 @@ def test_register_event_schema(tmp_path):
"""
schema = {
"$id": "http://test/test",
"version": 1,
"version": "1",
"type": "object",
"properties": {
"something": {
@@ -195,7 +294,7 @@ def test_register_event_schema_object(tmp_path):
"""
schema = {
"$id": "http://test/test",
"version": 1,
"version": "1",
"type": "object",
"properties": {
"something": {
@@ -219,7 +318,7 @@ def test_emit_badschema():
"""
schema = {
"$id": "http://test/test",
"version": 1,
"version": "1",
"type": "object",
"properties": {
"something": {
@@ -248,7 +347,7 @@ def test_emit_badschema_format():
"""
schema = {
"$id": "http://test/test",
"version": 1,
"version": "1",
"type": "object",
"properties": {
"something": {"type": "string", "title": "test", "format": "date-time"},
@@ -267,7 +366,7 @@ def test_emit_badschema_format():
def test_unique_logger_instances():
schema0 = {
"$id": "http://test/test0",
"version": 1,
"version": "1",
"type": "object",
"properties": {
"something": {
@@ -279,7 +378,7 @@ def test_unique_logger_instances():

schema1 = {
"$id": "http://test/test1",
"version": 1,
"version": "1",
"type": "object",
"properties": {
"something": {
@@ -316,34 +415,38 @@ def test_unique_logger_instances():
handler1.flush()

event_capsule0 = json.loads(output0.getvalue())
event_capsule0.pop("taskName", None)

assert "__timestamp__" in event_capsule0
# Remove timestamp from capsule when checking equality, since it is gonna vary
del event_capsule0["__timestamp__"]
assert event_capsule0 == {
expected = {
"__schema__": "http://test/test0",
"__schema_version__": 1,
"__schema_version__": "1",
"__metadata_version__": 1,
"something": "blah",
}
assert event_capsule0 == expected

event_capsule1 = json.loads(output1.getvalue())
event_capsule1.pop("taskName", None)

assert "__timestamp__" in event_capsule1
# Remove timestamp from capsule when checking equality, since it is gonna vary
del event_capsule1["__timestamp__"]
assert event_capsule1 == {
expected = {
"__schema__": "http://test/test1",
"__schema_version__": 1,
"__schema_version__": "1",
"__metadata_version__": 1,
"something": "blah",
}
assert event_capsule1 == expected


def test_register_duplicate_schemas():
schema0 = {
"$id": "http://test/test",
"version": 1,
"version": "1",
"type": "object",
"properties": {
"something": {
@@ -355,7 +458,7 @@ def test_register_duplicate_schemas():

schema1 = {
"$id": "http://test/test",
"version": 1,
"version": "1",
"type": "object",
"properties": {
"something": {
@@ -382,12 +485,12 @@ async def test_noop_emit():
# 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")
el.schemas.validate_event = MagicMock(name="validate_event") # type:ignore[method-assign]

schema_id1 = "http://test/test"
schema1 = {
"$id": schema_id1,
"version": 1,
"version": "1",
"type": "object",
"properties": {
"something": {
@@ -399,7 +502,7 @@ async def test_noop_emit():
schema_id2 = "http://test/test2"
schema2 = {
"$id": schema_id2,
"version": 1,
"version": "1",
"type": "object",
"properties": {
"something_elss": {
42 changes: 2 additions & 40 deletions tests/test_modifiers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import pytest

from jupyter_events.logger import EventLogger, ModifierError
from jupyter_events.schema import EventSchema

from .utils import SCHEMA_PATH
@@ -54,45 +55,6 @@ def redact(self, schema_id: str, data: dict) -> dict:
assert output["username"] == "<masked>"


def test_bad_modifier_functions(jp_event_logger: EventLogger, schema: EventSchema) -> None:
event_logger = jp_event_logger

def modifier_with_extra_args(schema_id: str, data: dict, unknown_arg: dict) -> dict:
return data

with pytest.raises(ModifierError):
event_logger.add_modifier(modifier=modifier_with_extra_args) # type:ignore[arg-type]

# Ensure no modifier was added.
assert len(event_logger._modifiers[schema.id]) == 0


def test_bad_modifier_method(jp_event_logger: EventLogger, schema: EventSchema) -> None:
event_logger = jp_event_logger

class Redactor:
def redact(self, schema_id: str, data: dict, extra_args: dict) -> dict:
return data

redactor = Redactor()

with pytest.raises(ModifierError):
event_logger.add_modifier(modifier=redactor.redact) # type:ignore[arg-type]

# Ensure no modifier was added
assert len(event_logger._modifiers[schema.id]) == 0


def test_modifier_without_annotations():
logger = EventLogger()

def modifier_with_extra_args(event):
return event

with pytest.raises(ModifierError):
logger.add_modifier(modifier=modifier_with_extra_args) # type:ignore[arg-type]


def test_remove_modifier(schema, jp_event_logger, jp_read_emitted_events):
event_logger = jp_event_logger

8 changes: 5 additions & 3 deletions tests/test_schema.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import os
from pathlib import Path

@@ -31,7 +33,7 @@ def test_bad_validations(schema_file, validation_error_msg):
a redactionPolicies field.
"""
# Read the schema file
with open(SCHEMA_PATH / "bad" / schema_file) as f:
with Path.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:
@@ -49,7 +51,7 @@ 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")
str_path = os.path.join(SCHEMA_PATH, "good", "some_schema.yaml") # noqa: PTH118
with pytest.raises(EventSchemaLoadingError) as e:
EventSchema(str_path)

@@ -79,7 +81,7 @@ def test_valid_json():
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:
with Path.open(SCHEMA_PATH / "good" / schema_file) as f:
schema = yaml.loads(f)
# assert that no exception gets raised
validate_schema(schema)
2 changes: 2 additions & 0 deletions tests/test_traits.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import logging

import pytest
2 changes: 2 additions & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import io
import json
import logging