From 0f59247a73ce047b2cc941aabb3e122002d0e886 Mon Sep 17 00:00:00 2001 From: Michael Booth Date: Sun, 31 May 2026 02:03:55 +1000 Subject: [PATCH 1/3] Sync README with main for vulnerability reporting docs Co-Authored-By: Oz --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 6110a65..ee8623f 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,15 @@ uv run pytest-notebook-quality --skip-ruff \ --report-dependency-enrichment \ path/to/notebooks ``` +Include optional vulnerability IDs (queried from OSV) in dependency enrichment output: + +```shell +uv run pytest-notebook-quality --skip-ruff \ + --report-nbom-json reports/notebook-policy-nbom.json \ + --report-dependency-vulns \ + path/to/notebooks +``` +`--report-dependency-vulns` implicitly enables dependency enrichment. Generate a markdown report with findings-first layout, touchpoint summary, and appendices: @@ -202,6 +211,7 @@ jupyter_max_cell_lines = 120 jupyter_max_inline_definitions = 5 report_md = "reports/notebook-policy-report.md" report_dependency_enrichment = true +report_dependency_vulns = false report_nbom_json = "reports/notebook-policy-nbom.json" ``` From bb6bc474adc8743fb8c3b742e0b8121679dfb226 Mon Sep 17 00:00:00 2001 From: Michael Booth Date: Sat, 20 Jun 2026 15:41:33 +1000 Subject: [PATCH 2/3] Release 1.0.0 baseline and add Great Docs workflow - bump package to 1.0.0 and move to Python 3.12+\n- add Great Docs config, docs workflows, and contributor guidance\n- add docs just recipes and rendering/link-check refinements\n\nCo-Authored-By: Oz --- .github/workflows/ci.yml | 29 + .github/workflows/docs.yml | 109 ++ .gitignore | 6 + CONTRIBUTING.md | 43 + README.md | 23 + RELEASE_NOTES.md | 24 + docs/JUST_SETUP.md | 37 + docs/MANUAL_NOTEBOOK_VALIDATION.md | 188 ++ docs/QUICKSTART.md | 62 +- docs/README.md | 5 +- docs/REPORT_INTERPRETATION.md | 87 + great-docs.yml | 126 ++ justfile | 59 + pyproject.toml | 11 +- tmp/manual_checks/experiments/pyproject.toml | 15 + uv.lock | 1732 +++++++++++++++++- 16 files changed, 2475 insertions(+), 81 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/docs.yml create mode 100644 CONTRIBUTING.md create mode 100644 docs/JUST_SETUP.md create mode 100644 docs/MANUAL_NOTEBOOK_VALIDATION.md create mode 100644 docs/REPORT_INTERPRETATION.md create mode 100644 great-docs.yml create mode 100644 justfile create mode 100644 tmp/manual_checks/experiments/pyproject.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bd47b5b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: ci +on: + pull_request: + push: + branches: + - main +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.12", "3.13"] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Set up uv + uses: astral-sh/setup-uv@v5 + - name: Sync dependencies + run: uv sync --frozen --dev + - name: Ruff + run: uv run ruff check src tests + - name: Report regression contracts + run: uv run python -m pytest tests/test_quality.py tests/test_report_regressions.py + - name: Pytest + run: uv run python -m pytest tests diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..14f2f36 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,109 @@ +name: CI Docs + +on: + push: + branches: + - main + pull_request: + +jobs: + build-docs: + name: "Build Docs" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 # Full history for accurate page timestamps + + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Install package and dependencies + run: | + python -m pip install uv + uv sync --frozen --dev + + - name: Set up Quarto + uses: quarto-dev/quarto-actions/setup@v2 + + - name: Build docs + run: uv run great-docs build + + - name: Save docs artifact + uses: actions/upload-artifact@v7 + with: + name: docs-html + path: great-docs/_site + include-hidden-files: true + + - name: Upload build timings + uses: actions/upload-artifact@v7 + with: + name: build-timings + path: great-docs/_site/build-timings.json + + publish-docs: + name: "Publish Docs" + runs-on: ubuntu-latest + needs: "build-docs" + if: github.ref == 'refs/heads/main' + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/download-artifact@v7 + with: + name: docs-html + path: great-docs/_site + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v5 + with: + path: great-docs/_site + include-hidden-files: true + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 + + preview-docs: + name: "Preview Docs" + runs-on: ubuntu-latest + needs: "build-docs" + if: github.event_name == 'pull_request' + permissions: + deployments: write + pull-requests: write + steps: + - uses: actions/download-artifact@v7 + with: + name: docs-html + path: great-docs/_site + + # Start deployment + - name: Configure pull release name + if: ${{ github.event_name == 'pull_request' }} + run: | + echo "RELEASE_NAME=pr-${{ github.event.number }}" >> $GITHUB_ENV + + - name: Configure branch release name + if: ${{ github.event_name != 'pull_request' }} + run: | + # use branch name, but replace slashes. E.g. feat/a -> feat-a + echo "RELEASE_NAME=${GITHUB_REF_NAME//\//-}" >> $GITHUB_ENV + + # Deploy + - name: Create Github Deployment + uses: bobheadxi/deployments@v1 + id: deployment + if: ${{ !github.event.pull_request.head.repo.fork }} + with: + step: start + token: ${{ secrets.GITHUB_TOKEN }} + env: ${{ env.RELEASE_NAME }} + ref: ${{ github.head_ref }} + logs: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" diff --git a/.gitignore b/.gitignore index 505a3b1..2742adb 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,9 @@ wheels/ # Virtual environments .venv + +# Local docs drafts and private notes +docs/_drafts/ + +# Great Docs build output (ephemeral) +great-docs/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d73e5ab --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,43 @@ +# Contributing +Thanks for contributing to `pytest-notebook-policy`. + +## Prerequisites +- Python `3.12+` +- `uv` for environment and dependency management +- `just` (optional but recommended for common workflows) + +## Quick local workflow +1. Sync dependencies: + - `uv sync --dev` +2. Run quality gates: + - `just qa` +3. Build docs: + - `just docs-build` + +## Documentation workflow (Great Docs) +Use this as the default docs gate before opening a PR: + +- `just docs-workflow` + +This runs: +- docs build (`great-docs build`) +- prepublish link checks with sensible temporary ignores for not-yet-live URLs + +For strict enforcement (for example after Pages is live and branch/source links are stable), run: + +- `just docs-workflow-strict` + +Useful docs commands: +- `just docs-preview` (local preview with reload) +- `just docs-scan` (API discovery view) +- `just docs-setup-pages` (scaffold/refresh GitHub Pages workflow) + +## Release-related checks +Before packaging/release work: +- `just qa` +- `just build-check` + +## Pull requests +- Keep changes scoped and explain intent in the PR description. +- Include validation commands/results for code and docs where relevant. +- Update docs and release notes when behaviour or user-facing workflows change. diff --git a/README.md b/README.md index ee8623f..98785f9 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ This keeps the feedback loop short: Detailed rationale and remediation guidance: [`docs/RULES.md`](docs/RULES.md). ## Usage +Runtime baseline: Python `3.12+`. Install in a project: ```shell @@ -198,6 +199,7 @@ uv run pytest-notebook-quality --skip-ruff \ --report-dependency-enrichment \ path/to/notebooks ``` +Interpret report outputs and tune policy profiles: [`docs/REPORT_INTERPRETATION.md`](docs/REPORT_INTERPRETATION.md). Project-specific quality defaults can be set in `pyproject.toml`: @@ -221,6 +223,27 @@ Enable optional sync tooling: uv add --dev 'pytest-notebook-policy[sync]' ``` +## Documentation site (Great Docs) +This repository uses Great Docs for documentation generation and deployment. + +Build docs locally: + +```shell +uv run great-docs build +``` + +Preview docs locally: + +```shell +uv run great-docs preview +``` + +Scaffold or refresh the GitHub Pages workflow: + +```shell +uv run great-docs setup-github-pages +``` + ## Versioning and release workflow - Versioning follows Semantic Versioning (`MAJOR.MINOR.PATCH`). - Release history lives in `RELEASE_NOTES.md`. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 5005706..048a9be 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -5,6 +5,30 @@ This project follows Semantic Versioning (`MAJOR.MINOR.PATCH`). ### Changed - No unreleased entries yet. +## [1.0.0] - Stable baseline + Great Docs foundation +### Release intent +- Mark the first stable major release and establish a modern runtime/docs baseline for ongoing development. + +### Highlights +- Promoted package versioning to `1.0.0`. +- Raised runtime support baseline to Python `3.12+`. +- Updated Ruff target configuration to `py312`. +- Added Great Docs integration for documentation generation and preview workflows. +- Added GitHub Pages docs workflow scaffold via Great Docs (`.github/workflows/docs.yml`). +- Expanded CI test matrix to validate against Python `3.12` and `3.13`. + +## [0.9.0] - Report UX and NBOM alignment +### Release intent +- Improve markdown report readability and operator guidance while keeping machine-readable NBOM output aligned. + +### Highlights +- Refactored markdown report rendering to use a Jinja2 template. +- Added standard executive summary guidance under the report header. +- Added short descriptive text under major report sections to clarify purpose and usage. +- Added Appendix C NBOM alignment summary fields for easier cross-checking against NBOM JSON output. +- Shifted optional dependency metadata to Appendix D when enrichment is enabled. +- Added regression assertions for updated report structure and section ordering. + ## [0.8.0] - Planned initial PyPI release ### Release intent - First public release of `pytest-notebook-policy` to PyPI. diff --git a/docs/JUST_SETUP.md b/docs/JUST_SETUP.md new file mode 100644 index 0000000..10da56f --- /dev/null +++ b/docs/JUST_SETUP.md @@ -0,0 +1,37 @@ +# Just setup +This repository includes a `justfile` with common local workflows for validation and release preparation. + +## What is Just? +Just is a command runner similar to `make`, with a simpler syntax. + +Official site: https://just.systems + +Installation instructions: https://just.systems/man/en/ + +## Quick start +From the repository root, list available recipes: + +```shell +just --list +``` + +## Key recipes in this repository +- `just create-manual-examples`: scaffold manual marimo/Jupyter example notebooks in `manual_checks/`. +- `just refresh-fixtures`: print observed rule-code sets for pinned real fixtures. +- `just qa`: run Ruff and tests. +- `just docs-init`: initialise Great Docs configuration (`great-docs.yml`). +- `just docs-build`: build the docs site into `great-docs/_site`. +- `just docs-preview`: preview docs locally with live reload. +- `just docs-scan`: scan discovered exports for API docs coverage. +- `just docs-setup-pages`: scaffold GitHub Pages workflow via Great Docs. +- `just docs-check`: prepublish link check (ignores known not-yet-live URLs). +- `just docs-check-strict`: strict link check with no ignores. +- `just docs-workflow`: run the default docs build + prepublish checks. +- `just docs-workflow-strict`: run build + strict checks. +- `just build-check`: clean artefacts, build, and run `twine check`. +- `just test-publish-dry-run`: build and validate publish artefacts without uploading. + +## Recommended sequence before manual notebook validation +1. `just create-manual-examples` +2. `uv run pytest-notebook-quality --skip-ruff manual_checks` +3. Follow `MANUAL_NOTEBOOK_VALIDATION.md` for remediation and sign-off. diff --git a/docs/MANUAL_NOTEBOOK_VALIDATION.md b/docs/MANUAL_NOTEBOOK_VALIDATION.md new file mode 100644 index 0000000..2dd304e --- /dev/null +++ b/docs/MANUAL_NOTEBOOK_VALIDATION.md @@ -0,0 +1,188 @@ +# Manual notebook validation and authoring guide +Use this guide when you want to manually confirm policy behaviour on real notebooks, including: + +- creating new notebooks that should pass policy checks +- importing complex notebooks and remediating them to compliance +- validating both marimo (`.py`) and Jupyter (`.ipynb`) workflows + +This guide also serves as a practical notebook **best-practice assurance** and **testing** checklist for teams who want explicit manual validation before release. + +## Scope +This guide is intentionally practical. It assumes you want to test behaviour locally before publishing. + +## 1) One-time setup for manual checks +Create a scratch area: + +```shell +mkdir -p manual_checks/marimo manual_checks/jupyter +``` + +Or use the built-in recipe to scaffold known examples: + +```shell +just create-manual-examples +``` + +For Just installation and recipe usage, see [`JUST_SETUP.md`](JUST_SETUP.md). + +Run policy checks directly: + +```shell +uv run pytest-notebook-quality --skip-ruff manual_checks +``` + +Optional pre-commit hook (local/manual usage): + +```yaml +repos: + - repo: local + hooks: + - id: notebook-policy-manual + name: notebook-policy-manual + entry: uv run pytest-notebook-quality --skip-ruff manual_checks + language: system + pass_filenames: false +``` + +Then run: + +```shell +uv run --with pre-commit pre-commit run notebook-policy-manual --all-files +``` + +## 2) New notebook guide (`.ipynb` and `.py`) +Use these defaults to avoid common violations from the start. +### 2.0 Initialisation: clean isolated environment for notebook experiments +Create an isolated workspace and environment before authoring new notebooks: + +```shell +mkdir -p manual_checks/experiments +cd manual_checks/experiments +uv init --python 3.13 +uv add --dev pytest-notebook-policy +uv add --dev 'pytest-notebook-policy[sync]' +``` + +Notes: + +- `uv init` creates a clean local project scaffold for experiments. +- `uv add --dev pytest-notebook-policy` installs the policy checker for manual validation. +- `'pytest-notebook-policy[sync]'` enables paired `.ipynb` ↔ `.py` sync support (useful when testing `J010`/paired-source workflows). + +Quick verification: + +```shell +uv run pytest --version +uv run pytest-notebook-quality --help +``` + +### 2.1 New Jupyter notebook (`.ipynb`) that is policy-friendly +Recommended structure: + +1. First code cell: parameter/configuration cell (`J011`) +2. Keep code cells compact and focused (`J012`) +3. Avoid magics/shell escapes (`J001`) +4. Keep non-idempotent operations explicit and controlled (`J002`) +5. Move reusable classes/functions into modules (`J013`) + +Suggested first code cell (parameters): + +```python +params = { + "seed": 42, + "dataset_path": "data/train.csv", + "run_mode": "dev", +} +``` + +Suggested workflow: + +- Keep each cell focused on one transformation step. +- Move reusable logic to `src/...` modules and import it. +- If you use randomness/time/UUIDs, gate it behind explicit parameters. + +Validate: + +```shell +uv run pytest-notebook-quality --skip-ruff path/to/notebook.ipynb +``` + +### 2.2 New marimo notebook (`.py`) that is policy-friendly +Recommended structure: + +1. Avoid `on_change` handlers where pure reactivity is enough (`M001`) +2. Avoid mutable module-level state (`M003`) +3. Avoid cross-cell mutation patterns (`M005`) +4. Keep non-idempotent calls controlled (`M006`) +5. Keep fixtures out of notebook modules (`M004`) + +Suggested authoring defaults: + +- treat cells as pure transformations where possible +- return new values instead of mutating shared inputs +- keep state local to cells/functions + +Validate: + +```shell +uv run pytest-notebook-quality --skip-ruff path/to/notebook.py +``` + +## 3) Second-stage checklist for complex imported notebooks +Use this when a real-world notebook fails several J-rules at once. + +### Stage A: Baseline +1. Copy the candidate notebook into `manual_checks/...`. +2. Run: + +```shell +uv run pytest-notebook-quality --skip-ruff manual_checks/jupyter/.ipynb +``` + +3. Record initial codes and count (for example: `J001`, `J011`, `J012`, `J013`). + +### Stage B: Rule-by-rule remediation order (recommended) +Apply in this order to reduce rework. + +1. `J011` first: add a top-of-notebook parameter/configuration cell. +2. `J001`: remove magics and shell escapes; replace with Python APIs. +3. `J013`: extract inline classes/functions into importable modules. +4. `J012`: split large notebooks and long cells after extraction work. +5. `J002`: make non-idempotent behaviour explicit/controlled. +6. `J010` (if using paired files): verify `.ipynb` and paired `.py` stay in sync. + +### Stage C: Verification gates +After each remediation step: + +```shell +uv run pytest-notebook-quality --skip-ruff manual_checks/jupyter/.ipynb +``` + +When stable, run on the whole manual check area: + +```shell +uv run pytest-notebook-quality --skip-ruff manual_checks +``` + +If using pre-commit: + +```shell +uv run --with pre-commit pre-commit run notebook-policy-manual --all-files +``` + +### Stage D: Compliance sign-off +Consider the notebook compliant when: + +- no rule violations are reported for the target files +- extracted reusable logic lives in modules (not large inline cells) +- parameter/configuration intent is clear in the first code cell +- the notebook remains readable and reviewable + +## 4) Optional paired-source check for Jupyter notebooks +If you maintain paired `.ipynb`/`.py` notebooks, run with paired source mode: + +```shell +uv run pytest --notebook-check --notebook-check-jupyter-source paired-py path/to/notebook.ipynb +``` + +Use this when you want J-rules to evaluate the paired `.py` representation while still enforcing sync rules. diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 8df0339..d4106f2 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -1,5 +1,6 @@ # Quickstart This quickstart shows the fastest path to run `pytest-notebook-policy` in a project. +Runtime baseline: Python `3.12+`. ## 1) Install Add as a development dependency: @@ -38,8 +39,61 @@ Current rule codes: - `J013`: avoid excessive inline definitions; move reusable logic to modules. Detailed rationale and remediation guidance: [`RULES.md`](RULES.md). +Report interpretation and policy-tuning workflow: [`REPORT_INTERPRETATION.md`](REPORT_INTERPRETATION.md). -## 4) Optional: enable by default +## 4) Optional: start with a proportionate rule profile +Choose a baseline that matches your notebook context, then tighten over time. + +PoC / exploratory (fast iteration with core guardrails): + +```shell +uv run pytest-notebook-quality --skip-ruff \ + --notebook-check-select M001 \ + --notebook-check-select M003 \ + --notebook-check-select M006 \ + --notebook-check-select J011 \ + notebooks/ +``` + +MVP / delivery hardening (maintainability + reproducibility): + +```shell +uv run pytest-notebook-quality --skip-ruff \ + --notebook-check-select M \ + --notebook-check-select J001 \ + --notebook-check-select J002 \ + --notebook-check-select J011 \ + --notebook-check-select J012 \ + --notebook-check-select J013 \ + --notebook-check-ignore J010 \ + notebooks/ +``` + +Production / operational (full policy set): + +```shell +uv run pytest-notebook-quality --skip-ruff \ + --notebook-check-select M \ + --notebook-check-select J \ + notebooks/ +``` + +Equivalent project defaults: + +```toml +[tool.pytest_notebook_policy.quality] +# PoC profile example: +# select = ["M001", "M003", "M006", "J011"] + +# MVP profile example: +# select = ["M", "J001", "J002", "J011", "J012", "J013"] +# ignore = ["J010"] + +# Production profile example: +select = ["M", "J"] +``` + +## 5) Optional: enable by default Add to `pyproject.toml`: ```toml @@ -79,7 +133,7 @@ notebook_check_jupyter_max_cell_lines = "120" notebook_check_jupyter_max_inline_definitions = "5" ``` -## 5) Optional: run combined quality checks +## 6) Optional: run combined quality checks Run Ruff and notebook policy checks together: ```shell @@ -151,7 +205,7 @@ Enable optional sync tooling: uv add --dev 'pytest-notebook-policy[sync]' ``` -## 6) Optional: pre-commit hook +## 7) Optional: pre-commit hook Example local hook: ```yaml @@ -165,7 +219,7 @@ repos: pass_filenames: false ``` -## 7) Manual validation and remediation workflow +## 8) Manual validation and remediation workflow For a full manual testing walkthrough (including complex notebook remediation and new notebook authoring guidance for both `.ipynb` and `.py`), see: - [`MANUAL_NOTEBOOK_VALIDATION.md`](MANUAL_NOTEBOOK_VALIDATION.md) diff --git a/docs/README.md b/docs/README.md index 017f403..d61adb4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,12 +3,13 @@ This directory contains focused documentation for `pytest-notebook-policy`. ## Available docs - [`QUICKSTART.md`](QUICKSTART.md): install, first run, and CI/pre-commit examples. +- [`REPORT_INTERPRETATION.md`](REPORT_INTERPRETATION.md): interpreting markdown/NBOM outputs and tuning policy profiles proportionately. - [`JUST_SETUP.md`](JUST_SETUP.md): using the repository's `just` recipes and where to install Just. - [`MANUAL_NOTEBOOK_VALIDATION.md`](MANUAL_NOTEBOOK_VALIDATION.md): manual behaviour validation, second-stage remediation checklist, and authoring guide for new `.ipynb` and marimo `.py` notebooks. - [`RULES.md`](RULES.md): detailed rule catalogue with concise rationale and remediation guidance. +- [`../great-docs.yml`](../great-docs.yml): Great Docs site configuration used for generated package documentation. +- [`../CONTRIBUTING.md`](../CONTRIBUTING.md): contributor workflow, including docs and release quality gates. ## Terminology Across this documentation set, related terms such as **best practices**, **assurance**, **validation**, and **testing** all refer to the same core goal: checking notebook quality and policy compliance in a repeatable way. -## Planned docs -- `CONTRIBUTING.md`: contribution workflow for this repository. diff --git a/docs/REPORT_INTERPRETATION.md b/docs/REPORT_INTERPRETATION.md new file mode 100644 index 0000000..66100a7 --- /dev/null +++ b/docs/REPORT_INTERPRETATION.md @@ -0,0 +1,87 @@ +# Report interpretation and policy tuning +Use this guide to interpret `pytest-notebook-policy` outputs and tune rules without losing delivery speed. + +## 1) Interpreting the markdown report +When you run: + +```shell +uv run pytest-notebook-quality --skip-ruff --report-md reports/notebook-policy-report.md notebooks/ +``` + +the markdown report gives you: + +- status summary (`Pass` or `Findings`) +- findings table with rule code, line, rationale, and suggested fix +- notebook surface summary (imports, files, HTTP endpoints, data sources) +- configuration appendix (selected/ignored rules and thresholds) +- scanned-files appendix +- optional dependency appendix (when dependency enrichment is enabled) + +Interpretation order that works well in practice. Use this sequence: + +1. Read status and total violation count. +2. Group findings by rule code to find repeating patterns. +3. Prioritise high-impact reproducibility issues first (`M006`, `J002`, `J011`). +4. Address maintainability/shape issues next (`J012`, `J013`, `M005`). +5. Re-run and compare deltas, not just the latest absolute count. + +## 2) Interpreting the NBOM report +When you run: + +```shell +uv run pytest-notebook-quality --skip-ruff --report-nbom-json reports/notebook-policy-nbom.json notebooks/ +``` + +the NBOM output is best used for: + +- machine-readable policy evidence in CI +- notebook-to-dependency traceability +- change tracking over time (violations, imports, external touchpoints) + +Use it to answer: + +- what changed in notebook surface area between runs? +- which dependencies are newly introduced? +- which notebooks produce repeated policy violations? + +## 3) Practical tuning workflow (proportionate and optional) +Use this loop when rules need to adapt as notebooks move from PoC to MVP to production. + +1. Start with a proportionate baseline profile. +2. Run checks and capture reports. +3. Fix repeated high-value findings first. +4. Tune only when false positives or deliberate exceptions are clear. +5. Record tuned defaults in `pyproject.toml`. +6. Reassess profile strictness at each lifecycle transition (PoC → MVP → production). + +## 4) What to tune first +Prefer tuning in this order: + +1. `ignore` list for explicit, justified exceptions. +2. Jupyter thresholds (`jupyter_max_code_cells`, `jupyter_max_cell_lines`, `jupyter_max_inline_definitions`) for team context. +3. `select` scope once the team is ready for broader coverage. + +Example defaults: + +```toml +[tool.pytest_notebook_policy.quality] +select = ["M", "J"] +ignore = ["J010"] +jupyter_source = "paired-py" +jupyter_max_code_cells = 30 +jupyter_max_cell_lines = 120 +jupyter_max_inline_definitions = 5 +report_md = "reports/notebook-policy-report.md" +report_nbom_json = "reports/notebook-policy-nbom.json" +``` + +## 5) Tuning anti-patterns to avoid +- Disabling broad rule families too early (`ignore = ["M", "J"]`). +- Using `ignore` instead of fixing repeated reproducibility issues. +- Tightening thresholds before the team has stabilised notebook structure. +- Treating PoC and production notebooks as the same risk class. + +## 6) Recommended review cadence +- PoC: tune weekly while notebook shape is changing quickly. +- MVP: tune per release cycle. +- Production: tune only via explicit change review, with rationale captured in PR notes. diff --git a/great-docs.yml b/great-docs.yml new file mode 100644 index 0000000..8743081 --- /dev/null +++ b/great-docs.yml @@ -0,0 +1,126 @@ +# Great Docs Configuration +# See https://posit-dev.github.io/great-docs/user-guide/configuration.html + +# Module Name (optional) +# ---------------------- +# Set this if your importable module name differs from the project name. +# Example: project 'py-yaml12' with module name 'yaml12' +# module: yaml12 + +# Docstring Parser +# ---------------- +# The docstring format used in your package (numpy, google, or sphinx) +parser: numpy + +# Dynamic Introspection +# --------------------- +# Use runtime introspection for more accurate documentation (default: true) +# Set to false if your package has cyclic alias issues (e.g., PyO3/Rust bindings) +dynamic: true + +# API Discovery Settings +# ---------------------- +# Exclude items from auto-documentation +# exclude: +# - InternalClass +# - helper_function + +# Logo & Favicon +# --------------- +# Point to a single logo file (replaces the text title in the navbar): +# logo: assets/logo.svg +# +# For light/dark variants: +# logo: +# light: assets/logo-light.svg +# dark: assets/logo-dark.svg +# +# To show the text title alongside the logo, add: show_title: true + +# Author Information +# ------------------ +# Author metadata for display in the landing page sidebar +# You can add additional fields: github, orcid, affiliation, homepage +authors: + - name: Michael Booth + role: Author + affiliation: DataBooth + email: "michael@databooth.com.au" + github: mjboothaus + # orcid: + homepage: "https://www.databooth.com.au" + +# Funding / Copyright Holder +# -------------------------- +# Credit the organization that funds or holds copyright for this package. +# Displays in sidebar and footer. Homepage and ROR provide links. +# funding: +# name: "Posit Software, PBC" +# roles: +# - Copyright holder +# - funder +# homepage: https://posit.co +# ror: https://ror.org/03wc8by49 + +# API Reference Structure +# ----------------------- +# Customize the sections below to organize your API documentation. +# - Reorder items within a section to change their display order +# - Move items between sections or create new sections +# - Use 'members: false' to exclude methods from documentation +# - Add 'desc:' to sections for descriptions + +reference: + - title: Dataclasses + desc: Dataclass definitions + contents: + - Violation # 1 method(s) + + - title: Functions + desc: Utility functions + contents: + - scan_file + +# Site URL +# -------- +# Canonical address of the deployed documentation site. +# Required for subdirectory deployments, skills page install commands, +# .well-known/ discovery, and sitemaps. +site_url: "https://databooth.github.io/pytest-notebook-policy/" + +# Source links +# ------------ +# Pin links to the default branch and src-layout paths. +source: + enabled: true + branch: main + path: src + +# Custom sections +# --------------- +sections: + - title: Project docs + dir: docs + +# Site Settings +# ------------- +# site: +# theme: flatly # Quarto theme (default: flatly) +# toc: true # Show table of contents (default: true) +# toc-depth: 2 # TOC heading depth (default: 2) +# toc-title: On this page # TOC title (default: "On this page") + +# Jupyter Kernel +# -------------- +# Jupyter kernel to use for executing code cells in .qmd files. +# This is set at the project level so it applies to all pages, including +# auto-generated API reference pages. Can be overridden in individual .qmd +# file frontmatter if needed for special cases. +jupyter: python3 + +# CLI Documentation +# ----------------- +# cli: +# enabled: true # Enable CLI documentation +# module: my_package.cli # Module containing Click commands +# name: cli # Name of the Click command object diff --git a/justfile b/justfile new file mode 100644 index 0000000..664c0ca --- /dev/null +++ b/justfile @@ -0,0 +1,59 @@ +default: + @just --list + +bump-1_0_0: + uv version 1.0.0 + +clean-release-artifacts: + uv run python scripts/clean_release_artifacts.py +create-manual-examples: + uv run python scripts/create_manual_examples.py + +refresh-fixtures: + uv run python scripts/refresh_real_fixtures.py --report-only +notebook-policy-report paths report='reports/notebook-policy-report.md': + uv run pytest-notebook-quality --skip-ruff \ + --notebook-check-select M \ + --notebook-check-select J \ + --notebook-check-ignore J010 \ + --report-md {{report}} \ + {{paths}} + +qa: + uv run ruff check src tests scripts + uv run python -m pytest tests + +docs-build: + uv run great-docs build + +docs-preview: + uv run great-docs preview +docs-init: + uv run great-docs init + +docs-setup-pages: + uv run great-docs setup-github-pages + +docs-scan: + uv run great-docs scan + +docs-check: docs-build + uv run great-docs check-links \ + --ignore "https://databooth.github.io/pytest-notebook-policy/?$" \ + --ignore "https://github.com/DataBooth/pytest-notebook-policy/actions/workflows/ci\\.yml/badge\\.svg" \ + --ignore "https://github.com/DataBooth/pytest-notebook-policy/blob/main/pytest_notebook_policy/.*" + +docs-check-strict: docs-build + uv run great-docs check-links + +docs-workflow: docs-build docs-check + +docs-workflow-strict: docs-build docs-check-strict + +build-check: clean-release-artifacts + uv build + uv run --with twine twine check dist/* +test-publish-dry-run: build-check + uv run python -c "from pathlib import Path; dist=Path('dist'); files=sorted(list(dist.glob('*.whl')) + list(dist.glob('*.tar.gz'))); assert files, 'No build artefacts found in dist/'; assert all(file.name.startswith('pytest_notebook_policy-') for file in files), 'Unexpected non-target artefact found in dist/'; print('\n'.join(str(file) for file in files))" + +prepare-1_0_0: refresh-fixtures qa build-check diff --git a/pyproject.toml b/pyproject.toml index 927ed4e..a33b783 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pytest-notebook-policy" -version = "0.8.0" +version = "1.0.0" description = "Pytest plugin for notebook policy and quality checks" readme = "README.md" license = "MIT" @@ -14,14 +14,13 @@ classifiers = [ "Framework :: Pytest", "Intended Audience :: Developers", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Quality Assurance", ] -requires-python = ">=3.10" +requires-python = ">=3.12" dependencies = [ + "jinja2>=3.1", "pytest>=8.0", ] @@ -29,6 +28,7 @@ dependencies = [ Homepage = "https://github.com/DataBooth/pytest-notebook-policy" Repository = "https://github.com/DataBooth/pytest-notebook-policy" Issues = "https://github.com/DataBooth/pytest-notebook-policy/issues" +Documentation = "https://databooth.github.io/pytest-notebook-policy/" [project.scripts] pytest-notebook-quality = "pytest_notebook_policy.quality:main" @@ -47,13 +47,14 @@ build-backend = "uv_build" [dependency-groups] dev = [ + "great-docs>=0.14.0", "jupytext>=1.16", "pytest>=8.0", "ruff>=0.14.0", ] [tool.ruff] -target-version = "py310" +target-version = "py312" line-length = 100 extend-exclude = ["tests/fixtures/real/*.ipynb"] diff --git a/tmp/manual_checks/experiments/pyproject.toml b/tmp/manual_checks/experiments/pyproject.toml new file mode 100644 index 0000000..8c34dd6 --- /dev/null +++ b/tmp/manual_checks/experiments/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "experiments" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [] + +[dependency-groups] +dev = [ + "pytest-notebook-policy[sync]==1.0.0", +] + +[tool.uv.sources] +pytest-notebook-policy = { workspace = true } diff --git a/uv.lock b/uv.lock index c6b43c2..472a3fa 100644 --- a/uv.lock +++ b/uv.lock @@ -1,35 +1,1066 @@ version = 1 revision = 3 -requires-python = ">=3.10" +requires-python = ">=3.13" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version < '3.14'", +] + +[manifest] +members = [ + "experiments", + "pytest-notebook-policy", +] + +[[package]] +name = "anyio" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/b5/001890774a9552aff22502b8da382593109ce0c95314abaebbb116567545/anyio-4.14.0.tar.gz", hash = "sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89", size = 253586, upload-time = "2026-06-15T22:00:49.021Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9", size = 123506, upload-time = "2026-06-15T22:00:47.595Z" }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, +] + +[[package]] +name = "arrow" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "async-lru" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/1f/989ecfef8e64109a489fff357450cb73fa73a865a92bd8c272170a6922c2/async_lru-2.3.0.tar.gz", hash = "sha256:89bdb258a0140d7313cf8f4031d816a042202faa61d0ab310a0a538baa1c24b6", size = 16332, upload-time = "2026-03-19T01:04:32.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl", hash = "sha256:eea27b01841909316f2cc739807acea1c623df2be8c5cfad7583286397bb8315", size = 8403, upload-time = "2026-03-19T01:04:30.883Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, +] + +[[package]] +name = "bleach" +version = "6.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/3c/e12ac860709702bd5ebeb9b56a4fe334f1001246ee1b8f2b7ee28912df7d/bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452", size = 204857, upload-time = "2026-06-05T13:01:13.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/9d/40b6267367182187139a4000b82a3b287d84d745bccd808e75d916920e9d/bleach-6.4.0-py3-none-any.whl", hash = "sha256:4b6b6a54fff2e69a3dde9d21cc6301220bee3c3cb792187d11403fd795031081", size = 165109, upload-time = "2026-06-05T13:01:12.504Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "comm" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/aa/12037145b7a56eaa5b29b41872f7a21b538e807e13f32c4d3c46e59be084/debugpy-1.8.21.tar.gz", hash = "sha256:a3c53278e84c94e11bd87c53970ec391d1a67396c8b22609fcac576520e611a6", size = 1697577, upload-time = "2026-06-01T19:30:35.156Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/6b/d817e1f8cc77aa055d37fba092e0febfdff40fe652d8d53d4cd7a86ad98d/debugpy-1.8.21-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:13678151fc401e2d68c9880b91e28714f797d40422994572b24560ef80910a88", size = 2477398, upload-time = "2026-06-01T19:30:57.644Z" }, + { url = "https://files.pythonhosted.org/packages/48/57/412421516afc3055fa577516f00beec3d663f9b0ab330639547ae6c57720/debugpy-1.8.21-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:ecbd158386c31ffe71d46f72d44d56e66331ab9b16cad649156d514368f23ab2", size = 3962096, upload-time = "2026-06-01T19:30:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2c616337cf6ba7b07ebbc97f02c6c945a8e2f76b365e33ee809c32ee36d1/debugpy-1.8.21-cp313-cp313-win32.whl", hash = "sha256:2c2ae706dec41d99a9ca1f7ebc987a83e65578363be6f6b3ac9067504917fae1", size = 5336288, upload-time = "2026-06-01T19:31:00.79Z" }, + { url = "https://files.pythonhosted.org/packages/f8/99/9175103392f84c4b1bf7622888cdc68da07f0ff7d9e581266428f6776033/debugpy-1.8.21-cp313-cp313-win_amd64.whl", hash = "sha256:aa648733047443eb1d07682c4ef287d36a54507b643ffdf38b09a3ef002c72a0", size = 5376567, upload-time = "2026-06-01T19:31:02.56Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3d/f4bbb323a548bfab2af3d6b4ffd9bf22636e55956a1285d317a1de643aad/debugpy-1.8.21-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9bb2a685287a2ac9b181cde89edcec64845cb51de7faaa75badb9a698bc24782", size = 2477209, upload-time = "2026-06-01T19:31:04.157Z" }, + { url = "https://files.pythonhosted.org/packages/8c/2d/6e7ec524984a1702777868de49a4c53202bddac2a432a76a093469587750/debugpy-1.8.21-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:3d6922439bf33fd38a3e2c447869ebc7b97da5cd3d329ff1ef9bc06c4903437e", size = 3927115, upload-time = "2026-06-01T19:31:05.863Z" }, + { url = "https://files.pythonhosted.org/packages/97/47/d1aa6d64005a98a9144647d99306b419396f9ad7bf1d73c119e17a81fb4d/debugpy-1.8.21-cp314-cp314-win32.whl", hash = "sha256:15d4963bd5ffa48f0da0947fd06757fa7621945048a14ad7705431566d3c0e7c", size = 5336724, upload-time = "2026-06-01T19:31:07.711Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/b905b90d163af11878c1af8abafa4a25206335e112e284e413454543a6da/debugpy-1.8.21-cp314-cp314-win_amd64.whl", hash = "sha256:fe0744a12353406de0ae8ccff0d0a4a666f00801a3db8fd04e7a5f761cd520e8", size = 5373803, upload-time = "2026-06-01T19:31:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl", hash = "sha256:b1e37d333663c8851516a47364ef473da127f9caebe4417e6df6f5825a7e9a92", size = 5352888, upload-time = "2026-06-01T19:31:25.186Z" }, +] + +[[package]] +name = "decorator" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "experiments" +version = "0.1.0" +source = { virtual = "tmp/manual_checks/experiments" } + +[package.dev-dependencies] +dev = [ + { name = "pytest-notebook-policy", extra = ["sync"] }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [{ name = "pytest-notebook-policy", extras = ["sync"], editable = "." }] + +[[package]] +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, +] + +[[package]] +name = "fqdn" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, +] + +[[package]] +name = "great-docs" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "griffe" }, + { name = "jupyter" }, + { name = "pillow" }, + { name = "py-yaml12" }, + { name = "pygments" }, + { name = "requests" }, + { name = "ruff" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/1b/1620d56583cd14b3398298f7f72ebc7d2fb68dd427c39d51a2eb6deef11f/great_docs-0.14.0.tar.gz", hash = "sha256:86df097ff11525d352664b7a1beca8f852bc2bae397edc7af46663e4517ad69c", size = 28353775, upload-time = "2026-06-16T14:09:19.67Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/46/662719f5aada86c51bee15da9638e8f4bf8ce746e9e4988dc7b9409b75ed/great_docs-0.14.0-py3-none-any.whl", hash = "sha256:06fb21a33dca4058d9e904df898bf320e811107177a5fba782c7cf0a5fb04f8f", size = 947386, upload-time = "2026-06-16T14:09:18.147Z" }, +] + +[[package]] +name = "griffe" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112, upload-time = "2025-11-10T15:03:15.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705, upload-time = "2025-11-10T15:03:13.549Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "ipykernel" +version = "7.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio2" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/c4/e4a38f579de4225a561305666f7541cdabb30075def2aa1ac17bd73c1fb5/ipykernel-7.3.0.tar.gz", hash = "sha256:9acaaaf97d16355166e4085afe9d225bfbdf2b7ef520f9df3be8f2b248275e09", size = 184899, upload-time = "2026-06-10T08:41:25.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl", hash = "sha256:897eb64da762549ef610698fca5e9675195ec6ac8ec7f19d81ce1ca20c876057", size = 120583, upload-time = "2026-06-10T08:41:23.648Z" }, +] + +[[package]] +name = "ipython" +version = "9.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "psutil", marker = "sys_platform != 'emscripten'" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/23/3a27530575643c8bb7bfc757a28e2e7ef80092afbf59a2bc5716320b6602/ipython-9.14.1.tar.gz", hash = "sha256:f913bf74df06d458e46ced84ca506c23797590d594b236fe60b14df213291e7b", size = 4433457, upload-time = "2026-06-05T08:12:34.921Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/22/58818a63eaf8982b67632b1bc20585c811611b15a8da19d6012323dc76a5/ipython-9.14.1-py3-none-any.whl", hash = "sha256:5d4a9ecaa3b10e6e5f269dd0948bdb58ca9cb851899cd23e07c320d3eb11613c", size = 627770, upload-time = "2026-06-05T08:12:33.045Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "ipywidgets" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "comm" }, + { name = "ipython" }, + { name = "jupyterlab-widgets" }, + { name = "traitlets" }, + { name = "widgetsnbextension" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/ae/c5ce1edc1afe042eadb445e95b0671b03cee61895264357956e61c0d2ac0/ipywidgets-8.1.8.tar.gz", hash = "sha256:61f969306b95f85fba6b6986b7fe45d73124d1d9e3023a8068710d47a22ea668", size = 116739, upload-time = "2025-11-01T21:18:12.393Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl", hash = "sha256:ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e", size = 139808, upload-time = "2025-11-01T21:18:10.956Z" }, +] + +[[package]] +name = "isoduration" +version = "20.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arrow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, +] + +[[package]] +name = "jedi" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/93/242e2eab5fe682ffcb8b0084bde703a41d51e17ee0f3a31ff0d9d813620a/jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67", size = 4884812, upload-time = "2026-05-01T23:38:43.919Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "json5" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/7d/05c46a96a78147ae3bf99c2f4169ce144a70220b8d6fcd56f6ec368b8ce9/json5-0.15.0.tar.gz", hash = "sha256:7424d1f1eb1d56da6e3d70643f53619862b4ce81440bdb8ecfd6f875e5ba4a71", size = 53278, upload-time = "2026-06-19T20:08:27.716Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/be/59527c99478aade6bb33a68d72e6e18dd4e6ff6eacfc7d01bdb15bc76912/json5-0.15.0-py3-none-any.whl", hash = "sha256:56636a30c0e8a4665fe2179c0212f32eae3796dea89ea6f649b9436ecdb39618", size = 36570, upload-time = "2026-06-19T20:08:26.748Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[package.optional-dependencies] +format-nongpl = [ + { name = "fqdn" }, + { name = "idna" }, + { name = "isoduration" }, + { name = "jsonpointer" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "rfc3987-syntax" }, + { name = "uri-template" }, + { name = "webcolors" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "ipywidgets" }, + { name = "jupyter-console" }, + { name = "jupyterlab" }, + { name = "nbconvert" }, + { name = "notebook" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/f3/af28ea964ab8bc1e472dba2e82627d36d470c51f5cd38c37502eeffaa25e/jupyter-1.1.1.tar.gz", hash = "sha256:d55467bceabdea49d7e3624af7e33d59c37fff53ed3a350e1ac957bed731de7a", size = 5714959, upload-time = "2024-08-30T07:15:48.299Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl", hash = "sha256:7a59533c22af65439b24bbe60373a4e95af8f16ac65a6c00820ad378e3f7cc83", size = 2657, upload-time = "2024-08-30T07:15:47.045Z" }, +] + +[[package]] +name = "jupyter-builder" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/45/d0df8b43c10a61529c0f4a8af5e19ebe108f0c3af8f57e0fc358969907af/jupyter_builder-1.0.2.tar.gz", hash = "sha256:6155d78a5325010532a6419ffcba89eac643fd1aa56ea83115e661924d6f6aab", size = 968638, upload-time = "2026-06-12T02:33:25.767Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/b6/c418e0b3256f67c04933566b80bfce947350682db92c4b786a8653db32d6/jupyter_builder-1.0.2-py3-none-any.whl", hash = "sha256:b024f65d36e1d530542db597b00dd513261aa59842e0d0fbbb1015a9f1935e9c", size = 910789, upload-time = "2026-06-12T02:33:23.317Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/dc/5512503b088997c2250b8bf18258fba9d9ce5ead641183700960d3c9d342/jupyter_client-8.9.1.tar.gz", hash = "sha256:a58f730dd9e728ba16ba1d62ebccf7ffe1ebbdbce4e95cfae941b7321ae1f4fa", size = 359256, upload-time = "2026-06-09T13:15:01.033Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl", hash = "sha256:0b7a295bc46e8751e9adae84781f726c851c1d911bd793edc4a3bde942e3da81", size = 109828, upload-time = "2026-06-09T13:14:58.835Z" }, +] + +[[package]] +name = "jupyter-console" +version = "6.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "pyzmq" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/2d/e2fd31e2fc41c14e2bcb6c976ab732597e907523f6b2420305f9fc7fdbdb/jupyter_console-6.6.3.tar.gz", hash = "sha256:566a4bf31c87adbfadf22cdf846e3069b59a71ed5da71d6ba4d8aaad14a53539", size = 34363, upload-time = "2023-03-06T14:13:31.02Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/77/71d78d58f15c22db16328a476426f7ac4a60d3a5a7ba3b9627ee2f7903d4/jupyter_console-6.6.3-py3-none-any.whl", hash = "sha256:309d33409fcc92ffdad25f0bcdf9a4a9daa61b6f341177570fdac03de5352485", size = 24510, upload-time = "2023-03-06T14:13:28.229Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "jupyter-events" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema", extra = ["format-nongpl"] }, + { name = "packaging" }, + { name = "python-json-logger" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/f8/475c4241b2b75af0deaae453ed003c6c851766dbc44d332d8baf245dc931/jupyter_events-0.12.1.tar.gz", hash = "sha256:faff25f77218335752f35f23c5fe6e4a392a7bd99a5939ccb9b8fbf594636cf3", size = 62854, upload-time = "2026-04-20T23:17:50.66Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/6c/6fcde0c8f616ed360ffd3587f7db9e225a7e62b583a04494d2f069cf64ea/jupyter_events-0.12.1-py3-none-any.whl", hash = "sha256:c366585253f537a627da52fa7ca7410c5b5301fe893f511e7b077c2d93ec8bcf", size = 19512, upload-time = "2026-04-20T23:17:48.927Z" }, +] + +[[package]] +name = "jupyter-lsp" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/ff/1e4a61f5170a9a1d978f3ac3872449de6c01fc71eaf89657824c878b1549/jupyter_lsp-2.3.1.tar.gz", hash = "sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6", size = 55677, upload-time = "2026-04-02T08:10:06.749Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/e8/9d61dcbd1dce8ef418f06befd4ac084b4720429c26b0b1222bc218685eff/jupyter_lsp-2.3.1-py3-none-any.whl", hash = "sha256:71b954d834e85ff3096400554f2eefaf7fe37053036f9a782b0f7c5e42dadb81", size = 77513, upload-time = "2026-04-02T08:10:01.753Z" }, +] + +[[package]] +name = "jupyter-server" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "argon2-cffi" }, + { name = "jinja2" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "jupyter-events" }, + { name = "jupyter-server-terminals" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "prometheus-client" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "pyzmq" }, + { name = "send2trash" }, + { name = "terminado" }, + { name = "tornado" }, + { name = "traitlets" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/dc/db3a582633170186f8c8b31298d7eb26ad0eb031a1f53476c258b64eed05/jupyter_server-2.20.0.tar.gz", hash = "sha256:b5778ba337d8015a3dc2b80803ecdd5ac18d3797fddf61a50ea5fb472b4ebe14", size = 756523, upload-time = "2026-06-17T12:09:09.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/71/8c002223e873a870f5c41dc69b0a7c922301123e4a31d5d01ecb700aef77/jupyter_server-2.20.0-py3-none-any.whl", hash = "sha256:c3b67c93c471e947c18b5026f04f21614218adb706df8f48227d3ee8e0a7cdcc", size = 393143, upload-time = "2026-06-17T12:09:07.234Z" }, +] + +[[package]] +name = "jupyter-server-terminals" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "terminado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/a7/bcd0a9b0cbba88986fe944aaaf91bfda603e5a50bda8ed15123f381a3b2f/jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5", size = 31770, upload-time = "2026-01-14T16:53:20.213Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl", hash = "sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14", size = 13704, upload-time = "2026-01-14T16:53:18.738Z" }, +] + +[[package]] +name = "jupyterlab" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-lru" }, + { name = "httpx" }, + { name = "ipykernel" }, + { name = "jinja2" }, + { name = "jupyter-builder" }, + { name = "jupyter-core" }, + { name = "jupyter-lsp" }, + { name = "jupyter-server" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, + { name = "packaging" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/3c/1ebd737b860cdbe61eed71536d06aab4e1781fbdcf07ecd5a1afe05b8adf/jupyterlab-4.6.0.tar.gz", hash = "sha256:6a8b88f2aae7ed4d012c634fc957c1a27f3aa217c32f0ced0175fac9ee17f9e5", size = 28181861, upload-time = "2026-06-18T13:52:56.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/eb/aa48075d0aa3d0188db34ba2704f53791757743c0bb02e18c4eef989b6de/jupyterlab-4.6.0-py3-none-any.whl", hash = "sha256:b6938cb8a1ef3d43860ff4745a680c62cc0a9385f9672295bb56cd2e7cfeebe2", size = 17143447, upload-time = "2026-06-18T13:52:51.42Z" }, +] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, +] + +[[package]] +name = "jupyterlab-server" +version = "2.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "jinja2" }, + { name = "json5" }, + { name = "jsonschema" }, + { name = "jupyter-server" }, + { name = "packaging" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/2c/90153f189e421e93c4bb4f9e3f59802a1f01abd2ac5cf40b152d7f735232/jupyterlab_server-2.28.0.tar.gz", hash = "sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c", size = 76996, upload-time = "2025-10-22T13:59:18.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl", hash = "sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968", size = 59830, upload-time = "2025-10-22T13:59:16.767Z" }, +] + +[[package]] +name = "jupyterlab-widgets" +version = "3.0.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/2d/ef58fed122b268c69c0aa099da20bc67657cdfb2e222688d5731bd5b971d/jupyterlab_widgets-3.0.16.tar.gz", hash = "sha256:423da05071d55cf27a9e602216d35a3a65a3e41cdf9c5d3b643b814ce38c19e0", size = 897423, upload-time = "2025-11-01T21:11:29.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" }, +] + +[[package]] +name = "jupytext" +version = "1.19.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/2d/15624c3d9440d85a280ff13d2d23afd989802f25470ac59932f4fef6f0c6/jupytext-1.19.3.tar.gz", hash = "sha256:713c3ed4441afe0f31474d28ea2e6b61a268c04c40fd78e5ccfd7f7ac9e9f766", size = 4305350, upload-time = "2026-05-17T09:09:29.294Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/ec/d9be3bd1db141e76b2f525c265f70e66edd30a51a3307d8edf0ef1909c54/jupytext-1.19.3-py3-none-any.whl", hash = "sha256:acf75492f80895ad8e664fd8db1708b617008dd0e71c341a1abc3d0d07310ed0", size = 170579, upload-time = "2026-05-17T09:09:27.478Z" }, +] + +[[package]] +name = "lark" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mistune" +version = "3.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/84/620cc3f7e3adf6f5067e10f4dbae71295d8f9e16d5d3f9ef97c40f2f592c/mistune-3.2.1.tar.gz", hash = "sha256:7c8e5501d38bac1582e067e46c8343f17d57ea1aaa735823f3aba1fd59c88a28", size = 98003, upload-time = "2026-05-03T14:33:22.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/7f/a946aa4f8752b37102b41e64dca18a1976ac705c3a0d1dfe74d820a02552/mistune-3.2.1-py3-none-any.whl", hash = "sha256:78cdb0ba5e938053ccf63651b352508d2efa9411dc8810bfb05f2dc5140c0048", size = 53749, upload-time = "2026-05-03T14:33:20.551Z" }, +] [[package]] -name = "colorama" -version = "0.4.6" +name = "nbclient" +version = "0.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/a5/b3bae4b590c0cbcada2c63a34f7580024e834a8ba213e949a2f906705787/nbclient-0.11.0.tar.gz", hash = "sha256:04a134a5b087f2c5887f228aca155db50169b8cd9334dee6942c8e927e56081a", size = 62535, upload-time = "2026-06-05T07:52:41.746Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, + { url = "https://files.pythonhosted.org/packages/36/c9/94d73e5a01c5b926c3fa2496e97d7a8dc28ed5a77c0b2ed712f1a62e6694/nbclient-0.11.0-py3-none-any.whl", hash = "sha256:ef7fa0d59d6e1d41103933d8a445a18d5de860ca6b613b87b8574accdb3c2895", size = 25288, upload-time = "2026-06-05T07:52:40.115Z" }, ] [[package]] -name = "exceptiongroup" -version = "1.3.1" +name = "nbconvert" +version = "7.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "beautifulsoup4" }, + { name = "bleach", extra = ["css"] }, + { name = "defusedxml" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/b1/708e53fe2e429c103c6e6e159106bcf0357ac41aa4c28772bd8402339051/nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2", size = 865311, upload-time = "2026-04-08T00:44:14.914Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8", size = 261927, upload-time = "2026-04-08T00:44:12.845Z" }, ] [[package]] -name = "iniconfig" -version = "2.3.0" +name = "nbformat" +version = "5.10.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "nest-asyncio2" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/73/731debf26e27e0a0323d7bda270dc2f634b398e38f040a09da1f4351d0aa/nest_asyncio2-1.7.2.tar.gz", hash = "sha256:1921d70b92cc4612c374928d081552efb59b83d91b2b789d935c665fa01729a8", size = 14743, upload-time = "2026-02-13T00:34:04.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl", hash = "sha256:f5dfa702f3f81f6a03857e9a19e2ba578c0946a4ad417b4c50a24d7ba641fe01", size = 7843, upload-time = "2026-02-13T00:34:02.691Z" }, +] + +[[package]] +name = "notebook" +version = "7.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-builder" }, + { name = "jupyter-server" }, + { name = "jupyterlab" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/44/d5c65783f490298473bb1c05722e05ee2256231389559c2c5ae0a3e5d975/notebook-7.6.0.tar.gz", hash = "sha256:ea13e79e601bf273074895fdfb17dd3f2da916d3c045e0b9c47d18b16ab62481", size = 5497344, upload-time = "2026-06-18T16:18:55.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d1/e617c40db57ff40e75f43a7d4d1c305e3a54c053ab5cb0534a6c314664f9/notebook-7.6.0-py3-none-any.whl", hash = "sha256:98aa2811b54ac191321d5dfce12ca700f8a511a33a26e4de2fa106a357c43d6a", size = 5544575, upload-time = "2026-06-18T16:18:52.551Z" }, +] + +[[package]] +name = "notebook-shim" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167, upload-time = "2024-02-14T23:35:18.353Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, ] [[package]] @@ -41,6 +1072,103 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, +] + +[[package]] +name = "parso" +version = "0.8.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/4b/90c937815137d43ce71ba043cd3566221e9df6b9c805f24b5d138c9d40a7/parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1", size = 401824, upload-time = "2026-05-01T23:13:02.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/5d/8268b644392ee874ee82a635cd0df1773de230bde356c38de28e298392cc/parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c", size = 107025, upload-time = "2026-05-01T23:12:58.867Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "pillow" +version = "12.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -50,6 +1178,103 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "prometheus-client" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/fb/d9aa83ffe43ce1f19e557c0971d04b90561b0cfd50762aafb01968285553/prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28", size = 86035, upload-time = "2026-04-09T19:53:42.359Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9b/d4b1e644385499c8346fa9b622a3f030dce14cd6ef8a1871c221a17a67e7/prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1", size = 64154, upload-time = "2026-04-09T19:53:41.324Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "py-yaml12" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/eb/a0b260f7b8625d21c7a900311c863e899d2e258ebdc9a056a0cd17031f80/py_yaml12-0.1.0.tar.gz", hash = "sha256:e7b6140fad56d5528e6d51c7b8ff79bdb27631b73a9a7c1bc4b55fda1bddbaef", size = 117187, upload-time = "2025-12-16T18:06:44.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/d9/5f4ccd96d7c776e822e63d2b79e3594653bcf55720dbad1cdcbc18a170ad/py_yaml12-0.1.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f6dcce183b6744c0909a8440d1f7a00836023826e463601d03b879c0e84c8a19", size = 468720, upload-time = "2025-12-16T18:06:35.053Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ab/c08a62f92f69d45fc479b1e893968c9d9b2dc87809e538cea0409cdca25e/py_yaml12-0.1.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b9c8e54afc415d4eb038a024bc9e28fed95b8c447708bf1a24da0291bbfdd8", size = 501529, upload-time = "2025-12-16T18:06:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/b6/05/a8408ca1f4de61f8ef626391908006fa1c5e54eaf0f3288d78bc663d65aa/py_yaml12-0.1.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e637189cb07e180bdc9db847ca5711b54df2233b55604bf3609cfa0f0228f7c", size = 519733, upload-time = "2025-12-16T18:06:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/2b/73/4e1f7cdc26b1deb60ea42ca4b3717fab279a42df10f9b16b869550e92825/py_yaml12-0.1.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:003a9526a4731010f4e37eced9f33fd070ff563fe3bc2fe2416ab7598982b285", size = 640522, upload-time = "2025-12-16T18:06:25.551Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a9/7be4ced3e490fd2a5e92cc8e8fe5b6143036a480ba004235e0a78ccbc6f9/py_yaml12-0.1.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6845871e3a0233d71f75e56caca5c350f48cb5d9c50e6bb37874243be1ca5f2b", size = 530277, upload-time = "2025-12-16T18:06:27.115Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0c/1e1b171246a44753bf04c6399179da1ccf99cfb9de1e086afd7326765b34/py_yaml12-0.1.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbc5d54ec0471c0f64b931ea0907f655c728597b98a9e7f4a98f4c76cc3614ba", size = 516865, upload-time = "2025-12-16T18:06:31.845Z" }, + { url = "https://files.pythonhosted.org/packages/e3/00/b4d1db376b35f0021c7e45b6351536c217426b32f71ad8b89782a3a6b562/py_yaml12-0.1.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3d9f1ad2e166eb113fd4e44263baad929e268dc15abc7eb87736aa7148d07534", size = 557448, upload-time = "2025-12-16T18:06:28.78Z" }, + { url = "https://files.pythonhosted.org/packages/7a/41/828743df93b97ca987a808895f5171f2430ee61f18f25fe3d9033075879d/py_yaml12-0.1.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c9fabfe8fdf53eb661eaaace95961dae742abe0679a9a989936e8b2cf893dbe", size = 683748, upload-time = "2025-12-16T18:06:37.961Z" }, + { url = "https://files.pythonhosted.org/packages/74/b4/f293b9fa7ad26b86278840f3c571a211111091fd03ba553fa45e4bcaf780/py_yaml12-0.1.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0226a5aeba67b97ed8e2947da332e5c3b5ef004c0058d2cff2413c99fc4b9ff4", size = 789703, upload-time = "2025-12-16T18:06:39.272Z" }, + { url = "https://files.pythonhosted.org/packages/a9/18/703812d770166e292e13388bf899f2540c83a8c9d6505862fe756b2818d1/py_yaml12-0.1.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:a29558b3ba9138eac75996dd0a92fda7993c085aa10c1c93b641239389b731be", size = 764707, upload-time = "2025-12-16T18:06:40.943Z" }, + { url = "https://files.pythonhosted.org/packages/81/f5/936eb310b0c84d044d92ccd90b99b53fb8326dc6084ce59fe47a6b78eb93/py_yaml12-0.1.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fcd21f854faf23bd1fff4822e7cf29a6e8b69dc43f69f6f2f34a698dcde4548b", size = 719443, upload-time = "2025-12-16T18:06:42.464Z" }, + { url = "https://files.pythonhosted.org/packages/37/5b/eaa16c85074b44b1b6843d52fb3b75da26410dcea1e98cf6b6c4f46f27af/py_yaml12-0.1.0-cp310-abi3-win32.whl", hash = "sha256:2552c7957819f53b7d667baab38e59db066f067aafa6695849ae94c0f01bf999", size = 349644, upload-time = "2025-12-16T18:06:48.417Z" }, + { url = "https://files.pythonhosted.org/packages/48/60/d50a1e5e44333aa6533080df9c587b6b25e0205071078c63619202592bb3/py_yaml12-0.1.0-cp310-abi3-win_amd64.whl", hash = "sha256:228ce36885a16dcaf20007bf7b74ae1b36ccd20a46149f369e4b8164e7542023", size = 362326, upload-time = "2025-12-16T18:06:45.271Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "pygments" version = "2.20.0" @@ -65,12 +1290,10 @@ version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ @@ -78,28 +1301,286 @@ wheels = [ ] [[package]] -name = "pytest-marimo" -version = "0.1.0" +name = "pytest-notebook-policy" +version = "1.0.0" source = { editable = "." } dependencies = [ + { name = "jinja2" }, { name = "pytest" }, ] +[package.optional-dependencies] +sync = [ + { name = "jupytext" }, +] + [package.dev-dependencies] dev = [ + { name = "great-docs" }, + { name = "jupytext" }, { name = "pytest" }, { name = "ruff" }, ] [package.metadata] -requires-dist = [{ name = "pytest", specifier = ">=8.0" }] +requires-dist = [ + { name = "jinja2", specifier = ">=3.1" }, + { name = "jupytext", marker = "extra == 'sync'", specifier = ">=1.16" }, + { name = "pytest", specifier = ">=8.0" }, +] +provides-extras = ["sync"] [package.metadata.requires-dev] dev = [ + { name = "great-docs", specifier = ">=0.14.0" }, + { name = "jupytext", specifier = ">=1.16" }, { name = "pytest", specifier = ">=8.0" }, { name = "ruff", specifier = ">=0.14.0" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-json-logger" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/ff/3cc9165fd44106973cd7ac9facb674a65ed853494592541d339bdc9a30eb/python_json_logger-4.1.0.tar.gz", hash = "sha256:b396b9e3ed782b09ff9d6e4f1683d46c83ad0d35d2e407c09a9ebbf038f88195", size = 17573, upload-time = "2026-03-29T04:39:56.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/be/0631a861af4d1c875f096c07d34e9a63639560a717130e7a87cbc82b7e3f/python_json_logger-4.1.0-py3-none-any.whl", hash = "sha256:132994765cf75bf44554be9aa49b06ef2345d23661a96720262716438141b6b2", size = 15021, upload-time = "2026-03-29T04:39:55.266Z" }, +] + +[[package]] +name = "pywinpty" +version = "3.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/ef/2d27f30c59a67be7025b2d7858c8c2d282b74d66544b2384730b82de74fd/pywinpty-3.0.5.tar.gz", hash = "sha256:61db0db063de9865adbea66db294628f8577f608d9764a4c7d3384eeacc4e81b", size = 16223484, upload-time = "2026-06-11T00:11:58.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f4/2a464b9893cceb3b3f416356e94fdc3e1bca9476993927e4e6d99fe95382/pywinpty-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:48db1b0ad9d0a1b81dcaaa7163a99a7808deaceb0c1b2344716dc1fc090c3c4c", size = 2090471, upload-time = "2026-06-10T23:42:11.071Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2c/a138491a0afbdb50eb79395577bd326d4b0fbde7209417d1a8087ff2493a/pywinpty-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:2c6008fb2d3774b48693b2fcb7f2cc317ade9dc581289a964ffeeaf81307c9b5", size = 815518, upload-time = "2026-06-10T23:42:02.363Z" }, + { url = "https://files.pythonhosted.org/packages/6f/15/54400049a380582acd1282665c70fcf11e0bd3713679aca78e24c3aae738/pywinpty-3.0.5-cp313-cp313t-win_amd64.whl", hash = "sha256:22ce1b780d89821cc52daf6eac0708af22d93d000ce9c7c07e37489db8594598", size = 2089920, upload-time = "2026-06-10T23:44:13.395Z" }, + { url = "https://files.pythonhosted.org/packages/94/0c/6f24f3c0799f502259b24bdf841a99ad2b0d59df5c2525b4e2a286d14be2/pywinpty-3.0.5-cp313-cp313t-win_arm64.whl", hash = "sha256:9c2919a81bc5cfb09b86fc5a002112b2de95ca4304a07413cbeeb746a1307a5c", size = 814520, upload-time = "2026-06-10T23:43:28.588Z" }, + { url = "https://files.pythonhosted.org/packages/e9/23/f3cd1b1e5fc56517f54452c49f92049e7dd9ffc8a63de22a495581f50d04/pywinpty-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:03bb3c16d691d9242267201830bcd0e64a9b663170e9042bc84b210da9de15ac", size = 2090663, upload-time = "2026-06-10T23:43:59.845Z" }, + { url = "https://files.pythonhosted.org/packages/9d/dd/96d6cbfc6d9ddab5c1c2f92c26545ae8997446a2ba7ee2024cd43c81f49b/pywinpty-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:89c5c6ef08997a3b4b277b214a35fe15cab4dd6d119f0140aa71df5b1168fdbc", size = 815700, upload-time = "2026-06-10T23:40:50.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/36/d98087bce0acaa4cce7f196103cfa7be3f63ce65f52473bb3e38784ae5d9/pywinpty-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7b566165e0c5fdd6abe167a5ac8b954be6a843eb55a85946576d6bc1dea03d6d", size = 2090093, upload-time = "2026-06-10T23:40:58.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/fd/fe2b0db922ba052ce3976a08f3fc05d0c05047c8b4ebb6102e832b8ef563/pywinpty-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:24366280a8aa677323da87bec729cb3ea3b35367386cece0978bdc6e4695c690", size = 814517, upload-time = "2026-06-10T23:42:34.946Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, +] + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760, upload-time = "2019-10-28T16:00:19.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" }, +] + +[[package]] +name = "rfc3987-syntax" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lark" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, +] + [[package]] name = "ruff" version = "0.15.13" @@ -126,57 +1607,96 @@ wheels = [ ] [[package]] -name = "tomli" -version = "2.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, - { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, - { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, - { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, - { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, - { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, - { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, - { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, - { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, - { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, - { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, - { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, - { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, - { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, - { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, - { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, - { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, - { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, - { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, - { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, - { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, - { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, - { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, - { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, - { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, - { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, - { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, - { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, - { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, - { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, - { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, - { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, - { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, - { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, - { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, - { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, - { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, - { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, - { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, - { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, - { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +name = "send2trash" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/f0/184b4b5f8d00f2a92cf96eec8967a3d550b52cf94362dad1100df9e48d57/send2trash-2.1.0.tar.gz", hash = "sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459", size = 17255, upload-time = "2026-01-14T06:27:36.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610, upload-time = "2026-01-14T06:27:35.218Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "terminado" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "os_name != 'nt'" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/64/24/95ec527ad67b76d59299e5465b3935d05e4294b7e0290a3924b7487df30b/tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2", size = 519252, upload-time = "2026-06-08T17:34:51.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163", size = 448543, upload-time = "2026-06-08T17:34:38.052Z" }, + { url = "https://files.pythonhosted.org/packages/92/4f/090b1431e5a43df696feceffc268c5383cc079ecb5f08ce58f917109aafe/tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100", size = 446707, upload-time = "2026-06-08T17:34:39.594Z" }, + { url = "https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972", size = 449774, upload-time = "2026-06-08T17:34:41.204Z" }, + { url = "https://files.pythonhosted.org/packages/35/37/d434c73f4c6e014b745b9b37085f34f40c022f007efff3d7fe65991899f3/tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b", size = 450745, upload-time = "2026-06-08T17:34:42.531Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/56b9aff361d7f1ab728a805ec7d7ea835f8807afa9f5cc690ea0e630efb9/tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92", size = 450578, upload-time = "2026-06-08T17:34:43.787Z" }, + { url = "https://files.pythonhosted.org/packages/02/30/a7444fb23aa76860a14198fab96ac79f1866b0a6e19e26c4381b0938e50f/tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5", size = 449985, upload-time = "2026-06-08T17:34:45.326Z" }, + { url = "https://files.pythonhosted.org/packages/5c/42/5f0e56c01e8d9d36f4e23f367b85ae6cae0c1ecddd5e6977d8388ad27488/tornado-6.5.7-cp39-abi3-win32.whl", hash = "sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4", size = 451047, upload-time = "2026-06-08T17:34:46.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl", hash = "sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4", size = 451485, upload-time = "2026-06-08T17:34:48.248Z" }, + { url = "https://files.pythonhosted.org/packages/71/2e/7b1c769803121b809112cf9a00681c472eae1d80e32d7ec0e0bd61d0d0e1/tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796", size = 450506, upload-time = "2026-06-08T17:34:49.702Z" }, +] + +[[package]] +name = "traitlets" +version = "5.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/22/40f55b26baeab80c2d7b3f1db0682f8954e4617fee7d90ce634022ef05c6/traitlets-5.15.0.tar.gz", hash = "sha256:4fead733f81cf1c4c938e06f8ca4633896833c9d89eff878159457f4d4392971", size = 163197, upload-time = "2026-05-06T08:05:58.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/98/a9937a969d018a23badfea0b381f66783649d48e0ea6c41923265c3cbeb3/traitlets-5.15.0-py3-none-any.whl", hash = "sha256:fb36a18867a6803deab09f3c5e0fa81bb7b26a5c9e82501c9933f759166eff40", size = 85877, upload-time = "2026-05-06T08:05:55.853Z" }, ] [[package]] @@ -187,3 +1707,75 @@ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac8 wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] + +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] + +[[package]] +name = "uri-template" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/b4/51fe890511f0f242d07cb1ebe6a5b6db417262b9d2568b460347c57d95cc/wcwidth-0.8.1.tar.gz", hash = "sha256:faf5b4a5366a72dc49cad48cdf21f52bdf63bdda995178e483ba247ff79089b9", size = 1466072, upload-time = "2026-06-08T05:57:23.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/6e/95b0e537de1f4d4301f76f944642c6da50d1511cc7b3d64dc418a66c7509/wcwidth-0.8.1-py3-none-any.whl", hash = "sha256:f453740b1e4a4f3291faa37944c555d71056c4da08d59809b307ef4feba695c8", size = 323092, upload-time = "2026-06-08T05:57:21.413Z" }, +] + +[[package]] +name = "webcolors" +version = "25.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + +[[package]] +name = "widgetsnbextension" +version = "4.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/f4/c67440c7fb409a71b7404b7aefcd7569a9c0d6bd071299bf4198ae7a5d95/widgetsnbextension-4.0.15.tar.gz", hash = "sha256:de8610639996f1567952d763a5a41af8af37f2575a41f9852a38f947eb82a3b9", size = 1097402, upload-time = "2025-11-01T21:15:55.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl", hash = "sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366", size = 2196503, upload-time = "2025-11-01T21:15:53.565Z" }, +] From b74ef84f7b8e4ec247e1a6e9bfb9678f7be38efa Mon Sep 17 00:00:00 2001 From: Michael Booth Date: Sat, 20 Jun 2026 15:42:59 +1000 Subject: [PATCH 3/3] Add remaining notebook policy assets and fixture coverage - add package/module files, scripts, docs, and fixture assets\n- remove legacy pytest_marimo package path\n- include report regression contracts and manual-check artifacts\n\nCo-Authored-By: Oz --- .pre-commit-config.yaml | 18 + LICENSE | 21 + ROADMAP.md | 15 + backlog.md | 12 + docs/RULES.md | 67 +- .../blog/announcing-pytest-notebook-policy.md | 76 + .../jupyter/jupyter_complex_candidate.ipynb | 1768 +++++++++++++++++ .../jupyter/jupyter_complex_good.ipynb | 886 +++++++++ manual_checks/marimo/marimo_complex_good.py | 22 + reports/fixtures-real/generation-summary.json | 63 + .../notebook-policy-nbom.json | 63 + .../notebook-policy-report.md | 68 + .../notebook-policy-nbom.json | 112 ++ .../notebook-policy-report.md | 84 + .../notebook-policy-nbom.json | 55 + .../notebook-policy-report.md | 68 + .../notebook-policy-nbom.json | 91 + .../notebook-policy-report.md | 74 + .../notebook-policy-nbom.json | 71 + .../notebook-policy-report.md | 69 + .../gallery_matrix/notebook-policy-nbom.json | 53 + .../gallery_matrix/notebook-policy-report.md | 66 + .../notebook-policy-nbom.json | 52 + .../notebook-policy-report.md | 65 + .../notebook-policy-nbom.json | 52 + .../notebook-policy-report.md | 65 + scripts/clean_release_artifacts.py | 31 + scripts/create_manual_examples.py | 44 + scripts/refresh_real_fixtures.py | 110 + src/pytest_marimo/plugin.py | 570 ------ src/pytest_marimo/quality.py | 75 - .../__init__.py | 2 +- src/pytest_notebook_policy/plugin.py | 1224 ++++++++++++ src/pytest_notebook_policy/quality.py | 191 +- .../report_templates.py | 146 ++ tests/fixtures/real/SOURCES.txt | 14 +- ...jupyter_connecting_qt_console_v6_4_7.ipynb | 130 ++ .../jupyter_importing_notebooks_v6_4_7.ipynb | 537 +++++ .../real/jupyter_running_code_v6_4_7.ipynb | 886 +++++++++ ...book_assistants_api_overview_42bc4ed.ipynb | 1768 +++++++++++++++++ .../synthetic/bad_fixture_in_notebook.py | 18 + .../synthetic/jupyter_bad_inline_defs.ipynb | 61 + .../jupyter_bad_large_notebook.ipynb | 28 + .../synthetic/jupyter_bad_magic.ipynb | 18 + .../jupyter_bad_non_idempotent.ipynb | 18 + tests/test_notebook_fixtures.py | 16 +- tests/test_plugin_rules.py | 333 +++- tests/test_quality.py | 6 +- tests/test_report_regressions.py | 151 ++ tmp/manual_checks/experiments/.python-version | 1 + tmp/manual_checks/experiments/README.md | 0 tmp/manual_checks/experiments/main.py | 6 + 52 files changed, 9697 insertions(+), 743 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 LICENSE create mode 100644 backlog.md create mode 100644 docs/blog/announcing-pytest-notebook-policy.md create mode 100644 manual_checks/jupyter/jupyter_complex_candidate.ipynb create mode 100644 manual_checks/jupyter/jupyter_complex_good.ipynb create mode 100644 manual_checks/marimo/marimo_complex_good.py create mode 100644 reports/fixtures-real/generation-summary.json create mode 100644 reports/fixtures-real/jupyter/jupyter_connecting_qt_console_v6_4_7/notebook-policy-nbom.json create mode 100644 reports/fixtures-real/jupyter/jupyter_connecting_qt_console_v6_4_7/notebook-policy-report.md create mode 100644 reports/fixtures-real/jupyter/jupyter_importing_notebooks_v6_4_7/notebook-policy-nbom.json create mode 100644 reports/fixtures-real/jupyter/jupyter_importing_notebooks_v6_4_7/notebook-policy-report.md create mode 100644 reports/fixtures-real/jupyter/jupyter_running_code_v6_4_7/notebook-policy-nbom.json create mode 100644 reports/fixtures-real/jupyter/jupyter_running_code_v6_4_7/notebook-policy-report.md create mode 100644 reports/fixtures-real/jupyter/openai_cookbook_assistants_api_overview_42bc4ed/notebook-policy-nbom.json create mode 100644 reports/fixtures-real/jupyter/openai_cookbook_assistants_api_overview_42bc4ed/notebook-policy-report.md create mode 100644 reports/fixtures-real/marimo/gallery_earthquake/notebook-policy-nbom.json create mode 100644 reports/fixtures-real/marimo/gallery_earthquake/notebook-policy-report.md create mode 100644 reports/fixtures-real/marimo/gallery_matrix/notebook-policy-nbom.json create mode 100644 reports/fixtures-real/marimo/gallery_matrix/notebook-policy-report.md create mode 100644 reports/fixtures-real/marimo/marimo_examples_ui_file/notebook-policy-nbom.json create mode 100644 reports/fixtures-real/marimo/marimo_examples_ui_file/notebook-policy-report.md create mode 100644 reports/fixtures-real/marimo/marimo_smoke_cross_cell_md/notebook-policy-nbom.json create mode 100644 reports/fixtures-real/marimo/marimo_smoke_cross_cell_md/notebook-policy-report.md create mode 100644 scripts/clean_release_artifacts.py create mode 100644 scripts/create_manual_examples.py create mode 100644 scripts/refresh_real_fixtures.py delete mode 100644 src/pytest_marimo/plugin.py delete mode 100644 src/pytest_marimo/quality.py rename src/{pytest_marimo => pytest_notebook_policy}/__init__.py (67%) create mode 100644 src/pytest_notebook_policy/plugin.py create mode 100644 src/pytest_notebook_policy/report_templates.py create mode 100644 tests/fixtures/real/jupyter_connecting_qt_console_v6_4_7.ipynb create mode 100644 tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb create mode 100644 tests/fixtures/real/jupyter_running_code_v6_4_7.ipynb create mode 100644 tests/fixtures/real/openai_cookbook_assistants_api_overview_42bc4ed.ipynb create mode 100644 tests/fixtures/synthetic/bad_fixture_in_notebook.py create mode 100644 tests/fixtures/synthetic/jupyter_bad_inline_defs.ipynb create mode 100644 tests/fixtures/synthetic/jupyter_bad_large_notebook.ipynb create mode 100644 tests/fixtures/synthetic/jupyter_bad_magic.ipynb create mode 100644 tests/fixtures/synthetic/jupyter_bad_non_idempotent.ipynb create mode 100644 tests/test_report_regressions.py create mode 100644 tmp/manual_checks/experiments/.python-version create mode 100644 tmp/manual_checks/experiments/README.md create mode 100644 tmp/manual_checks/experiments/main.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a0f287a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +repos: + - repo: local + hooks: + - id: ruff-check + name: ruff-check + entry: uv run ruff check src tests + language: system + pass_filenames: false + - id: pytest-tests + name: pytest-tests + entry: uv run python -m pytest tests + language: system + pass_filenames: false + - id: pytest-report-regressions + name: pytest-report-regressions + entry: uv run python -m pytest tests/test_quality.py tests/test_report_regressions.py + language: system + pass_filenames: false diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..671f964 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Michael Booth + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ROADMAP.md b/ROADMAP.md index ec6e745..c55cb6c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -13,6 +13,16 @@ This document captures likely next steps for `pytest-notebook-policy` after the - data source hints - Improve docs for report interpretation and policy tuning workflows. +## Immediate easy wins (prioritised) +1. Improve docs for report interpretation and policy tuning workflows. + - Why first: highest user impact for lowest implementation effort. +2. Tighten pre-commit and CI quality gates around report regressions. + - Why second: mostly workflow configuration with fast confidence gains. +3. Expand example notebooks and fixture coverage for real-world edge cases. + - Why third: incremental additions that improve trust and regression safety. +4. Finalise cleaner multi-notebook report summaries. + - Why fourth: contained UX improvement with clear value for teams scanning many notebooks. + ## Dependency and supply-chain visibility - Add opt-in dependency enrichment for report output: - import-to-package mapping @@ -32,6 +42,11 @@ This document captures likely next steps for `pytest-notebook-policy` after the - Add deeper static analysis patterns while remaining deterministic and fast. - Improve rule-level configuration granularity and report drill-down detail. - Keep optional advisory features non-gating by default. +- Add a local LLM Lite notebook-style assessor (optional advisory mode): + - runs locally for style/narrative/cohesion recommendations + - never auto-applies changes + - non-gating by default, designed as guidance/coaching + - complements deterministic rules with human-readable improvement suggestions ## Release and ecosystem readiness - Tighten pre-commit and CI quality gates around report regressions. diff --git a/backlog.md b/backlog.md new file mode 100644 index 0000000..09aa41e --- /dev/null +++ b/backlog.md @@ -0,0 +1,12 @@ +# Backlog +## Candidate enhancements +### Optional LLM-powered advisory reporting (non-gating) +- Status: Backlog +- Goal: add an optional advisory layer that explains notebook policy findings in more depth (what, why, and suggested remediation), without changing deterministic pass/fail behaviour. +- Scope: + - add markdown report output with enriched guidance per finding + - keep deterministic rules as the only enforcement path in CI + - allow opt-in local execution only (no external dependency by default) +- Notes: + - this should remain optional and disabled by default + - explanations should be treated as guidance, not rule truth diff --git a/docs/RULES.md b/docs/RULES.md index 479d107..d0fbe4b 100644 --- a/docs/RULES.md +++ b/docs/RULES.md @@ -1,62 +1,127 @@ # Rule reference -This page documents each `pytest-notebook-policy` rule with concise intent and practical remediation guidance. +This page documents each implemented `pytest-notebook-policy` rule with intent, scope, and practical remediation guidance. + +## Rule families and applicability +`pytest-notebook-policy` has two rule families: + +- `M` rules: module-level behavioural rules for Python notebook modules (`.py`), including marimo notebooks and paired Jupyter Python sources. +- `J` rules: Jupyter-structure rules for Jupyter notebooks (`.ipynb`) and, where configured, their paired Python sources. + +Practical interpretation: + +- For marimo-first projects, `M` rules are usually the core baseline. +- For Jupyter-first projects, use both `M` and `J` rules. +- `J010` only matters when your team intentionally uses paired `.ipynb` + `.py` workflows. + +## Proportionate adoption profiles (all optional) +All rules are optional. Teams can adopt them in stages based on context and risk. + +### Profile 1: PoC / exploratory +Focus on rapid iteration with basic guardrails. + +- Suggested selection: `M001`, `M003`, `M006`, `J011` +- Typical posture: guidance-heavy, low blocking, faster feedback loops + +### Profile 2: MVP / delivery hardening +Add maintainability and reproducibility checks as usage broadens. + +- Suggested selection: `M001-M006`, `J001`, `J002`, `J011`, `J012`, `J013` +- Optional: enable `J010` if your team relies on paired sources + +### Profile 3: Production / operational +Treat notebooks as operational software assets with stronger controls. + +- Suggested selection: all implemented `M` and `J` rules +- Typical posture: tighter thresholds for size/complexity rules and explicit policy exceptions when needed + +This staged model reflects real notebook lifecycles where work can move quickly from PoC to MVP to production. ## `M001` Prefer reactive dependencies over `on_change` +Applies to: marimo and Python notebook modules. Why: callback-based state propagation can become hard to reason about in reactive notebooks. How to fix: model dependencies explicitly so downstream cells derive values from inputs/state instead of callback side effects. ## `M002` Keep test cells focused +Applies to: marimo and Python notebook modules. Why: mixing assertions with setup/helper logic makes failures less clear. How to fix: keep assertion logic in dedicated test cells and move setup/helpers into separate cells or modules. ## `M003` Avoid mutable module-level state +Applies to: marimo and Python notebook modules. Why: mutable globals introduce hidden coupling and order-dependent behaviour. How to fix: keep mutable state local to functions/cells and pass values explicitly. ## `M004` Keep fixtures out of notebook modules +Applies to: marimo and Python notebook modules. Why: notebook-local fixtures are harder to collect and maintain consistently. How to fix: define fixtures in `conftest.py` or helper modules and import them. ## `M005` Avoid cross-cell mutation of shared objects +Applies to: marimo and Python notebook modules. Why: mutating shared objects across cells can make re-runs inconsistent. How to fix: prefer immutable transformations or return new values/objects. ## `M006` Avoid non-idempotent calls in cells +Applies to: marimo and Python notebook modules. Why: non-deterministic calls make repeated execution produce different outcomes. How to fix: add explicit seeds/inputs, isolate non-deterministic operations, or move them behind clear interfaces. ## `J001` Avoid magics and shell escapes +Applies to: Jupyter notebooks (`.ipynb`) and Jupyter paired Python sources. Why: magics and shell escapes can reduce portability and reproducibility. How to fix: replace with explicit Python equivalents where practical. ## `J002` Avoid non-idempotent calls in Jupyter code cells +Applies to: Jupyter notebooks (`.ipynb`) and Jupyter paired Python sources. Why: Jupyter cells with non-deterministic calls can produce unstable notebook behaviour. How to fix: seed/random-control where needed and isolate side-effecting operations. ## `J010` Keep paired `.ipynb` and `.py` sources synchronised +Applies to: paired-source Jupyter workflows only. Why: drift between paired sources creates review and maintenance confusion. How to fix: update paired files together or disable `J010` if pairing is not required. ## `J011` Require a top-of-notebook parameter/configuration cell +Applies to: Jupyter notebooks (`.ipynb`) and Jupyter paired Python sources. Why: an explicit parameter cell improves reproducibility and automation. How to fix: include a configuration/parameter code cell within the first few code cells. ## `J012` Keep notebooks and cells reviewable +Applies to: Jupyter notebooks (`.ipynb`) and Jupyter paired Python sources. Why: very large notebooks or long cells are harder to review and maintain. How to fix: split notebooks and/or large cells and move heavy logic into modules. ## `J013` Limit inline definitions in notebooks +Applies to: Jupyter notebooks (`.ipynb`) and Jupyter paired Python sources. Why: excessive inline function/class definitions blur narrative and reusable logic boundaries. How to fix: extract reusable functions/classes into modules and keep notebooks focused on orchestration. + +## Candidate future rules (not implemented yet) +The following candidates are intentionally listed as optional and proportionate extensions: + +- `J020` Notebook environment contract: + require an explicit runtime/environment declaration cell (Python version, key packages, execution assumptions). +- `J021` Data input contract: + require a clear declaration of key data inputs and expected schema/shape at notebook start. +- `J022` Side-effect boundary: + flag notebooks that perform external writes/calls without a clearly marked side-effect section. +- `J023` Output noise budget: + flag notebooks with very large persisted outputs that reduce reviewability. +- `M020` Secret handling guard: + flag obvious inline secrets/tokens in notebook modules. +- `M021` Testability extraction threshold: + nudge extraction to modules once inline complexity exceeds a configured threshold. + +Recommended path: adopt candidate rules first as advisory guidance in PoC/MVP contexts, then promote selected rules to stricter enforcement where operational risk justifies it. diff --git a/docs/blog/announcing-pytest-notebook-policy.md b/docs/blog/announcing-pytest-notebook-policy.md new file mode 100644 index 0000000..b4bb1a7 --- /dev/null +++ b/docs/blog/announcing-pytest-notebook-policy.md @@ -0,0 +1,76 @@ +# Announcing `pytest-notebook-policy`: enforcement-lite guardrails for outstanding notebook development +Notebooks are one of the fastest ways to explore ideas, build data workflows, and communicate technical thinking. They are also one of the easiest places for hidden state, noisy diffs, and accidental fragility to creep in. + +That is why I built `pytest-notebook-policy`: a lightweight policy and quality package for notebook-centric teams that want to move quickly **and** keep quality high. + +## Why this package exists +The goal is simple: preserve the agility that makes notebooks powerful, while reducing the risks that usually appear as notebook projects scale. + +`pytest-notebook-policy` supports this with practical checks and reporting that help teams: + +- keep notebook behaviour more reproducible and reviewable; +- detect risky patterns early; +- generate readable quality reports for humans and machine-friendly outputs for automation. + +## Fast with confidence, not fast and fragile +This package is designed so policy does not become a drag on delivery. +In the same way well-configured Ruff linting rules, enforced via pre-commit hooks, enable rapid high-quality code delivery, this package is the notebook equivalent: it guides and coaches users towards better outcomes while preserving notebook velocity. + +The intent is to help you **move faster confidently**, not slow you down: + +- quick feedback where you work already (pytest and CI); +- guidance-oriented findings that explain what to fix and why; +- enforcement-lite defaults so teams can phase in strictness as needed. + +When paired with notebook-to-script synchronisation workflows, this further enhances the agility play: + +- notebooks stay expressive for exploration and communication; +- synchronised scripts stay clean for diffing, testing, and maintenance; +- policy checks provide confidence that speed is not compromising quality. + +## What “enforcement-lite” means +Enforcement-lite is not “anything goes”. It is a practical middle path: + +1. Use rules as guardrails, not punishment. +2. Prioritise explainable findings over opaque pass/fail gates. +3. Start with visibility, then tighten only where needed. +4. Keep policy outcomes review-friendly and actionable. + +This approach gives teams a clearer path from exploratory notebooks to dependable production workflows. + +## Appendix: reflections and linkages to best-practice guidance +The following reflection connects directly to the thinking behind this package and guidance approach, with one explicit source of inspiration: + +- Joe Riad, *I Use Free Software to Build All-Purpose Notebooks* (Level Up Coding, May 2026): + https://levelup.gitconnected.com/i-use-free-software-to-build-all-purpose-notebooks-eb49ce3d86dc + +### Reflections +I agree with core themes in that article: reproducibility matters, diff quality matters, and notebook workflows need intentional execution models. + +Where I intentionally diverge is on mixed-language notebook practice in production contexts. + +Polyglot can be valuable in constrained research cases, but as a default it often increases long-term maintenance cost: + +- more runtime and dependency surfaces; +- more packaging and operational complexity; +- harder debugging and onboarding; +- greater risk of subtle integration failures. + +My default recommendation is: + +- **monoglot by default** for production notebook workflows; +- **polyglot by explicit exception**, with strong justification and clear ownership. + +In many real-world cases, modern LLM-assisted translation can flatten a polyglot design into a monoglot architecture, provided the translated outcome is validated with robust tests. + +The outcome is usually a better balance of agility and reliability. + +### Practical contract for notebook teams +This is the contract I optimise for: + +- reproducibility over convenience; +- reviewability over cleverness; +- operational simplicity over tooling sprawl; +- explicit, documented exceptions when trade-offs are necessary. + +That is the core philosophy behind `pytest-notebook-policy`: maintain notebook velocity, increase confidence, and keep quality guardrails lightweight but real. diff --git a/manual_checks/jupyter/jupyter_complex_candidate.ipynb b/manual_checks/jupyter/jupyter_complex_candidate.ipynb new file mode 100644 index 0000000..dc3a74a --- /dev/null +++ b/manual_checks/jupyter/jupyter_complex_candidate.ipynb @@ -0,0 +1,1768 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Assistants API Overview (Python SDK)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The new [Assistants API](https://platform.openai.com/docs/assistants/overview) is a stateful evolution of our [Chat Completions API](https://platform.openai.com/docs/guides/text-generation/chat-completions-api) meant to simplify the creation of assistant-like experiences, and enable developer access to powerful tools like Code Interpreter and File Search." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Assistants API Diagram](../images/assistants_overview_diagram.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Chat Completions API vs Assistants API\n", + "\n", + "The primitives of the **Chat Completions API** are `Messages`, on which you perform a `Completion` with a `Model` (`gpt-4o`, `gpt-4o-mini`, etc). It is lightweight and powerful, but inherently stateless, which means you have to manage conversation state, tool definitions, retrieval documents, and code execution manually.\n", + "\n", + "The primitives of the **Assistants API** are\n", + "\n", + "- `Assistants`, which encapsulate a base model, instructions, tools, and (context) documents,\n", + "- `Threads`, which represent the state of a conversation, and\n", + "- `Runs`, which power the execution of an `Assistant` on a `Thread`, including textual responses and multi-step tool use.\n", + "\n", + "We'll take a look at how these can be used to create powerful, stateful experiences.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "### Python SDK\n", + "\n", + "> **Note**\n", + "> We've updated our [Python SDK](https://github.com/openai/openai-python) to add support for the Assistants API, so you'll need to update it to the latest version (`1.59.4` at time of writing).\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: openai in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (1.59.4)\n", + "Requirement already satisfied: anyio<5,>=3.5.0 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (3.7.1)\n", + "Requirement already satisfied: distro<2,>=1.7.0 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (1.9.0)\n", + "Requirement already satisfied: httpx<1,>=0.23.0 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (0.27.0)\n", + "Requirement already satisfied: jiter<1,>=0.4.0 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (0.7.0)\n", + "Requirement already satisfied: pydantic<3,>=1.9.0 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (2.8.2)\n", + "Requirement already satisfied: sniffio in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (1.3.1)\n", + "Requirement already satisfied: tqdm>4 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (4.66.4)\n", + "Requirement already satisfied: typing-extensions<5,>=4.11 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (4.12.2)\n", + "Requirement already satisfied: idna>=2.8 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from anyio<5,>=3.5.0->openai) (3.7)\n", + "Requirement already satisfied: certifi in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from httpx<1,>=0.23.0->openai) (2024.7.4)\n", + "Requirement already satisfied: httpcore==1.* in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from httpx<1,>=0.23.0->openai) (1.0.5)\n", + "Requirement already satisfied: h11<0.15,>=0.13 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from httpcore==1.*->httpx<1,>=0.23.0->openai) (0.14.0)\n", + "Requirement already satisfied: annotated-types>=0.4.0 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from pydantic<3,>=1.9.0->openai) (0.7.0)\n", + "Requirement already satisfied: pydantic-core==2.20.1 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from pydantic<3,>=1.9.0->openai) (2.20.1)\n" + ] + } + ], + "source": [ + "!pip install --upgrade openai" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And make sure it's up to date by running:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Version: 1.59.4\n" + ] + } + ], + "source": [ + "!pip show openai | grep Version" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Pretty Printing Helper\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "\n", + "def show_json(obj):\n", + " display(json.loads(obj.model_dump_json()))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Complete Example with Assistants API\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Assistants\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The easiest way to get started with the Assistants API is through the [Assistants Playground](https://platform.openai.com/playground).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Assistants Playground](../images/assistants_overview_assistants_playground.png)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's begin by creating an assistant! We'll create a Math Tutor just like in our [docs](https://platform.openai.com/docs/assistants/overview).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Creating New Assistant](../images/assistants_overview_new_assistant.png)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also create Assistants directly through the Assistants API, like so:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',\n", + " 'created_at': 1736340398,\n", + " 'description': None,\n", + " 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',\n", + " 'metadata': {},\n", + " 'model': 'gpt-4o',\n", + " 'name': 'Math Tutor',\n", + " 'object': 'assistant',\n", + " 'tools': [],\n", + " 'response_format': 'auto',\n", + " 'temperature': 1.0,\n", + " 'tool_resources': {'code_interpreter': None, 'file_search': None},\n", + " 'top_p': 1.0}", + " 'tools': [],\n", + " 'response_format': 'auto',\n", + " 'temperature': 1.0,\n", + " 'tool_resources': {'code_interpreter': None, 'file_search': None},\n", + " 'top_p': 1.0}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from openai import OpenAI\n", + "import os\n", + "\n", + "client = OpenAI(api_key=os.environ.get(\"OPENAI_API_KEY\", \"\"))\n", + "\n", + "\n", + "assistant = client.beta.assistants.create(\n", + " name=\"Math Tutor\",\n", + " instructions=\"You are a personal math tutor. Answer questions briefly, in a sentence or less.\",\n", + " model=\"gpt-4o\",\n", + ")\n", + "show_json(assistant)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Regardless of whether you create your Assistant through the Dashboard or with the API, you'll want to keep track of the Assistant ID. This is how you'll refer to your Assistant throughout Threads and Runs.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we'll create a new Thread and add a Message to it. This will hold the state of our conversation, so we don't have re-send the entire message history each time.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Threads\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a new thread:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': 'thread_j4dc1TiHPfkviKUHNi4aAsA6',\n", + " 'created_at': 1736340398,\n", + " 'metadata': {},\n", + " 'object': 'thread',\n", + " 'tool_resources': {'code_interpreter': None, 'file_search': None}}", + " 'object': 'thread',\n", + " 'tool_resources': {'code_interpreter': None, 'file_search': None}}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "thread = client.beta.threads.create()\n", + "show_json(thread)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then add the Message to the thread:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': 'msg_1q4Y7ZZ9gIcPoAKSx9UtrrKJ',\n", + " 'assistant_id': None,\n", + " 'attachments': [],\n", + " 'completed_at': None,\n", + " 'attachments': [],\n", + " 'completed_at': None,\n", + " 'content': [{'text': {'annotations': [],\n", + " 'value': 'I need to solve the equation `3x + 11 = 14`. Can you help me?'},\n", + " 'type': 'text'}],\n", + " 'created_at': 1736340400,\n", + " 'incomplete_at': None,\n", + " 'incomplete_details': None,\n", + " 'metadata': {},\n", + " 'object': 'thread.message',\n", + " 'role': 'user',\n", + " 'run_id': None,\n", + " 'status': None,\n", + " 'thread_id': 'thread_j4dc1TiHPfkviKUHNi4aAsA6'}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "message = client.beta.threads.messages.create(\n", + " thread_id=thread.id,\n", + " role=\"user\",\n", + " content=\"I need to solve the equation `3x + 11 = 14`. Can you help me?\",\n", + ")\n", + "show_json(message)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> **Note**\n", + "> Even though you're no longer sending the entire history each time, you will still be charged for the tokens of the entire conversation history with each Run.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Runs\n", + "\n", + "Notice how the Thread we created is **not** associated with the Assistant we created earlier! Threads exist independently from Assistants, which may be different from what you'd expect if you've used ChatGPT (where a thread is tied to a model/GPT).\n", + "\n", + "To get a completion from an Assistant for a given Thread, we must create a Run. Creating a Run will indicate to an Assistant it should look at the messages in the Thread and take action: either by adding a single response, or using tools.\n", + "\n", + "> **Note**\n", + "> Runs are a key difference between the Assistants API and Chat Completions API. While in Chat Completions the model will only ever respond with a single message, in the Assistants API a Run may result in an Assistant using one or multiple tools, and potentially adding multiple messages to the Thread.\n", + "\n", + "To get our Assistant to respond to the user, let's create the Run. As mentioned earlier, you must specify _both_ the Assistant and the Thread.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': 'run_qVYsWok6OCjHxkajpIrdHuVP',\n", + " 'assistant_id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',\n", + " 'cancelled_at': None,\n", + " 'completed_at': None,\n", + " 'created_at': 1736340403,\n", + " 'expires_at': 1736341003,\n", + " 'failed_at': None,\n", + " 'incomplete_details': None,\n", + " 'incomplete_details': None,\n", + " 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',\n", + " 'last_error': None,\n", + " 'max_completion_tokens': None,\n", + " 'max_prompt_tokens': None,\n", + " 'max_completion_tokens': None,\n", + " 'max_prompt_tokens': None,\n", + " 'metadata': {},\n", + " 'model': 'gpt-4o',\n", + " 'object': 'thread.run',\n", + " 'parallel_tool_calls': True,\n", + " 'parallel_tool_calls': True,\n", + " 'required_action': None,\n", + " 'response_format': 'auto',\n", + " 'response_format': 'auto',\n", + " 'started_at': None,\n", + " 'status': 'queued',\n", + " 'thread_id': 'thread_j4dc1TiHPfkviKUHNi4aAsA6',\n", + " 'tool_choice': 'auto',\n", + " 'tools': [],\n", + " 'truncation_strategy': {'type': 'auto', 'last_messages': None},\n", + " 'usage': None,\n", + " 'temperature': 1.0,\n", + " 'top_p': 1.0,\n", + " 'tool_resources': {}}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "run = client.beta.threads.runs.create(\n", + " thread_id=thread.id,\n", + " assistant_id=assistant.id,\n", + ")\n", + "show_json(run)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Unlike creating a completion in the Chat Completions API, **creating a Run is an asynchronous operation**. It will return immediately with the Run's metadata, which includes a `status` that will initially be set to `queued`. The `status` will be updated as the Assistant performs operations (like using tools and adding messages).\n", + "\n", + "To know when the Assistant has completed processing, we can poll the Run in a loop. (Support for streaming is coming soon!) While here we are only checking for a `queued` or `in_progress` status, in practice a Run may undergo a [variety of status changes](https://platform.openai.com/docs/api-reference/runs/object#runs/object-status) which you can choose to surface to the user. (These are called Steps, and will be covered later.)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "\n", + "def wait_on_run(run, thread):\n", + " while run.status == \"queued\" or run.status == \"in_progress\":\n", + " run = client.beta.threads.runs.retrieve(\n", + " thread_id=thread.id,\n", + " run_id=run.id,\n", + " )\n", + " time.sleep(0.5)\n", + " return run" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': 'run_qVYsWok6OCjHxkajpIrdHuVP',\n", + " 'assistant_id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',\n", + " 'cancelled_at': None,\n", + " 'completed_at': 1736340406,\n", + " 'created_at': 1736340403,\n", + " 'expires_at': None,\n", + " 'failed_at': None,\n", + " 'incomplete_details': None,\n", + " 'incomplete_details': None,\n", + " 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',\n", + " 'last_error': None,\n", + " 'max_completion_tokens': None,\n", + " 'max_prompt_tokens': None,\n", + " 'max_completion_tokens': None,\n", + " 'max_prompt_tokens': None,\n", + " 'metadata': {},\n", + " 'model': 'gpt-4o',\n", + " 'object': 'thread.run',\n", + " 'parallel_tool_calls': True,\n", + " 'parallel_tool_calls': True,\n", + " 'required_action': None,\n", + " 'response_format': 'auto',\n", + " 'started_at': 1736340405,\n", + " 'status': 'completed',\n", + " 'thread_id': 'thread_j4dc1TiHPfkviKUHNi4aAsA6',\n", + " 'tool_choice': 'auto',\n", + " 'tools': [],\n", + " 'truncation_strategy': {'type': 'auto', 'last_messages': None},\n", + " 'usage': {'completion_tokens': 35,\n", + " 'prompt_tokens': 66,\n", + " 'total_tokens': 101,\n", + " 'prompt_token_details': {'cached_tokens': 0},\n", + " 'completion_tokens_details': {'reasoning_tokens': 0}},\n", + " 'temperature': 1.0,\n", + " 'top_p': 1.0,\n", + " 'tool_resources': {}}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "run = wait_on_run(run, thread)\n", + "show_json(run)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Messages\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that the Run has completed, we can list the Messages in the Thread to see what got added by the Assistant.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'data': [{'id': 'msg_A5eAN6ZAJDmFBOYutEm5DFCy',\n", + " 'assistant_id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',\n", + " 'attachments': [],\n", + " 'completed_at': None,\n", + " 'content': [{'text': {'annotations': [],\n", + " 'value': 'Sure! Subtract 11 from both sides to get \\\\(3x = 3\\\\), then divide by 3 to find \\\\(x = 1\\\\).'},\n", + " 'type': 'text'}],\n", + " 'created_at': 1736340405,\n", + " 'incomplete_at': None,\n", + " 'incomplete_details': None,\n", + " 'metadata': {},\n", + " 'object': 'thread.message',\n", + " 'role': 'assistant',\n", + " 'run_id': 'run_qVYsWok6OCjHxkajpIrdHuVP',\n", + " 'status': None,\n", + " 'thread_id': 'thread_j4dc1TiHPfkviKUHNi4aAsA6'},\n", + " {'id': 'msg_1q4Y7ZZ9gIcPoAKSx9UtrrKJ',\n", + " 'assistant_id': None,\n", + " 'attachments': [],\n", + " 'completed_at': None,\n", + " 'attachments': [],\n", + " 'completed_at': None,\n", + " 'content': [{'text': {'annotations': [],\n", + " 'value': 'I need to solve the equation `3x + 11 = 14`. Can you help me?'},\n", + " 'type': 'text'}],\n", + " 'created_at': 1736340400,\n", + " 'incomplete_at': None,\n", + " 'incomplete_details': None,\n", + " 'metadata': {},\n", + " 'object': 'thread.message',\n", + " 'role': 'user',\n", + " 'run_id': None,\n", + " 'status': None,\n", + " 'thread_id': 'thread_j4dc1TiHPfkviKUHNi4aAsA6'}],\n", + " 'object': 'list',\n", + " 'first_id': 'msg_A5eAN6ZAJDmFBOYutEm5DFCy',\n", + " 'last_id': 'msg_1q4Y7ZZ9gIcPoAKSx9UtrrKJ',\n", + " 'has_more': False}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "messages = client.beta.threads.messages.list(thread_id=thread.id)\n", + "show_json(messages)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see, Messages are ordered in reverse-chronological order – this was done so the most recent results are always on the first `page` (since results can be paginated). Do keep a look out for this, since this is the opposite order to messages in the Chat Completions API.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's ask our Assistant to explain the result a bit further!\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'data': [{'id': 'msg_wSHHvaMnaWktZWsKs6gyoPUB',\n", + " 'assistant_id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',\n", + " 'attachments': [],\n", + " 'completed_at': None,\n", + " 'content': [{'text': {'annotations': [],\n", + " 'value': 'Certainly! To isolate \\\\(x\\\\), first subtract 11 from both sides of the equation \\\\(3x + 11 = 14\\\\), resulting in \\\\(3x = 3\\\\). Then, divide both sides by 3 to solve for \\\\(x\\\\), giving you \\\\(x = 1\\\\).'},\n", + " 'type': 'text'}],\n", + " 'created_at': 1736340414,\n", + " 'incomplete_at': None,\n", + " 'incomplete_details': None,\n", + " 'metadata': {},\n", + " 'object': 'thread.message',\n", + " 'role': 'assistant',\n", + " 'run_id': 'run_lJsumsDtPTmdG3Enx2CfYrrq',\n", + " 'status': None,\n", + " 'thread_id': 'thread_j4dc1TiHPfkviKUHNi4aAsA6'}],\n", + " 'object': 'list',\n", + " 'first_id': 'msg_wSHHvaMnaWktZWsKs6gyoPUB',\n", + " 'last_id': 'msg_wSHHvaMnaWktZWsKs6gyoPUB',\n", + " 'has_more': False}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Create a message to append to our thread\n", + "message = client.beta.threads.messages.create(\n", + " thread_id=thread.id, role=\"user\", content=\"Could you explain this to me?\"\n", + ")\n", + "\n", + "# Execute our run\n", + "run = client.beta.threads.runs.create(\n", + " thread_id=thread.id,\n", + " assistant_id=assistant.id,\n", + ")\n", + "\n", + "# Wait for completion\n", + "wait_on_run(run, thread)\n", + "\n", + "# Retrieve all the messages added after our last user message\n", + "messages = client.beta.threads.messages.list(\n", + " thread_id=thread.id, order=\"asc\", after=message.id\n", + ")\n", + "show_json(messages)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This may feel like a lot of steps to get a response back, especially for this simple example. However, you'll soon see how we can add very powerful functionality to our Assistant without changing much code at all!\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's take a look at how we could potentially put all of this together. Below is all the code you need to use an Assistant you've created.\n", + "\n", + "Since we've already created our Math Assistant, I've saved its ID in `MATH_ASSISTANT_ID`. I then defined two functions:\n", + "\n", + "- `submit_message`: create a Message on a Thread, then start (and return) a new Run\n", + "- `get_response`: returns the list of Messages in a Thread\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "from openai import OpenAI\n", + "\n", + "MATH_ASSISTANT_ID = assistant.id # or a hard-coded ID like \"asst-...\"\n", + "\n", + "client = OpenAI(api_key=os.environ.get(\"OPENAI_API_KEY\", \"\"))\n", + "\n", + "def submit_message(assistant_id, thread, user_message):\n", + " client.beta.threads.messages.create(\n", + " thread_id=thread.id, role=\"user\", content=user_message\n", + " )\n", + " return client.beta.threads.runs.create(\n", + " thread_id=thread.id,\n", + " assistant_id=assistant_id,\n", + " )\n", + "\n", + "\n", + "def get_response(thread):\n", + " return client.beta.threads.messages.list(thread_id=thread.id, order=\"asc\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "I've also defined a `create_thread_and_run` function that I can re-use (which is actually almost identical to the [`client.beta.threads.create_and_run`](https://platform.openai.com/docs/api-reference/runs/createThreadAndRun) compound function in our API ;) ). Finally, we can submit our mock user requests each to a new Thread.\n", + "\n", + "Notice how all of these API calls are asynchronous operations; this means we actually get async behavior in our code without the use of async libraries! (e.g. `asyncio`)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "def create_thread_and_run(user_input):\n", + " thread = client.beta.threads.create()\n", + " run = submit_message(MATH_ASSISTANT_ID, thread, user_input)\n", + " return thread, run\n", + "\n", + "\n", + "# Emulating concurrent user requests\n", + "thread1, run1 = create_thread_and_run(\n", + " \"I need to solve the equation `3x + 11 = 14`. Can you help me?\"\n", + ")\n", + "thread2, run2 = create_thread_and_run(\"Could you explain linear algebra to me?\")\n", + "thread3, run3 = create_thread_and_run(\"I don't like math. What can I do?\")\n", + "\n", + "# Now all Runs are executing..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once all Runs are going, we can wait on each and get the responses.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "# Messages\n", + "user: I need to solve the equation `3x + 11 = 14`. Can you help me?\n", + "assistant: Sure! Subtract 11 from both sides to get \\(3x = 3\\), then divide by 3 to find \\(x = 1\\).\n", + "\n", + "# Messages\n", + "user: Could you explain linear algebra to me?\n", + "assistant: Linear algebra is the branch of mathematics concerning vector spaces, linear transformations, and systems of linear equations, often represented with matrices.\n", + "\n", + "# Messages\n", + "user: I don't like math. What can I do?\n", + "assistant: Try relating math to real-life interests or hobbies, practice with fun games or apps, and gradually build confidence with easier problems.\n", + "\n", + "# Messages\n", + "user: I don't like math. What can I do?\n", + "assistant: Try relating math to real-life interests or hobbies, practice with fun games or apps, and gradually build confidence with easier problems.\n", + "user: Thank you!\n", + "assistant: You're welcome! If you have any more questions, feel free to ask!\n", + "\n" + ] + } + ], + "source": [ + "import time\n", + "\n", + "# Pretty printing helper\n", + "def pretty_print(messages):\n", + " print(\"# Messages\")\n", + " for m in messages:\n", + " print(f\"{m.role}: {m.content[0].text.value}\")\n", + " print()\n", + "\n", + "\n", + "# Waiting in a loop\n", + "def wait_on_run(run, thread):\n", + " while run.status == \"queued\" or run.status == \"in_progress\":\n", + " run = client.beta.threads.runs.retrieve(\n", + " thread_id=thread.id,\n", + " run_id=run.id,\n", + " )\n", + " time.sleep(0.5)\n", + " return run\n", + "\n", + "\n", + "# Wait for Run 1\n", + "run1 = wait_on_run(run1, thread1)\n", + "pretty_print(get_response(thread1))\n", + "\n", + "# Wait for Run 2\n", + "run2 = wait_on_run(run2, thread2)\n", + "pretty_print(get_response(thread2))\n", + "\n", + "# Wait for Run 3\n", + "run3 = wait_on_run(run3, thread3)\n", + "pretty_print(get_response(thread3))\n", + "\n", + "# Thank our assistant on Thread 3 :)\n", + "run4 = submit_message(MATH_ASSISTANT_ID, thread3, \"Thank you!\")\n", + "run4 = wait_on_run(run4, thread3)\n", + "pretty_print(get_response(thread3))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Et voilà!\n", + "\n", + "You may have noticed that this code is not actually specific to our math Assistant at all... this code will work for any new Assistant you create simply by changing the Assistant ID! That is the power of the Assistants API.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Tools\n", + "\n", + "A key feature of the Assistants API is the ability to equip our Assistants with Tools, like Code Interpreter, File Search, and custom Functions. Let's take a look at each.\n", + "\n", + "### Code Interpreter\n", + "\n", + "Let's equip our Math Tutor with the [Code Interpreter](https://platform.openai.com/docs/assistants/tools/code-interpreter) tool, which we can do from the Dashboard...\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Enabling code interpreter](../images/assistants_overview_enable_code_interpreter.png)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "...or the API, using the Assistant ID.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',\n", + " 'created_at': 1736340398,\n", + " 'description': None,\n", + " 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',\n", + " 'metadata': {},\n", + " 'model': 'gpt-4o',\n", + " 'name': 'Math Tutor',\n", + " 'object': 'assistant',\n", + " 'tools': [{'type': 'code_interpreter'}],\n", + " 'response_format': 'auto',\n", + " 'temperature': 1.0,\n", + " 'tool_resources': {'code_interpreter': {'file_ids': []}, 'file_search': None},\n", + " 'top_p': 1.0}", + " 'tools': [{'type': 'code_interpreter'}],\n", + " 'response_format': 'auto',\n", + " 'temperature': 1.0,\n", + " 'tool_resources': {'code_interpreter': {'file_ids': []}, 'file_search': None},\n", + " 'top_p': 1.0}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "assistant = client.beta.assistants.update(\n", + " MATH_ASSISTANT_ID,\n", + " tools=[{\"type\": \"code_interpreter\"}],\n", + ")\n", + "show_json(assistant)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's ask the Assistant to use its new tool.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "# Messages\n", + "user: Generate the first 20 fibbonaci numbers with code.\n", + "assistant: The first 20 Fibonacci numbers are: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181.\n", + "\n" + ] + } + ], + "source": [ + "thread, run = create_thread_and_run(\n", + " \"Generate the first 20 fibbonaci numbers with code.\"\n", + ")\n", + "run = wait_on_run(run, thread)\n", + "pretty_print(get_response(thread))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And that's it! The Assistant used Code Interpreter in the background, and gave us a final response.\n", + "\n", + "For some use cases this may be enough – however, if we want more details on what precisely an Assistant is doing we can take a look at a Run's Steps.\n", + "\n", + "### Steps\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A Run is composed of one or more Steps. Like a Run, each Step has a `status` that you can query. This is useful for surfacing the progress of a Step to a user (e.g. a spinner while the Assistant is writing code or performing retrieval).\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "run_steps = client.beta.threads.runs.steps.list(\n", + " thread_id=thread.id, run_id=run.id, order=\"asc\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's take a look at each Step's `step_details`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'tool_calls': [{'id': 'call_E1EE1loDmcWoc7FpkOMKYj6n',\n", + " 'code_interpreter': {'input': 'def generate_fibonacci(n):\\n fib_sequence = [0, 1]\\n while len(fib_sequence) < n:\\n next_value = fib_sequence[-1] + fib_sequence[-2]\\n fib_sequence.append(next_value)\\n return fib_sequence\\n\\n# Generate the first 20 Fibonacci numbers\\nfirst_20_fibonacci = generate_fibonacci(20)\\nfirst_20_fibonacci',\n", + " 'outputs': []},\n", + " 'type': 'code_interpreter'}],\n", + " 'type': 'tool_calls'}" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "null\n" + ] + }, + { + "data": { + "text/plain": [ + "{'message_creation': {'message_id': 'msg_RzTnbBMmzDYHk79a0x9qM5uU'},\n", + " 'type': 'message_creation'}" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "null\n" + ] + } + ], + "source": [ + "for step in run_steps.data:\n", + " step_details = step.step_details\n", + " print(json.dumps(show_json(step_details), indent=4))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see the `step_details` for two Steps:\n", + "\n", + "1. `tool_calls` (plural, since it could be more than one in a single Step)\n", + "2. `message_creation`\n", + "\n", + "The first Step is a `tool_calls`, specifically using the `code_interpreter` which contains:\n", + "\n", + "- `input`, which was the Python code generated before the tool was called, and\n", + "- `output`, which was the result of running the Code Interpreter.\n", + "\n", + "The second Step is a `message_creation`, which contains the `message` that was added to the Thread to communicate the results to the user.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### File search\n", + "\n", + "Another powerful tool in the Assistants API is [File search](https://platform.openai.com/docs/assistants/tools/file-search). This allows the uploading of files to the Assistant to be used as a knowledge base when answering questions.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Enabling retrieval](../images/assistants_overview_enable_retrieval.png)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "File added to vector store\n" + ] + }, + { + "data": { + "text/plain": [ + "{'id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',\n", + " 'created_at': 1736340398,\n", + " 'description': None,\n", + " 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',\n", + " 'metadata': {},\n", + " 'model': 'gpt-4o',\n", + " 'name': 'Math Tutor',\n", + " 'object': 'assistant',\n", + " 'tools': [{'type': 'code_interpreter'},\n", + " {'type': 'file_search',\n", + " 'file_search': {'max_num_results': None,\n", + " 'ranking_options': {'score_threshold': 0.0,\n", + " 'ranker': 'default_2024_08_21'}}}],\n", + " 'response_format': 'auto',\n", + " 'temperature': 1.0,\n", + " 'tool_resources': {'code_interpreter': {'file_ids': ['file-GQFm2i7N8LrAQatefWKEsE']},\n", + " 'file_search': {'vector_store_ids': ['vs_dEArILZSJh7J799QACi3QhuU']}},\n", + " 'top_p': 1.0}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Upload the file\n", + "file = client.files.create(\n", + " file=open(\n", + " \"data/language_models_are_unsupervised_multitask_learners.pdf\",\n", + " \"rb\",\n", + " ),\n", + " purpose=\"assistants\",\n", + ")\n", + "\n", + "# Create a vector store\n", + "vector_store = client.beta.vector_stores.create(\n", + " name=\"language_models_are_unsupervised_multitask_learners\",\n", + ")\n", + "\n", + "# Add the file to the vector store\n", + "vector_store_file = client.beta.vector_stores.files.create_and_poll(\n", + " vector_store_id=vector_store.id,\n", + " file_id=file.id,\n", + ")\n", + "\n", + "# Confirm the file was added\n", + "while vector_store_file.status == \"in_progress\":\n", + " time.sleep(1)\n", + "if vector_store_file.status == \"completed\":\n", + " print(\"File added to vector store\")\n", + "elif vector_store_file.status == \"failed\":\n", + " raise Exception(\"Failed to add file to vector store\")\n", + "\n", + "# Update Assistant\n", + "assistant = client.beta.assistants.update(\n", + " MATH_ASSISTANT_ID,\n", + " tools=[{\"type\": \"code_interpreter\"}, {\"type\": \"file_search\"}],\n", + " tool_resources={\n", + " \"file_search\":{\n", + " \"vector_store_ids\": [vector_store.id]\n", + " },\n", + " \"code_interpreter\": {\n", + " \"file_ids\": [file.id]\n", + " }\n", + " },\n", + ")\n", + "show_json(assistant)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "# Messages\n", + "user: What are some cool math concepts behind this ML paper pdf? Explain in two sentences.\n", + "assistant: The paper explores the concept of multitask learning where a single model is used to perform various tasks, modeling the conditional distribution \\( p(\\text{output} | \\text{input, task}) \\), inspired by probabilistic approaches【6:10†source】. It also discusses the use of Transformer-based architectures and parallel corpus substitution in language models, enhancing their ability to generalize across domain tasks without explicit task-specific supervision【6:2†source】【6:5†source】.\n", + "\n" + ] + } + ], + "source": [ + "thread, run = create_thread_and_run(\n", + " \"What are some cool math concepts behind this ML paper pdf? Explain in two sentences.\"\n", + ")\n", + "run = wait_on_run(run, thread)\n", + "pretty_print(get_response(thread))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> **Note**\n", + "> There are more intricacies in File Search, like [Annotations](https://platform.openai.com/docs/assistants/how-it-works/managing-threads-and-messages), which may be covered in another cookbook.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "VectorStoreDeleted(id='vs_dEArILZSJh7J799QACi3QhuU', deleted=True, object='vector_store.deleted')" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Delete the vector store\n", + "client.beta.vector_stores.delete(vector_store.id)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Functions\n", + "\n", + "As a final powerful tool for your Assistant, you can specify custom [Functions](https://platform.openai.com/docs/assistants/tools/function-calling) (much like the [Function Calling](https://platform.openai.com/docs/guides/function-calling) in the Chat Completions API). During a Run, the Assistant can then indicate it wants to call one or more functions you specified. You are then responsible for calling the Function, and providing the output back to the Assistant.\n", + "\n", + "Let's take a look at an example by defining a `display_quiz()` Function for our Math Tutor.\n", + "\n", + "This function will take a `title` and an array of `question`s, display the quiz, and get input from the user for each:\n", + "\n", + "- `title`\n", + "- `questions`\n", + " - `question_text`\n", + " - `question_type`: [`MULTIPLE_CHOICE`, `FREE_RESPONSE`]\n", + " - `choices`: [\"choice 1\", \"choice 2\", ...]\n", + "\n", + "I'll mocking out responses with `get_mock_response...`. This is where you'd get the user's actual input.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "def get_mock_response_from_user_multiple_choice():\n", + " return \"a\"\n", + "\n", + "\n", + "def get_mock_response_from_user_free_response():\n", + " return \"I don't know.\"\n", + "\n", + "\n", + "def display_quiz(title, questions):\n", + " print(\"Quiz:\", title)\n", + " print()\n", + " responses = []\n", + "\n", + " for q in questions:\n", + " print(q[\"question_text\"])\n", + " response = \"\"\n", + "\n", + " # If multiple choice, print options\n", + " if q[\"question_type\"] == \"MULTIPLE_CHOICE\":\n", + " for i, choice in enumerate(q[\"choices\"]):\n", + " print(f\"{i}. {choice}\")\n", + " response = get_mock_response_from_user_multiple_choice()\n", + "\n", + " # Otherwise, just get response\n", + " elif q[\"question_type\"] == \"FREE_RESPONSE\":\n", + " response = get_mock_response_from_user_free_response()\n", + "\n", + " responses.append(response)\n", + " print()\n", + "\n", + " return responses" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here's what a sample quiz would look like:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Quiz: Sample Quiz\n", + "\n", + "What is your name?\n", + "\n", + "What is your favorite color?\n", + "0. Red\n", + "1. Blue\n", + "2. Green\n", + "3. Yellow\n", + "\n", + "Responses: [\"I don't know.\", 'a']\n" + ] + } + ], + "source": [ + "responses = display_quiz(\n", + " \"Sample Quiz\",\n", + " [\n", + " {\"question_text\": \"What is your name?\", \"question_type\": \"FREE_RESPONSE\"},\n", + " {\n", + " \"question_text\": \"What is your favorite color?\",\n", + " \"question_type\": \"MULTIPLE_CHOICE\",\n", + " \"choices\": [\"Red\", \"Blue\", \"Green\", \"Yellow\"],\n", + " },\n", + " ],\n", + ")\n", + "print(\"Responses:\", responses)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's define the interface of this function in JSON format, so our Assistant can call it:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [], + "source": [ + "function_json = {\n", + " \"name\": \"display_quiz\",\n", + " \"description\": \"Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.\",\n", + " \"parameters\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"title\": {\"type\": \"string\"},\n", + " \"questions\": {\n", + " \"type\": \"array\",\n", + " \"description\": \"An array of questions, each with a title and potentially options (if multiple choice).\",\n", + " \"items\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"question_text\": {\"type\": \"string\"},\n", + " \"question_type\": {\n", + " \"type\": \"string\",\n", + " \"enum\": [\"MULTIPLE_CHOICE\", \"FREE_RESPONSE\"]\n", + " },\n", + " \"choices\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}}\n", + " },\n", + " \"required\": [\"question_text\"]\n", + " }\n", + " }\n", + " },\n", + " \"required\": [\"title\", \"questions\"]\n", + " }\n", + "}\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once again, let's update our Assistant either through the Dashboard or the API.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Enabling custom function](../images/assistants_overview_enable_function.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> **Note**\n", + "> Pasting the function JSON into the Dashboard was a bit finicky due to indentation, etc. I just asked ChatGPT to format my function the same as one of the examples on the Dashboard :).\n" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',\n", + " 'created_at': 1736340398,\n", + " 'description': None,\n", + " 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',\n", + " 'metadata': {},\n", + " 'model': 'gpt-4o',\n", + " 'name': 'Math Tutor',\n", + " 'object': 'assistant',\n", + " 'tools': [{'type': 'code_interpreter'},\n", + " {'type': 'file_search',\n", + " 'file_search': {'max_num_results': None,\n", + " 'ranking_options': {'score_threshold': 0.0,\n", + " 'ranker': 'default_2024_08_21'}}},\n", + " {'function': {'name': 'display_quiz',\n", + " 'description': \"Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.\",\n", + " 'description': \"Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.\",\n", + " 'parameters': {'type': 'object',\n", + " 'properties': {'title': {'type': 'string'},\n", + " 'questions': {'type': 'array',\n", + " 'description': 'An array of questions, each with a title and potentially options (if multiple choice).',\n", + " 'items': {'type': 'object',\n", + " 'properties': {'question_text': {'type': 'string'},\n", + " 'question_type': {'type': 'string',\n", + " 'enum': ['MULTIPLE_CHOICE', 'FREE_RESPONSE']},\n", + " 'choices': {'type': 'array', 'items': {'type': 'string'}}},\n", + " 'required': ['question_text']}}},\n", + " 'required': ['title', 'questions']},\n", + " 'strict': False},\n", + " 'type': 'function'}],\n", + " 'response_format': 'auto',\n", + " 'temperature': 1.0,\n", + " 'tool_resources': {'code_interpreter': {'file_ids': ['file-GQFm2i7N8LrAQatefWKEsE']},\n", + " 'file_search': {'vector_store_ids': []}},\n", + " 'top_p': 1.0}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "assistant = client.beta.assistants.update(\n", + " MATH_ASSISTANT_ID,\n", + " tools=[\n", + " {\"type\": \"code_interpreter\"},\n", + " {\"type\": \"file_search\"},\n", + " {\"type\": \"function\", \"function\": function_json},\n", + " ],\n", + ")\n", + "show_json(assistant)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And now, we ask for a quiz.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'requires_action'" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "thread, run = create_thread_and_run(\n", + " \"Make a quiz with 2 questions: One open ended, one multiple choice. Then, give me feedback for the responses.\"\n", + ")\n", + "run = wait_on_run(run, thread)\n", + "run.status" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, however, when we check the Run's `status` we see `requires_action`! Let's take a closer.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': 'run_ekMRSI2h35asEzKirRf4BTwZ',\n", + " 'assistant_id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',\n", + " 'cancelled_at': None,\n", + " 'completed_at': None,\n", + " 'created_at': 1736341020,\n", + " 'expires_at': 1736341620,\n", + " 'failed_at': None,\n", + " 'incomplete_details': None,\n", + " 'incomplete_details': None,\n", + " 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',\n", + " 'last_error': None,\n", + " 'max_completion_tokens': None,\n", + " 'max_prompt_tokens': None,\n", + " 'max_completion_tokens': None,\n", + " 'max_prompt_tokens': None,\n", + " 'metadata': {},\n", + " 'model': 'gpt-4o',\n", + " 'object': 'thread.run',\n", + " 'parallel_tool_calls': True,\n", + " 'required_action': {'submit_tool_outputs': {'tool_calls': [{'id': 'call_uvJEn0fxM4sgmzek8wahBGLi',\n", + " 'function': {'arguments': '{\"title\":\"Math Quiz\",\"questions\":[{\"question_text\":\"What is the derivative of the function f(x) = 3x^2 + 2x - 5?\",\"question_type\":\"FREE_RESPONSE\"},{\"question_text\":\"What is the value of \\\\\\\\( \\\\\\\\int_{0}^{1} 2x \\\\\\\\, dx \\\\\\\\)?\",\"question_type\":\"MULTIPLE_CHOICE\",\"choices\":[\"0\",\"1\",\"2\",\"3\"]}]}',\n", + " 'name': 'display_quiz'},\n", + " 'type': 'function'}]},\n", + " 'type': 'submit_tool_outputs'},\n", + " 'response_format': 'auto',\n", + " 'started_at': 1736341022,\n", + " 'status': 'requires_action',\n", + " 'thread_id': 'thread_8bK2PXfoeijEHBVEzYuJXt17',\n", + " 'tool_choice': 'auto',\n", + " 'tools': [{'type': 'code_interpreter'},\n", + " {'type': 'file_search',\n", + " 'file_search': {'max_num_results': None,\n", + " 'ranking_options': {'score_threshold': 0.0,\n", + " 'ranker': 'default_2024_08_21'}}},\n", + " {'function': {'name': 'display_quiz',\n", + " 'description': \"Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.\",\n", + " 'description': \"Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.\",\n", + " 'parameters': {'type': 'object',\n", + " 'properties': {'title': {'type': 'string'},\n", + " 'questions': {'type': 'array',\n", + " 'description': 'An array of questions, each with a title and potentially options (if multiple choice).',\n", + " 'items': {'type': 'object',\n", + " 'properties': {'question_text': {'type': 'string'},\n", + " 'question_type': {'type': 'string',\n", + " 'enum': ['MULTIPLE_CHOICE', 'FREE_RESPONSE']},\n", + " 'choices': {'type': 'array', 'items': {'type': 'string'}}},\n", + " 'required': ['question_text']}}},\n", + " 'required': ['title', 'questions']},\n", + " 'strict': False},\n", + " 'type': 'function'}],\n", + " 'truncation_strategy': {'type': 'auto', 'last_messages': None},\n", + " 'usage': None,\n", + " 'temperature': 1.0,\n", + " 'top_p': 1.0,\n", + " 'tool_resources': {}}", + " 'strict': False},\n", + " 'type': 'function'}],\n", + " 'truncation_strategy': {'type': 'auto', 'last_messages': None},\n", + " 'usage': None,\n", + " 'temperature': 1.0,\n", + " 'top_p': 1.0,\n", + " 'tool_resources': {}}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "show_json(run)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `required_action` field indicates a Tool is waiting for us to run it and submit its output back to the Assistant. Specifically, the `display_quiz` function! Let's start by parsing the `name` and `arguments`.\n", + "\n", + "> **Note**\n", + "> While in this case we know there is only one Tool call, in practice the Assistant may choose to call multiple tools.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Function Name: display_quiz\n", + "Function Arguments:\n" + ] + }, + { + "data": { + "text/plain": [ + "{'title': 'Math Quiz',\n", + " 'questions': [{'question_text': 'What is the derivative of the function f(x) = 3x^2 + 2x - 5?',\n", + " 'question_type': 'FREE_RESPONSE'},\n", + " {'question_text': 'What is the value of \\\\( \\\\int_{0}^{1} 2x \\\\, dx \\\\)?',\n", + " 'question_type': 'MULTIPLE_CHOICE',\n", + " 'choices': ['0', '1', '2', '3']}]}" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Extract single tool call\n", + "tool_call = run.required_action.submit_tool_outputs.tool_calls[0]\n", + "name = tool_call.function.name\n", + "arguments = json.loads(tool_call.function.arguments)\n", + "\n", + "print(\"Function Name:\", name)\n", + "print(\"Function Arguments:\")\n", + "arguments" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's actually call our `display_quiz` function with the arguments provided by the Assistant:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Quiz: Math Quiz\n", + "Quiz: Math Quiz\n", + "\n", + "What is the derivative of the function f(x) = 3x^2 + 2x - 5?\n", + "\n", + "What is the value of \\( \\int_{0}^{1} 2x \\, dx \\)?\n", + "0. 0\n", + "1. 1\n", + "2. 2\n", + "3. 3\n", + "\n", + "Responses: [\"I don't know.\", 'a']\n" + ] + } + ], + "source": [ + "responses = display_quiz(arguments[\"title\"], arguments[\"questions\"])\n", + "print(\"Responses:\", responses)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Great! (Remember these responses are the one's we mocked earlier. In reality, we'd be getting input from the back from this function call.)\n", + "\n", + "Now that we have our responses, let's submit them back to the Assistant. We'll need the `tool_call` ID, found in the `tool_call` we parsed out earlier. We'll also need to encode our `list`of responses into a `str`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': 'run_ekMRSI2h35asEzKirRf4BTwZ',\n", + " 'assistant_id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',\n", + " 'cancelled_at': None,\n", + " 'completed_at': None,\n", + " 'created_at': 1736341020,\n", + " 'expires_at': 1736341620,\n", + " 'failed_at': None,\n", + " 'incomplete_details': None,\n", + " 'incomplete_details': None,\n", + " 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',\n", + " 'last_error': None,\n", + " 'max_completion_tokens': None,\n", + " 'max_prompt_tokens': None,\n", + " 'max_completion_tokens': None,\n", + " 'max_prompt_tokens': None,\n", + " 'metadata': {},\n", + " 'model': 'gpt-4o',\n", + " 'object': 'thread.run',\n", + " 'parallel_tool_calls': True,\n", + " 'parallel_tool_calls': True,\n", + " 'required_action': None,\n", + " 'response_format': 'auto',\n", + " 'started_at': 1736341022,\n", + " 'status': 'queued',\n", + " 'thread_id': 'thread_8bK2PXfoeijEHBVEzYuJXt17',\n", + " 'tool_choice': 'auto',\n", + " 'tools': [{'type': 'code_interpreter'},\n", + " {'type': 'file_search',\n", + " 'file_search': {'max_num_results': None,\n", + " 'ranking_options': {'score_threshold': 0.0,\n", + " 'ranker': 'default_2024_08_21'}}},\n", + " {'function': {'name': 'display_quiz',\n", + " 'description': \"Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.\",\n", + " 'description': \"Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.\",\n", + " 'parameters': {'type': 'object',\n", + " 'properties': {'title': {'type': 'string'},\n", + " 'questions': {'type': 'array',\n", + " 'description': 'An array of questions, each with a title and potentially options (if multiple choice).',\n", + " 'items': {'type': 'object',\n", + " 'properties': {'question_text': {'type': 'string'},\n", + " 'question_type': {'type': 'string',\n", + " 'enum': ['MULTIPLE_CHOICE', 'FREE_RESPONSE']},\n", + " 'choices': {'type': 'array', 'items': {'type': 'string'}}},\n", + " 'required': ['question_text']}}},\n", + " 'required': ['title', 'questions']},\n", + " 'strict': False},\n", + " 'type': 'function'}],\n", + " 'truncation_strategy': {'type': 'auto', 'last_messages': None},\n", + " 'usage': None,\n", + " 'temperature': 1.0,\n", + " 'top_p': 1.0,\n", + " 'tool_resources': {}}", + " 'strict': False},\n", + " 'type': 'function'}],\n", + " 'truncation_strategy': {'type': 'auto', 'last_messages': None},\n", + " 'usage': None,\n", + " 'temperature': 1.0,\n", + " 'top_p': 1.0,\n", + " 'tool_resources': {}}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "run = client.beta.threads.runs.submit_tool_outputs(\n", + " thread_id=thread.id,\n", + " run_id=run.id,\n", + " tool_outputs=tool_outputs\n", + ")\n", + "show_json(run)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now wait for the Run to complete once again, and check our Thread!\n" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "# Messages\n", + "user: Make a quiz with 2 questions: One open ended, one multiple choice. Then, give me feedback for the responses.\n", + "assistant: Since no specific information was found in the uploaded file, I'll create a general math quiz for you:\n", + "\n", + "1. **Open-ended Question**: What is the derivative of the function \\( f(x) = 3x^2 + 2x - 5 \\)?\n", + "\n", + "2. **Multiple Choice Question**: What is the value of \\( \\int_{0}^{1} 2x \\, dx \\)?\n", + " - A) 0\n", + " - B) 1\n", + " - C) 2\n", + " - D) 3\n", + "\n", + "I will now present the quiz to you for response.\n", + "assistant: Here is the feedback for your responses:\n", + "\n", + "1. **Derivative Question**: \n", + " - Your Response: \"I don't know.\"\n", + " - Feedback: The derivative of \\( f(x) = 3x^2 + 2x - 5 \\) is \\( f'(x) = 6x + 2 \\).\n", + "\n", + "2. **Integration Question**: \n", + " - Your Response: A) 0\n", + " - Feedback: The correct answer is B) 1. The integration \\(\\int_{0}^{1} 2x \\, dx \\) evaluates to 1.\n", + "\n" + ] + } + ], + "source": [ + "run = wait_on_run(run, thread)\n", + "pretty_print(get_response(thread))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Woohoo 🎉\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "We covered a lot of ground in this notebook, give yourself a high-five! Hopefully you should now have a strong foundation to build powerful, stateful experiences with tools like Code Interpreter, Retrieval, and Functions!\n", + "\n", + "There's a few sections we didn't cover for the sake of brevity, so here's a few resources to explore further:\n", + "\n", + "- [Annotations](https://platform.openai.com/docs/assistants/how-it-works/managing-threads-and-messages): parsing file citations\n", + "- [Files](https://platform.openai.com/docs/api-reference/assistants/file-object): Thread scoped vs Assistant scoped\n", + "- [Parallel Function Calls](https://platform.openai.com/docs/guides/function-calling/parallel-function-calling): calling multiple tools in a single Step\n", + "- Multi-Assistant Thread Runs: single Thread with Messages from multiple Assistants\n", + "- Streaming: coming soon!\n", + "\n", + "Now go off and build something ama[zing](https://www.youtube.com/watch?v=xvFZjo5PgG0&pp=ygUQcmljayByb2xsIG5vIGFkcw%3D%3D)!\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "openai", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/manual_checks/jupyter/jupyter_complex_good.ipynb b/manual_checks/jupyter/jupyter_complex_good.ipynb new file mode 100644 index 0000000..c2857ca --- /dev/null +++ b/manual_checks/jupyter/jupyter_complex_good.ipynb @@ -0,0 +1,886 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Running Code" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First and foremost, the Jupyter Notebook is an interactive environment for writing and running code. The notebook is capable of running code in a wide range of languages. However, each notebook is associated with a single kernel. This notebook is associated with the IPython kernel, therefore runs Python code." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Code cells allow you to enter and run code" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Run a code cell using `Shift-Enter` or pressing the button in the toolbar above:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "a = 10" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "10\n" + ] + } + ], + "source": [ + "print(a)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are two other keyboard shortcuts for running code:\n", + "\n", + "* `Alt-Enter` runs the current cell and inserts a new one below.\n", + "* `Ctrl-Enter` run the current cell and enters command mode." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Managing the Kernel" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Code is run in a separate process called the Kernel. The Kernel can be interrupted or restarted. Try running the following cell and then hit the button in the toolbar above." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import time\n", + "time.sleep(10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If the Kernel dies you will be prompted to restart it. Here we call the low-level system libc.time routine with the wrong argument via\n", + "ctypes to segfault the Python interpreter:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import sys\n", + "from ctypes import CDLL\n", + "# This will crash a Linux or Mac system\n", + "# equivalent calls can be made on Windows\n", + "\n", + "# Uncomment these lines if you would like to see the segfault\n", + "\n", + "# dll = 'dylib' if sys.platform == 'darwin' else 'so.6'\n", + "# libc = CDLL(\"libc.%s\" % dll) \n", + "# libc.time(-1) # BOOM!!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Cell menu" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The \"Cell\" menu has a number of menu items for running code in different ways. These includes:\n", + "\n", + "* Run and Select Below\n", + "* Run and Insert Below\n", + "* Run All\n", + "* Run All Above\n", + "* Run All Below" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Restarting the kernels" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The kernel maintains the state of a notebook's computations. You can reset this state by restarting the kernel. This is done by clicking on the in the toolbar above." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## sys.stdout and sys.stderr" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The stdout and stderr streams are displayed as text in the output area." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "hi, stdout\n" + ] + } + ], + "source": [ + "print(\"hi, stdout\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "hi, stderr\n" + ] + } + ], + "source": [ + "from __future__ import print_function\n", + "print('hi, stderr', file=sys.stderr)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Output is asynchronous" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All output is displayed asynchronously as it is generated in the Kernel. If you execute the next cell, you will see the output one piece at a time, not all at the end." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0\n", + "1\n", + "2\n", + "3\n", + "4\n", + "5\n", + "6\n", + "7\n" + ] + } + ], + "source": [ + "import time, sys\n", + "for i in range(8):\n", + " print(i)\n", + " time.sleep(0.5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Large outputs" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To better handle large outputs, the output area can be collapsed. Run the following cell and then single- or double- click on the active area to the left of the output:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0\n", + "1\n", + "2\n", + "3\n", + "4\n", + "5\n", + "6\n", + "7\n", + "8\n", + "9\n", + "10\n", + "11\n", + "12\n", + "13\n", + "14\n", + "15\n", + "16\n", + "17\n", + "18\n", + "19\n", + "20\n", + "21\n", + "22\n", + "23\n", + "24\n", + "25\n", + "26\n", + "27\n", + "28\n", + "29\n", + "30\n", + "31\n", + "32\n", + "33\n", + "34\n", + "35\n", + "36\n", + "37\n", + "38\n", + "39\n", + "40\n", + "41\n", + "42\n", + "43\n", + "44\n", + "45\n", + "46\n", + "47\n", + "48\n", + "49\n" + ] + } + ], + "source": [ + "for i in range(50):\n", + " print(i)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Beyond a certain point, output will scroll automatically:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0\n", + "1\n", + "3\n", + "7\n", + "15\n", + "31\n", + "63\n", + "127\n", + "255\n", + "511\n", + "1023\n", + "2047\n", + "4095\n", + "8191\n", + "16383\n", + "32767\n", + "65535\n", + "131071\n", + "262143\n", + "524287\n", + "1048575\n", + "2097151\n", + "4194303\n", + "8388607\n", + "16777215\n", + "33554431\n", + "67108863\n", + "134217727\n", + "268435455\n", + "536870911\n", + "1073741823\n", + "2147483647\n", + "4294967295\n", + "8589934591\n", + "17179869183\n", + "34359738367\n", + "68719476735\n", + "137438953471\n", + "274877906943\n", + "549755813887\n", + "1099511627775\n", + "2199023255551\n", + "4398046511103\n", + "8796093022207\n", + "17592186044415\n", + "35184372088831\n", + "70368744177663\n", + "140737488355327\n", + "281474976710655\n", + "562949953421311\n", + "1125899906842623\n", + "2251799813685247\n", + "4503599627370495\n", + "9007199254740991\n", + "18014398509481983\n", + "36028797018963967\n", + "72057594037927935\n", + "144115188075855871\n", + "288230376151711743\n", + "576460752303423487\n", + "1152921504606846975\n", + "2305843009213693951\n", + "4611686018427387903\n", + "9223372036854775807\n", + "18446744073709551615\n", + "36893488147419103231\n", + "73786976294838206463\n", + "147573952589676412927\n", + "295147905179352825855\n", + "590295810358705651711\n", + "1180591620717411303423\n", + "2361183241434822606847\n", + "4722366482869645213695\n", + "9444732965739290427391\n", + "18889465931478580854783\n", + "37778931862957161709567\n", + "75557863725914323419135\n", + "151115727451828646838271\n", + "302231454903657293676543\n", + "604462909807314587353087\n", + "1208925819614629174706175\n", + "2417851639229258349412351\n", + "4835703278458516698824703\n", + "9671406556917033397649407\n", + "19342813113834066795298815\n", + "38685626227668133590597631\n", + "77371252455336267181195263\n", + "154742504910672534362390527\n", + "309485009821345068724781055\n", + "618970019642690137449562111\n", + "1237940039285380274899124223\n", + "2475880078570760549798248447\n", + "4951760157141521099596496895\n", + "9903520314283042199192993791\n", + "19807040628566084398385987583\n", + "39614081257132168796771975167\n", + "79228162514264337593543950335\n", + "158456325028528675187087900671\n", + "316912650057057350374175801343\n", + "633825300114114700748351602687\n", + "1267650600228229401496703205375\n", + "2535301200456458802993406410751\n", + "5070602400912917605986812821503\n", + "10141204801825835211973625643007\n", + "20282409603651670423947251286015\n", + "40564819207303340847894502572031\n", + "81129638414606681695789005144063\n", + "162259276829213363391578010288127\n", + "324518553658426726783156020576255\n", + "649037107316853453566312041152511\n", + "1298074214633706907132624082305023\n", + "2596148429267413814265248164610047\n", + "5192296858534827628530496329220095\n", + "10384593717069655257060992658440191\n", + "20769187434139310514121985316880383\n", + "41538374868278621028243970633760767\n", + "83076749736557242056487941267521535\n", + "166153499473114484112975882535043071\n", + "332306998946228968225951765070086143\n", + "664613997892457936451903530140172287\n", + "1329227995784915872903807060280344575\n", + "2658455991569831745807614120560689151\n", + "5316911983139663491615228241121378303\n", + "10633823966279326983230456482242756607\n", + "21267647932558653966460912964485513215\n", + "42535295865117307932921825928971026431\n", + "85070591730234615865843651857942052863\n", + "170141183460469231731687303715884105727\n", + "340282366920938463463374607431768211455\n", + "680564733841876926926749214863536422911\n", + "1361129467683753853853498429727072845823\n", + "2722258935367507707706996859454145691647\n", + "5444517870735015415413993718908291383295\n", + "10889035741470030830827987437816582766591\n", + "21778071482940061661655974875633165533183\n", + "43556142965880123323311949751266331066367\n", + "87112285931760246646623899502532662132735\n", + "174224571863520493293247799005065324265471\n", + "348449143727040986586495598010130648530943\n", + "696898287454081973172991196020261297061887\n", + "1393796574908163946345982392040522594123775\n", + "2787593149816327892691964784081045188247551\n", + "5575186299632655785383929568162090376495103\n", + "11150372599265311570767859136324180752990207\n", + "22300745198530623141535718272648361505980415\n", + "44601490397061246283071436545296723011960831\n", + "89202980794122492566142873090593446023921663\n", + "178405961588244985132285746181186892047843327\n", + "356811923176489970264571492362373784095686655\n", + "713623846352979940529142984724747568191373311\n", + "1427247692705959881058285969449495136382746623\n", + "2854495385411919762116571938898990272765493247\n", + "5708990770823839524233143877797980545530986495\n", + "11417981541647679048466287755595961091061972991\n", + "22835963083295358096932575511191922182123945983\n", + "45671926166590716193865151022383844364247891967\n", + "91343852333181432387730302044767688728495783935\n", + "182687704666362864775460604089535377456991567871\n", + "365375409332725729550921208179070754913983135743\n", + "730750818665451459101842416358141509827966271487\n", + "1461501637330902918203684832716283019655932542975\n", + "2923003274661805836407369665432566039311865085951\n", + "5846006549323611672814739330865132078623730171903\n", + "11692013098647223345629478661730264157247460343807\n", + "23384026197294446691258957323460528314494920687615\n", + "46768052394588893382517914646921056628989841375231\n", + "93536104789177786765035829293842113257979682750463\n", + "187072209578355573530071658587684226515959365500927\n", + "374144419156711147060143317175368453031918731001855\n", + "748288838313422294120286634350736906063837462003711\n", + "1496577676626844588240573268701473812127674924007423\n", + "2993155353253689176481146537402947624255349848014847\n", + "5986310706507378352962293074805895248510699696029695\n", + "11972621413014756705924586149611790497021399392059391\n", + "23945242826029513411849172299223580994042798784118783\n", + "47890485652059026823698344598447161988085597568237567\n", + "95780971304118053647396689196894323976171195136475135\n", + "191561942608236107294793378393788647952342390272950271\n", + "383123885216472214589586756787577295904684780545900543\n", + "766247770432944429179173513575154591809369561091801087\n", + "1532495540865888858358347027150309183618739122183602175\n", + "3064991081731777716716694054300618367237478244367204351\n", + "6129982163463555433433388108601236734474956488734408703\n", + "12259964326927110866866776217202473468949912977468817407\n", + "24519928653854221733733552434404946937899825954937634815\n", + "49039857307708443467467104868809893875799651909875269631\n", + "98079714615416886934934209737619787751599303819750539263\n", + "196159429230833773869868419475239575503198607639501078527\n", + "392318858461667547739736838950479151006397215279002157055\n", + "784637716923335095479473677900958302012794430558004314111\n", + "1569275433846670190958947355801916604025588861116008628223\n", + "3138550867693340381917894711603833208051177722232017256447\n", + "6277101735386680763835789423207666416102355444464034512895\n", + "12554203470773361527671578846415332832204710888928069025791\n", + "25108406941546723055343157692830665664409421777856138051583\n", + "50216813883093446110686315385661331328818843555712276103167\n", + "100433627766186892221372630771322662657637687111424552206335\n", + "200867255532373784442745261542645325315275374222849104412671\n", + "401734511064747568885490523085290650630550748445698208825343\n", + "803469022129495137770981046170581301261101496891396417650687\n", + "1606938044258990275541962092341162602522202993782792835301375\n", + "3213876088517980551083924184682325205044405987565585670602751\n", + "6427752177035961102167848369364650410088811975131171341205503\n", + "12855504354071922204335696738729300820177623950262342682411007\n", + "25711008708143844408671393477458601640355247900524685364822015\n", + "51422017416287688817342786954917203280710495801049370729644031\n", + "102844034832575377634685573909834406561420991602098741459288063\n", + "205688069665150755269371147819668813122841983204197482918576127\n", + "411376139330301510538742295639337626245683966408394965837152255\n", + "822752278660603021077484591278675252491367932816789931674304511\n", + "1645504557321206042154969182557350504982735865633579863348609023\n", + "3291009114642412084309938365114701009965471731267159726697218047\n", + "6582018229284824168619876730229402019930943462534319453394436095\n", + "13164036458569648337239753460458804039861886925068638906788872191\n", + "26328072917139296674479506920917608079723773850137277813577744383\n", + "52656145834278593348959013841835216159447547700274555627155488767\n", + "105312291668557186697918027683670432318895095400549111254310977535\n", + "210624583337114373395836055367340864637790190801098222508621955071\n", + "421249166674228746791672110734681729275580381602196445017243910143\n", + "842498333348457493583344221469363458551160763204392890034487820287\n", + "1684996666696914987166688442938726917102321526408785780068975640575\n", + "3369993333393829974333376885877453834204643052817571560137951281151\n", + "6739986666787659948666753771754907668409286105635143120275902562303\n", + "13479973333575319897333507543509815336818572211270286240551805124607\n", + "26959946667150639794667015087019630673637144422540572481103610249215\n", + "53919893334301279589334030174039261347274288845081144962207220498431\n", + "107839786668602559178668060348078522694548577690162289924414440996863\n", + "215679573337205118357336120696157045389097155380324579848828881993727\n", + "431359146674410236714672241392314090778194310760649159697657763987455\n", + "862718293348820473429344482784628181556388621521298319395315527974911\n", + "1725436586697640946858688965569256363112777243042596638790631055949823\n", + "3450873173395281893717377931138512726225554486085193277581262111899647\n", + "6901746346790563787434755862277025452451108972170386555162524223799295\n", + "13803492693581127574869511724554050904902217944340773110325048447598591\n", + "27606985387162255149739023449108101809804435888681546220650096895197183\n", + "55213970774324510299478046898216203619608871777363092441300193790394367\n", + "110427941548649020598956093796432407239217743554726184882600387580788735\n", + "220855883097298041197912187592864814478435487109452369765200775161577471\n", + "441711766194596082395824375185729628956870974218904739530401550323154943\n", + "883423532389192164791648750371459257913741948437809479060803100646309887\n", + "1766847064778384329583297500742918515827483896875618958121606201292619775\n", + "3533694129556768659166595001485837031654967793751237916243212402585239551\n", + "7067388259113537318333190002971674063309935587502475832486424805170479103\n", + "14134776518227074636666380005943348126619871175004951664972849610340958207\n", + "28269553036454149273332760011886696253239742350009903329945699220681916415\n", + "56539106072908298546665520023773392506479484700019806659891398441363832831\n", + "113078212145816597093331040047546785012958969400039613319782796882727665663\n", + "226156424291633194186662080095093570025917938800079226639565593765455331327\n", + "452312848583266388373324160190187140051835877600158453279131187530910662655\n", + "904625697166532776746648320380374280103671755200316906558262375061821325311\n", + "1809251394333065553493296640760748560207343510400633813116524750123642650623\n", + "3618502788666131106986593281521497120414687020801267626233049500247285301247\n", + "7237005577332262213973186563042994240829374041602535252466099000494570602495\n", + "14474011154664524427946373126085988481658748083205070504932198000989141204991\n", + "28948022309329048855892746252171976963317496166410141009864396001978282409983\n", + "57896044618658097711785492504343953926634992332820282019728792003956564819967\n", + "115792089237316195423570985008687907853269984665640564039457584007913129639935\n", + "231584178474632390847141970017375815706539969331281128078915168015826259279871\n", + "463168356949264781694283940034751631413079938662562256157830336031652518559743\n", + "926336713898529563388567880069503262826159877325124512315660672063305037119487\n", + "1852673427797059126777135760139006525652319754650249024631321344126610074238975\n", + "3705346855594118253554271520278013051304639509300498049262642688253220148477951\n", + "7410693711188236507108543040556026102609279018600996098525285376506440296955903\n", + "14821387422376473014217086081112052205218558037201992197050570753012880593911807\n", + "29642774844752946028434172162224104410437116074403984394101141506025761187823615\n", + "59285549689505892056868344324448208820874232148807968788202283012051522375647231\n", + "118571099379011784113736688648896417641748464297615937576404566024103044751294463\n", + "237142198758023568227473377297792835283496928595231875152809132048206089502588927\n", + "474284397516047136454946754595585670566993857190463750305618264096412179005177855\n", + "948568795032094272909893509191171341133987714380927500611236528192824358010355711\n", + "1897137590064188545819787018382342682267975428761855001222473056385648716020711423\n", + "3794275180128377091639574036764685364535950857523710002444946112771297432041422847\n", + "7588550360256754183279148073529370729071901715047420004889892225542594864082845695\n", + "15177100720513508366558296147058741458143803430094840009779784451085189728165691391\n", + "30354201441027016733116592294117482916287606860189680019559568902170379456331382783\n", + "60708402882054033466233184588234965832575213720379360039119137804340758912662765567\n", + "121416805764108066932466369176469931665150427440758720078238275608681517825325531135\n", + "242833611528216133864932738352939863330300854881517440156476551217363035650651062271\n", + "485667223056432267729865476705879726660601709763034880312953102434726071301302124543\n", + "971334446112864535459730953411759453321203419526069760625906204869452142602604249087\n", + "1942668892225729070919461906823518906642406839052139521251812409738904285205208498175\n", + "3885337784451458141838923813647037813284813678104279042503624819477808570410416996351\n", + "7770675568902916283677847627294075626569627356208558085007249638955617140820833992703\n", + "15541351137805832567355695254588151253139254712417116170014499277911234281641667985407\n", + "31082702275611665134711390509176302506278509424834232340028998555822468563283335970815\n", + "62165404551223330269422781018352605012557018849668464680057997111644937126566671941631\n", + "124330809102446660538845562036705210025114037699336929360115994223289874253133343883263\n", + "248661618204893321077691124073410420050228075398673858720231988446579748506266687766527\n", + "497323236409786642155382248146820840100456150797347717440463976893159497012533375533055\n", + "994646472819573284310764496293641680200912301594695434880927953786318994025066751066111\n", + "1989292945639146568621528992587283360401824603189390869761855907572637988050133502132223\n", + "3978585891278293137243057985174566720803649206378781739523711815145275976100267004264447\n", + "7957171782556586274486115970349133441607298412757563479047423630290551952200534008528895\n", + "15914343565113172548972231940698266883214596825515126958094847260581103904401068017057791\n", + "31828687130226345097944463881396533766429193651030253916189694521162207808802136034115583\n", + "63657374260452690195888927762793067532858387302060507832379389042324415617604272068231167\n", + "127314748520905380391777855525586135065716774604121015664758778084648831235208544136462335\n", + "254629497041810760783555711051172270131433549208242031329517556169297662470417088272924671\n", + "509258994083621521567111422102344540262867098416484062659035112338595324940834176545849343\n", + "1018517988167243043134222844204689080525734196832968125318070224677190649881668353091698687\n", + "2037035976334486086268445688409378161051468393665936250636140449354381299763336706183397375\n", + "4074071952668972172536891376818756322102936787331872501272280898708762599526673412366794751\n", + "8148143905337944345073782753637512644205873574663745002544561797417525199053346824733589503\n", + "16296287810675888690147565507275025288411747149327490005089123594835050398106693649467179007\n", + "32592575621351777380295131014550050576823494298654980010178247189670100796213387298934358015\n", + "65185151242703554760590262029100101153646988597309960020356494379340201592426774597868716031\n", + "130370302485407109521180524058200202307293977194619920040712988758680403184853549195737432063\n", + "260740604970814219042361048116400404614587954389239840081425977517360806369707098391474864127\n", + "521481209941628438084722096232800809229175908778479680162851955034721612739414196782949728255\n", + "1042962419883256876169444192465601618458351817556959360325703910069443225478828393565899456511\n", + "2085924839766513752338888384931203236916703635113918720651407820138886450957656787131798913023\n", + "4171849679533027504677776769862406473833407270227837441302815640277772901915313574263597826047\n", + "8343699359066055009355553539724812947666814540455674882605631280555545803830627148527195652095\n", + "16687398718132110018711107079449625895333629080911349765211262561111091607661254297054391304191\n", + "33374797436264220037422214158899251790667258161822699530422525122222183215322508594108782608383\n", + "66749594872528440074844428317798503581334516323645399060845050244444366430645017188217565216767\n", + "133499189745056880149688856635597007162669032647290798121690100488888732861290034376435130433535\n", + "266998379490113760299377713271194014325338065294581596243380200977777465722580068752870260867071\n", + "533996758980227520598755426542388028650676130589163192486760401955554931445160137505740521734143\n", + "1067993517960455041197510853084776057301352261178326384973520803911109862890320275011481043468287\n", + "2135987035920910082395021706169552114602704522356652769947041607822219725780640550022962086936575\n", + "4271974071841820164790043412339104229205409044713305539894083215644439451561281100045924173873151\n", + "8543948143683640329580086824678208458410818089426611079788166431288878903122562200091848347746303\n", + "17087896287367280659160173649356416916821636178853222159576332862577757806245124400183696695492607\n", + "34175792574734561318320347298712833833643272357706444319152665725155515612490248800367393390985215\n", + "68351585149469122636640694597425667667286544715412888638305331450311031224980497600734786781970431\n", + "136703170298938245273281389194851335334573089430825777276610662900622062449960995201469573563940863\n", + "273406340597876490546562778389702670669146178861651554553221325801244124899921990402939147127881727\n", + "546812681195752981093125556779405341338292357723303109106442651602488249799843980805878294255763455\n", + "1093625362391505962186251113558810682676584715446606218212885303204976499599687961611756588511526911\n", + "2187250724783011924372502227117621365353169430893212436425770606409952999199375923223513177023053823\n", + "4374501449566023848745004454235242730706338861786424872851541212819905998398751846447026354046107647\n", + "8749002899132047697490008908470485461412677723572849745703082425639811996797503692894052708092215295\n", + "17498005798264095394980017816940970922825355447145699491406164851279623993595007385788105416184430591\n", + "34996011596528190789960035633881941845650710894291398982812329702559247987190014771576210832368861183\n", + "69992023193056381579920071267763883691301421788582797965624659405118495974380029543152421664737722367\n", + "139984046386112763159840142535527767382602843577165595931249318810236991948760059086304843329475444735\n", + "279968092772225526319680285071055534765205687154331191862498637620473983897520118172609686658950889471\n", + "559936185544451052639360570142111069530411374308662383724997275240947967795040236345219373317901778943\n", + "1119872371088902105278721140284222139060822748617324767449994550481895935590080472690438746635803557887\n", + "2239744742177804210557442280568444278121645497234649534899989100963791871180160945380877493271607115775\n", + "4479489484355608421114884561136888556243290994469299069799978201927583742360321890761754986543214231551\n", + "8958978968711216842229769122273777112486581988938598139599956403855167484720643781523509973086428463103\n", + "17917957937422433684459538244547554224973163977877196279199912807710334969441287563047019946172856926207\n", + "35835915874844867368919076489095108449946327955754392558399825615420669938882575126094039892345713852415\n", + "71671831749689734737838152978190216899892655911508785116799651230841339877765150252188079784691427704831\n", + "143343663499379469475676305956380433799785311823017570233599302461682679755530300504376159569382855409663\n", + "286687326998758938951352611912760867599570623646035140467198604923365359511060601008752319138765710819327\n", + "573374653997517877902705223825521735199141247292070280934397209846730719022121202017504638277531421638655\n", + "1146749307995035755805410447651043470398282494584140561868794419693461438044242404035009276555062843277311\n", + "2293498615990071511610820895302086940796564989168281123737588839386922876088484808070018553110125686554623\n", + "4586997231980143023221641790604173881593129978336562247475177678773845752176969616140037106220251373109247\n", + "9173994463960286046443283581208347763186259956673124494950355357547691504353939232280074212440502746218495\n", + "18347988927920572092886567162416695526372519913346248989900710715095383008707878464560148424881005492436991\n", + "36695977855841144185773134324833391052745039826692497979801421430190766017415756929120296849762010984873983\n", + "73391955711682288371546268649666782105490079653384995959602842860381532034831513858240593699524021969747967\n", + "146783911423364576743092537299333564210980159306769991919205685720763064069663027716481187399048043939495935\n", + "293567822846729153486185074598667128421960318613539983838411371441526128139326055432962374798096087878991871\n", + "587135645693458306972370149197334256843920637227079967676822742883052256278652110865924749596192175757983743\n", + "1174271291386916613944740298394668513687841274454159935353645485766104512557304221731849499192384351515967487\n", + "2348542582773833227889480596789337027375682548908319870707290971532209025114608443463698998384768703031934975\n", + "4697085165547666455778961193578674054751365097816639741414581943064418050229216886927397996769537406063869951\n", + "9394170331095332911557922387157348109502730195633279482829163886128836100458433773854795993539074812127739903\n", + "18788340662190665823115844774314696219005460391266558965658327772257672200916867547709591987078149624255479807\n", + "37576681324381331646231689548629392438010920782533117931316655544515344401833735095419183974156299248510959615\n", + "75153362648762663292463379097258784876021841565066235862633311089030688803667470190838367948312598497021919231\n", + "150306725297525326584926758194517569752043683130132471725266622178061377607334940381676735896625196994043838463\n", + "300613450595050653169853516389035139504087366260264943450533244356122755214669880763353471793250393988087676927\n", + "601226901190101306339707032778070279008174732520529886901066488712245510429339761526706943586500787976175353855\n", + "1202453802380202612679414065556140558016349465041059773802132977424491020858679523053413887173001575952350707711\n", + "2404907604760405225358828131112281116032698930082119547604265954848982041717359046106827774346003151904701415423\n", + "4809815209520810450717656262224562232065397860164239095208531909697964083434718092213655548692006303809402830847\n", + "9619630419041620901435312524449124464130795720328478190417063819395928166869436184427311097384012607618805661695\n", + "19239260838083241802870625048898248928261591440656956380834127638791856333738872368854622194768025215237611323391\n", + "38478521676166483605741250097796497856523182881313912761668255277583712667477744737709244389536050430475222646783\n", + "76957043352332967211482500195592995713046365762627825523336510555167425334955489475418488779072100860950445293567\n", + "153914086704665934422965000391185991426092731525255651046673021110334850669910978950836977558144201721900890587135\n", + "307828173409331868845930000782371982852185463050511302093346042220669701339821957901673955116288403443801781174271\n", + "615656346818663737691860001564743965704370926101022604186692084441339402679643915803347910232576806887603562348543\n", + "1231312693637327475383720003129487931408741852202045208373384168882678805359287831606695820465153613775207124697087\n", + "2462625387274654950767440006258975862817483704404090416746768337765357610718575663213391640930307227550414249394175\n", + "4925250774549309901534880012517951725634967408808180833493536675530715221437151326426783281860614455100828498788351\n", + "9850501549098619803069760025035903451269934817616361666987073351061430442874302652853566563721228910201656997576703\n", + "19701003098197239606139520050071806902539869635232723333974146702122860885748605305707133127442457820403313995153407\n", + "39402006196394479212279040100143613805079739270465446667948293404245721771497210611414266254884915640806627990306815\n", + "78804012392788958424558080200287227610159478540930893335896586808491443542994421222828532509769831281613255980613631\n", + "157608024785577916849116160400574455220318957081861786671793173616982887085988842445657065019539662563226511961227263\n", + "315216049571155833698232320801148910440637914163723573343586347233965774171977684891314130039079325126453023922454527\n", + "630432099142311667396464641602297820881275828327447146687172694467931548343955369782628260078158650252906047844909055\n", + "1260864198284623334792929283204595641762551656654894293374345388935863096687910739565256520156317300505812095689818111\n", + "2521728396569246669585858566409191283525103313309788586748690777871726193375821479130513040312634601011624191379636223\n", + "5043456793138493339171717132818382567050206626619577173497381555743452386751642958261026080625269202023248382759272447\n", + "10086913586276986678343434265636765134100413253239154346994763111486904773503285916522052161250538404046496765518544895\n", + "20173827172553973356686868531273530268200826506478308693989526222973809547006571833044104322501076808092993531037089791\n", + "40347654345107946713373737062547060536401653012956617387979052445947619094013143666088208645002153616185987062074179583\n", + "80695308690215893426747474125094121072803306025913234775958104891895238188026287332176417290004307232371974124148359167\n", + "161390617380431786853494948250188242145606612051826469551916209783790476376052574664352834580008614464743948248296718335\n", + "322781234760863573706989896500376484291213224103652939103832419567580952752105149328705669160017228929487896496593436671\n", + "645562469521727147413979793000752968582426448207305878207664839135161905504210298657411338320034457858975792993186873343\n", + "1291124939043454294827959586001505937164852896414611756415329678270323811008420597314822676640068915717951585986373746687\n", + "2582249878086908589655919172003011874329705792829223512830659356540647622016841194629645353280137831435903171972747493375\n", + "5164499756173817179311838344006023748659411585658447025661318713081295244033682389259290706560275662871806343945494986751\n", + "10328999512347634358623676688012047497318823171316894051322637426162590488067364778518581413120551325743612687890989973503\n", + "20657999024695268717247353376024094994637646342633788102645274852325180976134729557037162826241102651487225375781979947007\n", + "41315998049390537434494706752048189989275292685267576205290549704650361952269459114074325652482205302974450751563959894015\n", + "82631996098781074868989413504096379978550585370535152410581099409300723904538918228148651304964410605948901503127919788031\n", + "165263992197562149737978827008192759957101170741070304821162198818601447809077836456297302609928821211897803006255839576063\n", + "330527984395124299475957654016385519914202341482140609642324397637202895618155672912594605219857642423795606012511679152127\n", + "661055968790248598951915308032771039828404682964281219284648795274405791236311345825189210439715284847591212025023358304255\n", + "1322111937580497197903830616065542079656809365928562438569297590548811582472622691650378420879430569695182424050046716608511\n", + "2644223875160994395807661232131084159313618731857124877138595181097623164945245383300756841758861139390364848100093433217023\n", + "5288447750321988791615322464262168318627237463714249754277190362195246329890490766601513683517722278780729696200186866434047\n", + "10576895500643977583230644928524336637254474927428499508554380724390492659780981533203027367035444557561459392400373732868095\n", + "21153791001287955166461289857048673274508949854856999017108761448780985319561963066406054734070889115122918784800747465736191\n", + "42307582002575910332922579714097346549017899709713998034217522897561970639123926132812109468141778230245837569601494931472383\n", + "84615164005151820665845159428194693098035799419427996068435045795123941278247852265624218936283556460491675139202989862944767\n", + "169230328010303641331690318856389386196071598838855992136870091590247882556495704531248437872567112920983350278405979725889535\n", + "338460656020607282663380637712778772392143197677711984273740183180495765112991409062496875745134225841966700556811959451779071\n", + "676921312041214565326761275425557544784286395355423968547480366360991530225982818124993751490268451683933401113623918903558143\n", + "1353842624082429130653522550851115089568572790710847937094960732721983060451965636249987502980536903367866802227247837807116287\n", + "2707685248164858261307045101702230179137145581421695874189921465443966120903931272499975005961073806735733604454495675614232575\n", + "5415370496329716522614090203404460358274291162843391748379842930887932241807862544999950011922147613471467208908991351228465151\n", + "10830740992659433045228180406808920716548582325686783496759685861775864483615725089999900023844295226942934417817982702456930303\n", + "21661481985318866090456360813617841433097164651373566993519371723551728967231450179999800047688590453885868835635965404913860607\n", + "43322963970637732180912721627235682866194329302747133987038743447103457934462900359999600095377180907771737671271930809827721215\n", + "86645927941275464361825443254471365732388658605494267974077486894206915868925800719999200190754361815543475342543861619655442431\n", + "173291855882550928723650886508942731464777317210988535948154973788413831737851601439998400381508723631086950685087723239310884863\n", + "346583711765101857447301773017885462929554634421977071896309947576827663475703202879996800763017447262173901370175446478621769727\n", + "693167423530203714894603546035770925859109268843954143792619895153655326951406405759993601526034894524347802740350892957243539455\n", + "1386334847060407429789207092071541851718218537687908287585239790307310653902812811519987203052069789048695605480701785914487078911\n", + "2772669694120814859578414184143083703436437075375816575170479580614621307805625623039974406104139578097391210961403571828974157823\n", + "5545339388241629719156828368286167406872874150751633150340959161229242615611251246079948812208279156194782421922807143657948315647\n", + "11090678776483259438313656736572334813745748301503266300681918322458485231222502492159897624416558312389564843845614287315896631295\n", + "22181357552966518876627313473144669627491496603006532601363836644916970462445004984319795248833116624779129687691228574631793262591\n", + "44362715105933037753254626946289339254982993206013065202727673289833940924890009968639590497666233249558259375382457149263586525183\n", + "88725430211866075506509253892578678509965986412026130405455346579667881849780019937279180995332466499116518750764914298527173050367\n", + "177450860423732151013018507785157357019931972824052260810910693159335763699560039874558361990664932998233037501529828597054346100735\n", + "354901720847464302026037015570314714039863945648104521621821386318671527399120079749116723981329865996466075003059657194108692201471\n", + "709803441694928604052074031140629428079727891296209043243642772637343054798240159498233447962659731992932150006119314388217384402943\n", + "1419606883389857208104148062281258856159455782592418086487285545274686109596480318996466895925319463985864300012238628776434768805887\n", + "2839213766779714416208296124562517712318911565184836172974571090549372219192960637992933791850638927971728600024477257552869537611775\n", + "5678427533559428832416592249125035424637823130369672345949142181098744438385921275985867583701277855943457200048954515105739075223551\n", + "11356855067118857664833184498250070849275646260739344691898284362197488876771842551971735167402555711886914400097909030211478150447103\n", + "22713710134237715329666368996500141698551292521478689383796568724394977753543685103943470334805111423773828800195818060422956300894207\n", + "45427420268475430659332737993000283397102585042957378767593137448789955507087370207886940669610222847547657600391636120845912601788415\n", + "90854840536950861318665475986000566794205170085914757535186274897579911014174740415773881339220445695095315200783272241691825203576831\n", + "181709681073901722637330951972001133588410340171829515070372549795159822028349480831547762678440891390190630401566544483383650407153663\n", + "363419362147803445274661903944002267176820680343659030140745099590319644056698961663095525356881782780381260803133088966767300814307327\n", + "726838724295606890549323807888004534353641360687318060281490199180639288113397923326191050713763565560762521606266177933534601628614655\n", + "1453677448591213781098647615776009068707282721374636120562980398361278576226795846652382101427527131121525043212532355867069203257229311\n", + "2907354897182427562197295231552018137414565442749272241125960796722557152453591693304764202855054262243050086425064711734138406514458623\n", + "5814709794364855124394590463104036274829130885498544482251921593445114304907183386609528405710108524486100172850129423468276813028917247\n", + "11629419588729710248789180926208072549658261770997088964503843186890228609814366773219056811420217048972200345700258846936553626057834495\n", + "23258839177459420497578361852416145099316523541994177929007686373780457219628733546438113622840434097944400691400517693873107252115668991\n", + "46517678354918840995156723704832290198633047083988355858015372747560914439257467092876227245680868195888801382801035387746214504231337983\n", + "93035356709837681990313447409664580397266094167976711716030745495121828878514934185752454491361736391777602765602070775492429008462675967\n", + "186070713419675363980626894819329160794532188335953423432061490990243657757029868371504908982723472783555205531204141550984858016925351935\n", + "372141426839350727961253789638658321589064376671906846864122981980487315514059736743009817965446945567110411062408283101969716033850703871\n", + "744282853678701455922507579277316643178128753343813693728245963960974631028119473486019635930893891134220822124816566203939432067701407743\n", + "1488565707357402911845015158554633286356257506687627387456491927921949262056238946972039271861787782268441644249633132407878864135402815487\n", + "2977131414714805823690030317109266572712515013375254774912983855843898524112477893944078543723575564536883288499266264815757728270805630975\n", + "5954262829429611647380060634218533145425030026750509549825967711687797048224955787888157087447151129073766576998532529631515456541611261951\n", + "11908525658859223294760121268437066290850060053501019099651935423375594096449911575776314174894302258147533153997065059263030913083222523903\n", + "23817051317718446589520242536874132581700120107002038199303870846751188192899823151552628349788604516295066307994130118526061826166445047807\n", + "47634102635436893179040485073748265163400240214004076398607741693502376385799646303105256699577209032590132615988260237052123652332890095615\n", + "95268205270873786358080970147496530326800480428008152797215483387004752771599292606210513399154418065180265231976520474104247304665780191231\n", + "190536410541747572716161940294993060653600960856016305594430966774009505543198585212421026798308836130360530463953040948208494609331560382463\n", + "381072821083495145432323880589986121307201921712032611188861933548019011086397170424842053596617672260721060927906081896416989218663120764927\n", + "762145642166990290864647761179972242614403843424065222377723867096038022172794340849684107193235344521442121855812163792833978437326241529855\n", + "1524291284333980581729295522359944485228807686848130444755447734192076044345588681699368214386470689042884243711624327585667956874652483059711\n", + "3048582568667961163458591044719888970457615373696260889510895468384152088691177363398736428772941378085768487423248655171335913749304966119423\n", + "6097165137335922326917182089439777940915230747392521779021790936768304177382354726797472857545882756171536974846497310342671827498609932238847\n", + "12194330274671844653834364178879555881830461494785043558043581873536608354764709453594945715091765512343073949692994620685343654997219864477695\n", + "24388660549343689307668728357759111763660922989570087116087163747073216709529418907189891430183531024686147899385989241370687309994439728955391\n", + "48777321098687378615337456715518223527321845979140174232174327494146433419058837814379782860367062049372295798771978482741374619988879457910783\n", + "97554642197374757230674913431036447054643691958280348464348654988292866838117675628759565720734124098744591597543956965482749239977758915821567\n", + "195109284394749514461349826862072894109287383916560696928697309976585733676235351257519131441468248197489183195087913930965498479955517831643135\n", + "390218568789499028922699653724145788218574767833121393857394619953171467352470702515038262882936496394978366390175827861930996959911035663286271\n", + "780437137578998057845399307448291576437149535666242787714789239906342934704941405030076525765872992789956732780351655723861993919822071326572543\n", + "1560874275157996115690798614896583152874299071332485575429578479812685869409882810060153051531745985579913465560703311447723987839644142653145087\n", + "3121748550315992231381597229793166305748598142664971150859156959625371738819765620120306103063491971159826931121406622895447975679288285306290175\n", + "6243497100631984462763194459586332611497196285329942301718313919250743477639531240240612206126983942319653862242813245790895951358576570612580351\n", + "12486994201263968925526388919172665222994392570659884603436627838501486955279062480481224412253967884639307724485626491581791902717153141225160703\n", + "24973988402527937851052777838345330445988785141319769206873255677002973910558124960962448824507935769278615448971252983163583805434306282450321407\n", + "49947976805055875702105555676690660891977570282639538413746511354005947821116249921924897649015871538557230897942505966327167610868612564900642815\n", + "99895953610111751404211111353381321783955140565279076827493022708011895642232499843849795298031743077114461795885011932654335221737225129801285631\n", + "199791907220223502808422222706762643567910281130558153654986045416023791284464999687699590596063486154228923591770023865308670443474450259602571263\n", + "399583814440447005616844445413525287135820562261116307309972090832047582568929999375399181192126972308457847183540047730617340886948900519205142527\n", + "799167628880894011233688890827050574271641124522232614619944181664095165137859998750798362384253944616915694367080095461234681773897801038410285055\n", + "1598335257761788022467377781654101148543282249044465229239888363328190330275719997501596724768507889233831388734160190922469363547795602076820570111\n", + "3196670515523576044934755563308202297086564498088930458479776726656380660551439995003193449537015778467662777468320381844938727095591204153641140223\n", + "6393341031047152089869511126616404594173128996177860916959553453312761321102879990006386899074031556935325554936640763689877454191182408307282280447\n", + "12786682062094304179739022253232809188346257992355721833919106906625522642205759980012773798148063113870651109873281527379754908382364816614564560895\n", + "25573364124188608359478044506465618376692515984711443667838213813251045284411519960025547596296126227741302219746563054759509816764729633229129121791\n", + "51146728248377216718956089012931236753385031969422887335676427626502090568823039920051095192592252455482604439493126109519019633529459266458258243583\n", + "102293456496754433437912178025862473506770063938845774671352855253004181137646079840102190385184504910965208878986252219038039267058918532916516487167\n", + "204586912993508866875824356051724947013540127877691549342705710506008362275292159680204380770369009821930417757972504438076078534117837065833032974335\n", + "409173825987017733751648712103449894027080255755383098685411421012016724550584319360408761540738019643860835515945008876152157068235674131666065948671\n", + "818347651974035467503297424206899788054160511510766197370822842024033449101168638720817523081476039287721671031890017752304314136471348263332131897343\n", + "1636695303948070935006594848413799576108321023021532394741645684048066898202337277441635046162952078575443342063780035504608628272942696526664263794687\n" + ] + } + ], + "source": [ + "for i in range(500):\n", + " print(2**i - 1)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.5.1" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/manual_checks/marimo/marimo_complex_good.py b/manual_checks/marimo/marimo_complex_good.py new file mode 100644 index 0000000..f2ce464 --- /dev/null +++ b/manual_checks/marimo/marimo_complex_good.py @@ -0,0 +1,22 @@ +# Source: https://raw.githubusercontent.com/marimo-team/gallery-examples/main/notebooks/math/matrix.py +import marimo + +__generated_with = "0.20.2" +app = marimo.App(width="medium") + +with app.setup: + import marimo as mo + import numpy as np + + +@app.cell +def _(): + matrix = mo.ui.matrix(np.eye(3)) + matrix + return (matrix,) + + +@app.cell +def _(matrix): + np.asarray(matrix.value) + return diff --git a/reports/fixtures-real/generation-summary.json b/reports/fixtures-real/generation-summary.json new file mode 100644 index 0000000..76b3de0 --- /dev/null +++ b/reports/fixtures-real/generation-summary.json @@ -0,0 +1,63 @@ +{ + "processed": 8, + "ok_or_findings": 8, + "hard_failures": [], + "results": [ + { + "file": "marimo_examples_ui_file.py", + "kind": "marimo", + "exit_code": 0, + "report_md": "reports/fixtures-real/marimo/marimo_examples_ui_file/notebook-policy-report.md", + "report_nbom": "reports/fixtures-real/marimo/marimo_examples_ui_file/notebook-policy-nbom.json" + }, + { + "file": "marimo_smoke_cross_cell_md.py", + "kind": "marimo", + "exit_code": 0, + "report_md": "reports/fixtures-real/marimo/marimo_smoke_cross_cell_md/notebook-policy-report.md", + "report_nbom": "reports/fixtures-real/marimo/marimo_smoke_cross_cell_md/notebook-policy-nbom.json" + }, + { + "file": "gallery_matrix.py", + "kind": "marimo", + "exit_code": 0, + "report_md": "reports/fixtures-real/marimo/gallery_matrix/notebook-policy-report.md", + "report_nbom": "reports/fixtures-real/marimo/gallery_matrix/notebook-policy-nbom.json" + }, + { + "file": "gallery_earthquake.py", + "kind": "marimo", + "exit_code": 1, + "report_md": "reports/fixtures-real/marimo/gallery_earthquake/notebook-policy-report.md", + "report_nbom": "reports/fixtures-real/marimo/gallery_earthquake/notebook-policy-nbom.json" + }, + { + "file": "jupyter_running_code_v6_4_7.ipynb", + "kind": "jupyter", + "exit_code": 0, + "report_md": "reports/fixtures-real/jupyter/jupyter_running_code_v6_4_7/notebook-policy-report.md", + "report_nbom": "reports/fixtures-real/jupyter/jupyter_running_code_v6_4_7/notebook-policy-nbom.json" + }, + { + "file": "jupyter_importing_notebooks_v6_4_7.ipynb", + "kind": "jupyter", + "exit_code": 1, + "report_md": "reports/fixtures-real/jupyter/jupyter_importing_notebooks_v6_4_7/notebook-policy-report.md", + "report_nbom": "reports/fixtures-real/jupyter/jupyter_importing_notebooks_v6_4_7/notebook-policy-nbom.json" + }, + { + "file": "jupyter_connecting_qt_console_v6_4_7.ipynb", + "kind": "jupyter", + "exit_code": 1, + "report_md": "reports/fixtures-real/jupyter/jupyter_connecting_qt_console_v6_4_7/notebook-policy-report.md", + "report_nbom": "reports/fixtures-real/jupyter/jupyter_connecting_qt_console_v6_4_7/notebook-policy-nbom.json" + }, + { + "file": "openai_cookbook_assistants_api_overview_42bc4ed.ipynb", + "kind": "jupyter", + "exit_code": 1, + "report_md": "reports/fixtures-real/jupyter/openai_cookbook_assistants_api_overview_42bc4ed/notebook-policy-report.md", + "report_nbom": "reports/fixtures-real/jupyter/openai_cookbook_assistants_api_overview_42bc4ed/notebook-policy-nbom.json" + } + ] +} \ No newline at end of file diff --git a/reports/fixtures-real/jupyter/jupyter_connecting_qt_console_v6_4_7/notebook-policy-nbom.json b/reports/fixtures-real/jupyter/jupyter_connecting_qt_console_v6_4_7/notebook-policy-nbom.json new file mode 100644 index 0000000..1cf1bb7 --- /dev/null +++ b/reports/fixtures-real/jupyter/jupyter_connecting_qt_console_v6_4_7/notebook-policy-nbom.json @@ -0,0 +1,63 @@ +{ + "dependencies": [], + "files": [ + { + "path": "tests/fixtures/real/jupyter_connecting_qt_console_v6_4_7.ipynb", + "type": ".ipynb" + } + ], + "generated_at": "2026-05-30T23:07:51Z", + "runtime_seconds": 0.0003, + "schema_version": "0.1", + "settings": { + "dependency_enrichment": false, + "ignore": [], + "jupyter_source": "ipynb", + "max_cell_lines": 80, + "max_code_cells": 20, + "max_inline_definitions": 3, + "select": [ + "J001", + "J002", + "J011", + "J012", + "J013", + "M001", + "M002", + "M003", + "M004", + "M005", + "M006" + ] + }, + "summary": { + "data_sources": 0, + "dependencies": 0, + "file_references": 0, + "files_scanned": 1, + "http_requests": 0, + "imports": 0, + "violations": 2 + }, + "surface": { + "data_sources": [], + "file_references": [], + "http_requests": [], + "imports": [] + }, + "tool": "pytest-notebook-policy", + "violations": [ + { + "code": "J001", + "line": 1, + "message": "Avoid notebook magics/shell escapes in policy-checked notebooks; prefer Python code for reliable .ipynb\u2194.py sync.", + "path": "tests/fixtures/real/jupyter_connecting_qt_console_v6_4_7.ipynb" + }, + { + "code": "J001", + "line": 5, + "message": "Avoid notebook magics/shell escapes in policy-checked notebooks; prefer Python code for reliable .ipynb\u2194.py sync.", + "path": "tests/fixtures/real/jupyter_connecting_qt_console_v6_4_7.ipynb" + } + ] +} diff --git a/reports/fixtures-real/jupyter/jupyter_connecting_qt_console_v6_4_7/notebook-policy-report.md b/reports/fixtures-real/jupyter/jupyter_connecting_qt_console_v6_4_7/notebook-policy-report.md new file mode 100644 index 0000000..6497520 --- /dev/null +++ b/reports/fixtures-real/jupyter/jupyter_connecting_qt_console_v6_4_7/notebook-policy-report.md @@ -0,0 +1,68 @@ +## 🧪 Notebook policy report + +| Metric | Value | +|---|---| +| Status | ⚠️ Findings | +| Generated | `2026-05-30 23:07:51 UTC` | +| Runtime | `0.00s` | +| Files scanned | `1` | +| Violations | `2` | + +### 🔎 Findings + +| File | Rule | Line | What | Why this is undesirable | Suggested fix | +|---|---|---:|---|---|---| +| `tests/fixtures/real/jupyter_connecting_qt_console_v6_4_7.ipynb` | [`J001`](https://github.com/DataBooth/pytest-notebook-policy/blob/main/docs/RULES.md#j001) | `1` | Avoid notebook magics/shell escapes in policy-checked notebooks; prefer Python code for reliable .ipynb↔.py sync. | Magics and shell escapes often reduce portability and reproducibility. | Replace magics/shell escapes with explicit Python equivalents where possible. | +| `tests/fixtures/real/jupyter_connecting_qt_console_v6_4_7.ipynb` | [`J001`](https://github.com/DataBooth/pytest-notebook-policy/blob/main/docs/RULES.md#j001) | `5` | Avoid notebook magics/shell escapes in policy-checked notebooks; prefer Python code for reliable .ipynb↔.py sync. | Magics and shell escapes often reduce portability and reproducibility. | Replace magics/shell escapes with explicit Python equivalents where possible. | + +### 🧭 Notebook surface summary + +| Category | Count | +|---|---:| +| Key imports | `0` | +| File references | `0` | +| HTTP requests | `0` | +| Data sources | `0` | + +#### Key imports + +| Import | +|---| +| `(none)` | + +#### File references + +| File | Reference | +|---|---| +| `(none)` | `(none)` | + +#### HTTP requests + +| File | Endpoint | +|---|---| +| `(none)` | `(none)` | + +#### Data sources + +| File | Source | +|---|---| +| `(none)` | `(none)` | + +## Appendix A — Configuration + +| Setting | Value | +|---|---| +| Enabled rules/prefixes | `J001, J002, J011, J012, J013, M001, M002, M003, M004, M005, M006` | +| Ignored rules/prefixes | `(none)` | +| Jupyter source mode | `ipynb` | +| Jupyter max code cells | `20` | +| Jupyter max cell lines | `80` | +| Jupyter max inline definitions | `3` | +| Dependency enrichment | `False` | +| NBOM output path | `/Users/mjboothaus/code/github/databooth/pytest-notebook-policy/reports/fixtures-real/jupyter/jupyter_connecting_qt_console_v6_4_7/notebook-policy-nbom.json` | + +## Appendix B — Scanned files + +| File | Type | +|---|---| +| `tests/fixtures/real/jupyter_connecting_qt_console_v6_4_7.ipynb` | `.ipynb` | diff --git a/reports/fixtures-real/jupyter/jupyter_importing_notebooks_v6_4_7/notebook-policy-nbom.json b/reports/fixtures-real/jupyter/jupyter_importing_notebooks_v6_4_7/notebook-policy-nbom.json new file mode 100644 index 0000000..ff00e08 --- /dev/null +++ b/reports/fixtures-real/jupyter/jupyter_importing_notebooks_v6_4_7/notebook-policy-nbom.json @@ -0,0 +1,112 @@ +{ + "dependencies": [], + "files": [ + { + "path": "tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb", + "type": ".ipynb" + } + ], + "generated_at": "2026-05-30T23:07:51Z", + "runtime_seconds": 0.0022, + "schema_version": "0.1", + "settings": { + "dependency_enrichment": false, + "ignore": [], + "jupyter_source": "ipynb", + "max_cell_lines": 80, + "max_code_cells": 20, + "max_inline_definitions": 3, + "select": [ + "J001", + "J002", + "J011", + "J012", + "J013", + "M001", + "M002", + "M003", + "M004", + "M005", + "M006" + ] + }, + "summary": { + "data_sources": 0, + "dependencies": 0, + "file_references": 8, + "files_scanned": 1, + "http_requests": 0, + "imports": 9, + "violations": 3 + }, + "surface": { + "data_sources": [], + "file_references": [ + { + "path": "tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb", + "value": ".ipynb" + }, + { + "path": "tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb", + "value": "

%s cell

" + }, + { + "path": "tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb", + "value": "
%s
" + }, + { + "path": "tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb", + "value": "" + }, + { + "path": "tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb", + "value": "find a notebook, given its fully qualified name and an optional path\n \n This turns \"foo.bar\" into \"foo/bar.ipynb\"\n and tries turning \"Foo_Bar\" into \"Foo Bar\" if Foo_Bar\n does not exist." + }, + { + "path": "tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb", + "value": "inside_ipython.ipynb" + }, + { + "path": "tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb", + "value": "mynotebook.ipynb" + }, + { + "path": "tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb", + "value": "other.ipynb" + } + ], + "http_requests": [], + "imports": [ + "IPython", + "io", + "nbformat", + "nbpackage", + "os", + "pygments", + "shutil", + "sys", + "types" + ] + }, + "tool": "pytest-notebook-policy", + "violations": [ + { + "code": "J011", + "line": 1, + "message": "Add a top-of-notebook parameter/configuration cell (within the first three code cells) to make runs reproducible and easier to automate.", + "path": "tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb" + }, + { + "code": "J013", + "line": 7, + "message": "Notebook defines 4 functions/classes inline; keep at most 3 and extract reusable logic into importable modules.", + "path": "tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb" + }, + { + "code": "J001", + "line": 108, + "message": "Avoid notebook magics/shell escapes in policy-checked notebooks; prefer Python code for reliable .ipynb\u2194.py sync.", + "path": "tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb" + } + ] +} diff --git a/reports/fixtures-real/jupyter/jupyter_importing_notebooks_v6_4_7/notebook-policy-report.md b/reports/fixtures-real/jupyter/jupyter_importing_notebooks_v6_4_7/notebook-policy-report.md new file mode 100644 index 0000000..690f8b8 --- /dev/null +++ b/reports/fixtures-real/jupyter/jupyter_importing_notebooks_v6_4_7/notebook-policy-report.md @@ -0,0 +1,84 @@ +## 🧪 Notebook policy report + +| Metric | Value | +|---|---| +| Status | ⚠️ Findings | +| Generated | `2026-05-30 23:07:51 UTC` | +| Runtime | `0.00s` | +| Files scanned | `1` | +| Violations | `3` | + +### 🔎 Findings + +| File | Rule | Line | What | Why this is undesirable | Suggested fix | +|---|---|---:|---|---|---| +| `tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb` | [`J011`](https://github.com/DataBooth/pytest-notebook-policy/blob/main/docs/RULES.md#j011) | `1` | Add a top-of-notebook parameter/configuration cell (within the first three code cells) to make runs reproducible and easier to automate. | A clear parameter cell improves reproducibility and automation. | Add a top-of-notebook configuration/parameter code cell within the first three code cells. | +| `tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb` | [`J013`](https://github.com/DataBooth/pytest-notebook-policy/blob/main/docs/RULES.md#j013) | `7` | Notebook defines 4 functions/classes inline; keep at most 3 and extract reusable logic into importable modules. | Too many inline definitions blur notebook narrative and reusable logic boundaries. | Extract reusable functions/classes into modules and keep notebooks focused on orchestration. | +| `tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb` | [`J001`](https://github.com/DataBooth/pytest-notebook-policy/blob/main/docs/RULES.md#j001) | `108` | Avoid notebook magics/shell escapes in policy-checked notebooks; prefer Python code for reliable .ipynb↔.py sync. | Magics and shell escapes often reduce portability and reproducibility. | Replace magics/shell escapes with explicit Python equivalents where possible. | + +### 🧭 Notebook surface summary + +| Category | Count | +|---|---:| +| Key imports | `9` | +| File references | `8` | +| HTTP requests | `0` | +| Data sources | `0` | + +#### Key imports + +| Import | +|---| +| `IPython` | +| `io` | +| `nbformat` | +| `nbpackage` | +| `os` | +| `pygments` | +| `shutil` | +| `sys` | +| `types` | + +#### File references + +| File | Reference | +|---|---| +| `tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb` | `.ipynb` | +| `tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb` | `

%s cell

` | +| `tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb` | `
%s
` | +| `tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb` | `` | +| `tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb` | `find a notebook, given its fully qualified name and an optional path This turns "foo.bar" into "foo/bar.ipynb" and tries turning "Foo_Bar" into "Foo Bar" if Foo_Bar does not exist.` | +| `tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb` | `inside_ipython.ipynb` | +| `tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb` | `mynotebook.ipynb` | +| `tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb` | `other.ipynb` | + +#### HTTP requests + +| File | Endpoint | +|---|---| +| `(none)` | `(none)` | + +#### Data sources + +| File | Source | +|---|---| +| `(none)` | `(none)` | + +## Appendix A — Configuration + +| Setting | Value | +|---|---| +| Enabled rules/prefixes | `J001, J002, J011, J012, J013, M001, M002, M003, M004, M005, M006` | +| Ignored rules/prefixes | `(none)` | +| Jupyter source mode | `ipynb` | +| Jupyter max code cells | `20` | +| Jupyter max cell lines | `80` | +| Jupyter max inline definitions | `3` | +| Dependency enrichment | `False` | +| NBOM output path | `/Users/mjboothaus/code/github/databooth/pytest-notebook-policy/reports/fixtures-real/jupyter/jupyter_importing_notebooks_v6_4_7/notebook-policy-nbom.json` | + +## Appendix B — Scanned files + +| File | Type | +|---|---| +| `tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb` | `.ipynb` | diff --git a/reports/fixtures-real/jupyter/jupyter_running_code_v6_4_7/notebook-policy-nbom.json b/reports/fixtures-real/jupyter/jupyter_running_code_v6_4_7/notebook-policy-nbom.json new file mode 100644 index 0000000..40ec511 --- /dev/null +++ b/reports/fixtures-real/jupyter/jupyter_running_code_v6_4_7/notebook-policy-nbom.json @@ -0,0 +1,55 @@ +{ + "dependencies": [], + "files": [ + { + "path": "tests/fixtures/real/jupyter_running_code_v6_4_7.ipynb", + "type": ".ipynb" + } + ], + "generated_at": "2026-05-30T23:07:51Z", + "runtime_seconds": 0.0007, + "schema_version": "0.1", + "settings": { + "dependency_enrichment": false, + "ignore": [], + "jupyter_source": "ipynb", + "max_cell_lines": 80, + "max_code_cells": 20, + "max_inline_definitions": 3, + "select": [ + "J001", + "J002", + "J011", + "J012", + "J013", + "M001", + "M002", + "M003", + "M004", + "M005", + "M006" + ] + }, + "summary": { + "data_sources": 0, + "dependencies": 0, + "file_references": 0, + "files_scanned": 1, + "http_requests": 0, + "imports": 4, + "violations": 0 + }, + "surface": { + "data_sources": [], + "file_references": [], + "http_requests": [], + "imports": [ + "__future__", + "ctypes", + "sys", + "time" + ] + }, + "tool": "pytest-notebook-policy", + "violations": [] +} diff --git a/reports/fixtures-real/jupyter/jupyter_running_code_v6_4_7/notebook-policy-report.md b/reports/fixtures-real/jupyter/jupyter_running_code_v6_4_7/notebook-policy-report.md new file mode 100644 index 0000000..16644d3 --- /dev/null +++ b/reports/fixtures-real/jupyter/jupyter_running_code_v6_4_7/notebook-policy-report.md @@ -0,0 +1,68 @@ +## 🧪 Notebook policy report + +| Metric | Value | +|---|---| +| Status | ✅ Pass | +| Generated | `2026-05-30 23:07:51 UTC` | +| Runtime | `0.00s` | +| Files scanned | `1` | +| Violations | `0` | + +### 🔎 Findings + +No notebook policy violations were found. + +### 🧭 Notebook surface summary + +| Category | Count | +|---|---:| +| Key imports | `4` | +| File references | `0` | +| HTTP requests | `0` | +| Data sources | `0` | + +#### Key imports + +| Import | +|---| +| `__future__` | +| `ctypes` | +| `sys` | +| `time` | + +#### File references + +| File | Reference | +|---|---| +| `(none)` | `(none)` | + +#### HTTP requests + +| File | Endpoint | +|---|---| +| `(none)` | `(none)` | + +#### Data sources + +| File | Source | +|---|---| +| `(none)` | `(none)` | + +## Appendix A — Configuration + +| Setting | Value | +|---|---| +| Enabled rules/prefixes | `J001, J002, J011, J012, J013, M001, M002, M003, M004, M005, M006` | +| Ignored rules/prefixes | `(none)` | +| Jupyter source mode | `ipynb` | +| Jupyter max code cells | `20` | +| Jupyter max cell lines | `80` | +| Jupyter max inline definitions | `3` | +| Dependency enrichment | `False` | +| NBOM output path | `/Users/mjboothaus/code/github/databooth/pytest-notebook-policy/reports/fixtures-real/jupyter/jupyter_running_code_v6_4_7/notebook-policy-nbom.json` | + +## Appendix B — Scanned files + +| File | Type | +|---|---| +| `tests/fixtures/real/jupyter_running_code_v6_4_7.ipynb` | `.ipynb` | diff --git a/reports/fixtures-real/jupyter/openai_cookbook_assistants_api_overview_42bc4ed/notebook-policy-nbom.json b/reports/fixtures-real/jupyter/openai_cookbook_assistants_api_overview_42bc4ed/notebook-policy-nbom.json new file mode 100644 index 0000000..679ba25 --- /dev/null +++ b/reports/fixtures-real/jupyter/openai_cookbook_assistants_api_overview_42bc4ed/notebook-policy-nbom.json @@ -0,0 +1,91 @@ +{ + "dependencies": [], + "files": [ + { + "path": "tests/fixtures/real/openai_cookbook_assistants_api_overview_42bc4ed.ipynb", + "type": ".ipynb" + } + ], + "generated_at": "2026-05-30T23:07:52Z", + "runtime_seconds": 0.0042, + "schema_version": "0.1", + "settings": { + "dependency_enrichment": false, + "ignore": [], + "jupyter_source": "ipynb", + "max_cell_lines": 80, + "max_code_cells": 20, + "max_inline_definitions": 3, + "select": [ + "J001", + "J002", + "J011", + "J012", + "J013", + "M001", + "M002", + "M003", + "M004", + "M005", + "M006" + ] + }, + "summary": { + "data_sources": 0, + "dependencies": 0, + "file_references": 1, + "files_scanned": 1, + "http_requests": 0, + "imports": 4, + "violations": 5 + }, + "surface": { + "data_sources": [], + "file_references": [ + { + "path": "tests/fixtures/real/openai_cookbook_assistants_api_overview_42bc4ed.ipynb", + "value": "data/language_models_are_unsupervised_multitask_learners.pdf" + } + ], + "http_requests": [], + "imports": [ + "json", + "openai", + "os", + "time" + ] + }, + "tool": "pytest-notebook-policy", + "violations": [ + { + "code": "J001", + "line": 1, + "message": "Avoid notebook magics/shell escapes in policy-checked notebooks; prefer Python code for reliable .ipynb\u2194.py sync.", + "path": "tests/fixtures/real/openai_cookbook_assistants_api_overview_42bc4ed.ipynb" + }, + { + "code": "J011", + "line": 1, + "message": "Add a top-of-notebook parameter/configuration cell (within the first three code cells) to make runs reproducible and easier to automate.", + "path": "tests/fixtures/real/openai_cookbook_assistants_api_overview_42bc4ed.ipynb" + }, + { + "code": "J012", + "line": 1, + "message": "Notebook has 31 code cells; keep notebooks under 20 code cells or split into smaller notebooks/modules.", + "path": "tests/fixtures/real/openai_cookbook_assistants_api_overview_42bc4ed.ipynb" + }, + { + "code": "J001", + "line": 3, + "message": "Avoid notebook magics/shell escapes in policy-checked notebooks; prefer Python code for reliable .ipynb\u2194.py sync.", + "path": "tests/fixtures/real/openai_cookbook_assistants_api_overview_42bc4ed.ipynb" + }, + { + "code": "J013", + "line": 7, + "message": "Notebook defines 10 functions/classes inline; keep at most 3 and extract reusable logic into importable modules.", + "path": "tests/fixtures/real/openai_cookbook_assistants_api_overview_42bc4ed.ipynb" + } + ] +} diff --git a/reports/fixtures-real/jupyter/openai_cookbook_assistants_api_overview_42bc4ed/notebook-policy-report.md b/reports/fixtures-real/jupyter/openai_cookbook_assistants_api_overview_42bc4ed/notebook-policy-report.md new file mode 100644 index 0000000..7d60ce8 --- /dev/null +++ b/reports/fixtures-real/jupyter/openai_cookbook_assistants_api_overview_42bc4ed/notebook-policy-report.md @@ -0,0 +1,74 @@ +## 🧪 Notebook policy report + +| Metric | Value | +|---|---| +| Status | ⚠️ Findings | +| Generated | `2026-05-30 23:07:52 UTC` | +| Runtime | `0.00s` | +| Files scanned | `1` | +| Violations | `5` | + +### 🔎 Findings + +| File | Rule | Line | What | Why this is undesirable | Suggested fix | +|---|---|---:|---|---|---| +| `tests/fixtures/real/openai_cookbook_assistants_api_overview_42bc4ed.ipynb` | [`J001`](https://github.com/DataBooth/pytest-notebook-policy/blob/main/docs/RULES.md#j001) | `1` | Avoid notebook magics/shell escapes in policy-checked notebooks; prefer Python code for reliable .ipynb↔.py sync. | Magics and shell escapes often reduce portability and reproducibility. | Replace magics/shell escapes with explicit Python equivalents where possible. | +| `tests/fixtures/real/openai_cookbook_assistants_api_overview_42bc4ed.ipynb` | [`J011`](https://github.com/DataBooth/pytest-notebook-policy/blob/main/docs/RULES.md#j011) | `1` | Add a top-of-notebook parameter/configuration cell (within the first three code cells) to make runs reproducible and easier to automate. | A clear parameter cell improves reproducibility and automation. | Add a top-of-notebook configuration/parameter code cell within the first three code cells. | +| `tests/fixtures/real/openai_cookbook_assistants_api_overview_42bc4ed.ipynb` | [`J012`](https://github.com/DataBooth/pytest-notebook-policy/blob/main/docs/RULES.md#j012) | `1` | Notebook has 31 code cells; keep notebooks under 20 code cells or split into smaller notebooks/modules. | Large notebooks or very long cells are harder to review and maintain. | Split notebooks/cells into smaller units and move complex logic into importable modules. | +| `tests/fixtures/real/openai_cookbook_assistants_api_overview_42bc4ed.ipynb` | [`J001`](https://github.com/DataBooth/pytest-notebook-policy/blob/main/docs/RULES.md#j001) | `3` | Avoid notebook magics/shell escapes in policy-checked notebooks; prefer Python code for reliable .ipynb↔.py sync. | Magics and shell escapes often reduce portability and reproducibility. | Replace magics/shell escapes with explicit Python equivalents where possible. | +| `tests/fixtures/real/openai_cookbook_assistants_api_overview_42bc4ed.ipynb` | [`J013`](https://github.com/DataBooth/pytest-notebook-policy/blob/main/docs/RULES.md#j013) | `7` | Notebook defines 10 functions/classes inline; keep at most 3 and extract reusable logic into importable modules. | Too many inline definitions blur notebook narrative and reusable logic boundaries. | Extract reusable functions/classes into modules and keep notebooks focused on orchestration. | + +### 🧭 Notebook surface summary + +| Category | Count | +|---|---:| +| Key imports | `4` | +| File references | `1` | +| HTTP requests | `0` | +| Data sources | `0` | + +#### Key imports + +| Import | +|---| +| `json` | +| `openai` | +| `os` | +| `time` | + +#### File references + +| File | Reference | +|---|---| +| `tests/fixtures/real/openai_cookbook_assistants_api_overview_42bc4ed.ipynb` | `data/language_models_are_unsupervised_multitask_learners.pdf` | + +#### HTTP requests + +| File | Endpoint | +|---|---| +| `(none)` | `(none)` | + +#### Data sources + +| File | Source | +|---|---| +| `(none)` | `(none)` | + +## Appendix A — Configuration + +| Setting | Value | +|---|---| +| Enabled rules/prefixes | `J001, J002, J011, J012, J013, M001, M002, M003, M004, M005, M006` | +| Ignored rules/prefixes | `(none)` | +| Jupyter source mode | `ipynb` | +| Jupyter max code cells | `20` | +| Jupyter max cell lines | `80` | +| Jupyter max inline definitions | `3` | +| Dependency enrichment | `False` | +| NBOM output path | `/Users/mjboothaus/code/github/databooth/pytest-notebook-policy/reports/fixtures-real/jupyter/openai_cookbook_assistants_api_overview_42bc4ed/notebook-policy-nbom.json` | + +## Appendix B — Scanned files + +| File | Type | +|---|---| +| `tests/fixtures/real/openai_cookbook_assistants_api_overview_42bc4ed.ipynb` | `.ipynb` | diff --git a/reports/fixtures-real/marimo/gallery_earthquake/notebook-policy-nbom.json b/reports/fixtures-real/marimo/gallery_earthquake/notebook-policy-nbom.json new file mode 100644 index 0000000..7d46099 --- /dev/null +++ b/reports/fixtures-real/marimo/gallery_earthquake/notebook-policy-nbom.json @@ -0,0 +1,71 @@ +{ + "dependencies": [], + "files": [ + { + "path": "tests/fixtures/real/gallery_earthquake.py", + "type": ".py" + } + ], + "generated_at": "2026-05-30T23:07:51Z", + "runtime_seconds": 0.0029, + "schema_version": "0.1", + "settings": { + "dependency_enrichment": false, + "ignore": [], + "jupyter_source": "ipynb", + "max_cell_lines": 80, + "max_code_cells": 20, + "max_inline_definitions": 3, + "select": [ + "J001", + "J002", + "J011", + "J012", + "J013", + "M001", + "M002", + "M003", + "M004", + "M005", + "M006" + ] + }, + "summary": { + "data_sources": 0, + "dependencies": 0, + "file_references": 0, + "files_scanned": 1, + "http_requests": 1, + "imports": 2, + "violations": 2 + }, + "surface": { + "data_sources": [], + "file_references": [], + "http_requests": [ + { + "path": "tests/fixtures/real/gallery_earthquake.py", + "value": "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.geojson" + } + ], + "imports": [ + "marimo", + "openlayers" + ] + }, + "tool": "pytest-notebook-policy", + "violations": [ + { + "code": "M001", + "line": 53, + "message": "Prefer reactive dependencies over on_change handlers.", + "path": "tests/fixtures/real/gallery_earthquake.py" + }, + { + "code": "M001", + "line": 66, + "message": "Prefer reactive dependencies over on_change handlers.", + "path": "tests/fixtures/real/gallery_earthquake.py" + } + ] +} diff --git a/reports/fixtures-real/marimo/gallery_earthquake/notebook-policy-report.md b/reports/fixtures-real/marimo/gallery_earthquake/notebook-policy-report.md new file mode 100644 index 0000000..3099da0 --- /dev/null +++ b/reports/fixtures-real/marimo/gallery_earthquake/notebook-policy-report.md @@ -0,0 +1,69 @@ +## 🧪 Notebook policy report + +| Metric | Value | +|---|---| +| Status | ⚠️ Findings | +| Generated | `2026-05-30 23:07:51 UTC` | +| Runtime | `0.00s` | +| Files scanned | `1` | +| Violations | `2` | + +### 🔎 Findings + +| File | Rule | Line | What | Why this is undesirable | Suggested fix | +|---|---|---:|---|---|---| +| `tests/fixtures/real/gallery_earthquake.py` | [`M001`](https://github.com/DataBooth/pytest-notebook-policy/blob/main/docs/RULES.md#m001) | `53` | Prefer reactive dependencies over on_change handlers. | Reactive dataflow is easier to reason about than callback-style imperative updates. | Prefer derived-cell dependencies over `on_change` callbacks for state propagation. | +| `tests/fixtures/real/gallery_earthquake.py` | [`M001`](https://github.com/DataBooth/pytest-notebook-policy/blob/main/docs/RULES.md#m001) | `66` | Prefer reactive dependencies over on_change handlers. | Reactive dataflow is easier to reason about than callback-style imperative updates. | Prefer derived-cell dependencies over `on_change` callbacks for state propagation. | + +### 🧭 Notebook surface summary + +| Category | Count | +|---|---:| +| Key imports | `2` | +| File references | `0` | +| HTTP requests | `1` | +| Data sources | `0` | + +#### Key imports + +| Import | +|---| +| `marimo` | +| `openlayers` | + +#### File references + +| File | Reference | +|---|---| +| `(none)` | `(none)` | + +#### HTTP requests + +| File | Endpoint | +|---|---| +| `tests/fixtures/real/gallery_earthquake.py` | `https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.geojson` | + +#### Data sources + +| File | Source | +|---|---| +| `(none)` | `(none)` | + +## Appendix A — Configuration + +| Setting | Value | +|---|---| +| Enabled rules/prefixes | `J001, J002, J011, J012, J013, M001, M002, M003, M004, M005, M006` | +| Ignored rules/prefixes | `(none)` | +| Jupyter source mode | `ipynb` | +| Jupyter max code cells | `20` | +| Jupyter max cell lines | `80` | +| Jupyter max inline definitions | `3` | +| Dependency enrichment | `False` | +| NBOM output path | `/Users/mjboothaus/code/github/databooth/pytest-notebook-policy/reports/fixtures-real/marimo/gallery_earthquake/notebook-policy-nbom.json` | + +## Appendix B — Scanned files + +| File | Type | +|---|---| +| `tests/fixtures/real/gallery_earthquake.py` | `.py` | diff --git a/reports/fixtures-real/marimo/gallery_matrix/notebook-policy-nbom.json b/reports/fixtures-real/marimo/gallery_matrix/notebook-policy-nbom.json new file mode 100644 index 0000000..c34abad --- /dev/null +++ b/reports/fixtures-real/marimo/gallery_matrix/notebook-policy-nbom.json @@ -0,0 +1,53 @@ +{ + "dependencies": [], + "files": [ + { + "path": "tests/fixtures/real/gallery_matrix.py", + "type": ".py" + } + ], + "generated_at": "2026-05-30T23:07:51Z", + "runtime_seconds": 0.0016, + "schema_version": "0.1", + "settings": { + "dependency_enrichment": false, + "ignore": [], + "jupyter_source": "ipynb", + "max_cell_lines": 80, + "max_code_cells": 20, + "max_inline_definitions": 3, + "select": [ + "J001", + "J002", + "J011", + "J012", + "J013", + "M001", + "M002", + "M003", + "M004", + "M005", + "M006" + ] + }, + "summary": { + "data_sources": 0, + "dependencies": 0, + "file_references": 0, + "files_scanned": 1, + "http_requests": 0, + "imports": 2, + "violations": 0 + }, + "surface": { + "data_sources": [], + "file_references": [], + "http_requests": [], + "imports": [ + "marimo", + "numpy" + ] + }, + "tool": "pytest-notebook-policy", + "violations": [] +} diff --git a/reports/fixtures-real/marimo/gallery_matrix/notebook-policy-report.md b/reports/fixtures-real/marimo/gallery_matrix/notebook-policy-report.md new file mode 100644 index 0000000..5a6447e --- /dev/null +++ b/reports/fixtures-real/marimo/gallery_matrix/notebook-policy-report.md @@ -0,0 +1,66 @@ +## 🧪 Notebook policy report + +| Metric | Value | +|---|---| +| Status | ✅ Pass | +| Generated | `2026-05-30 23:07:51 UTC` | +| Runtime | `0.00s` | +| Files scanned | `1` | +| Violations | `0` | + +### 🔎 Findings + +No notebook policy violations were found. + +### 🧭 Notebook surface summary + +| Category | Count | +|---|---:| +| Key imports | `2` | +| File references | `0` | +| HTTP requests | `0` | +| Data sources | `0` | + +#### Key imports + +| Import | +|---| +| `marimo` | +| `numpy` | + +#### File references + +| File | Reference | +|---|---| +| `(none)` | `(none)` | + +#### HTTP requests + +| File | Endpoint | +|---|---| +| `(none)` | `(none)` | + +#### Data sources + +| File | Source | +|---|---| +| `(none)` | `(none)` | + +## Appendix A — Configuration + +| Setting | Value | +|---|---| +| Enabled rules/prefixes | `J001, J002, J011, J012, J013, M001, M002, M003, M004, M005, M006` | +| Ignored rules/prefixes | `(none)` | +| Jupyter source mode | `ipynb` | +| Jupyter max code cells | `20` | +| Jupyter max cell lines | `80` | +| Jupyter max inline definitions | `3` | +| Dependency enrichment | `False` | +| NBOM output path | `/Users/mjboothaus/code/github/databooth/pytest-notebook-policy/reports/fixtures-real/marimo/gallery_matrix/notebook-policy-nbom.json` | + +## Appendix B — Scanned files + +| File | Type | +|---|---| +| `tests/fixtures/real/gallery_matrix.py` | `.py` | diff --git a/reports/fixtures-real/marimo/marimo_examples_ui_file/notebook-policy-nbom.json b/reports/fixtures-real/marimo/marimo_examples_ui_file/notebook-policy-nbom.json new file mode 100644 index 0000000..603a221 --- /dev/null +++ b/reports/fixtures-real/marimo/marimo_examples_ui_file/notebook-policy-nbom.json @@ -0,0 +1,52 @@ +{ + "dependencies": [], + "files": [ + { + "path": "tests/fixtures/real/marimo_examples_ui_file.py", + "type": ".py" + } + ], + "generated_at": "2026-05-30T23:07:51Z", + "runtime_seconds": 0.0008, + "schema_version": "0.1", + "settings": { + "dependency_enrichment": false, + "ignore": [], + "jupyter_source": "ipynb", + "max_cell_lines": 80, + "max_code_cells": 20, + "max_inline_definitions": 3, + "select": [ + "J001", + "J002", + "J011", + "J012", + "J013", + "M001", + "M002", + "M003", + "M004", + "M005", + "M006" + ] + }, + "summary": { + "data_sources": 0, + "dependencies": 0, + "file_references": 0, + "files_scanned": 1, + "http_requests": 0, + "imports": 1, + "violations": 0 + }, + "surface": { + "data_sources": [], + "file_references": [], + "http_requests": [], + "imports": [ + "marimo" + ] + }, + "tool": "pytest-notebook-policy", + "violations": [] +} diff --git a/reports/fixtures-real/marimo/marimo_examples_ui_file/notebook-policy-report.md b/reports/fixtures-real/marimo/marimo_examples_ui_file/notebook-policy-report.md new file mode 100644 index 0000000..3fe33b3 --- /dev/null +++ b/reports/fixtures-real/marimo/marimo_examples_ui_file/notebook-policy-report.md @@ -0,0 +1,65 @@ +## 🧪 Notebook policy report + +| Metric | Value | +|---|---| +| Status | ✅ Pass | +| Generated | `2026-05-30 23:07:51 UTC` | +| Runtime | `0.00s` | +| Files scanned | `1` | +| Violations | `0` | + +### 🔎 Findings + +No notebook policy violations were found. + +### 🧭 Notebook surface summary + +| Category | Count | +|---|---:| +| Key imports | `1` | +| File references | `0` | +| HTTP requests | `0` | +| Data sources | `0` | + +#### Key imports + +| Import | +|---| +| `marimo` | + +#### File references + +| File | Reference | +|---|---| +| `(none)` | `(none)` | + +#### HTTP requests + +| File | Endpoint | +|---|---| +| `(none)` | `(none)` | + +#### Data sources + +| File | Source | +|---|---| +| `(none)` | `(none)` | + +## Appendix A — Configuration + +| Setting | Value | +|---|---| +| Enabled rules/prefixes | `J001, J002, J011, J012, J013, M001, M002, M003, M004, M005, M006` | +| Ignored rules/prefixes | `(none)` | +| Jupyter source mode | `ipynb` | +| Jupyter max code cells | `20` | +| Jupyter max cell lines | `80` | +| Jupyter max inline definitions | `3` | +| Dependency enrichment | `False` | +| NBOM output path | `/Users/mjboothaus/code/github/databooth/pytest-notebook-policy/reports/fixtures-real/marimo/marimo_examples_ui_file/notebook-policy-nbom.json` | + +## Appendix B — Scanned files + +| File | Type | +|---|---| +| `tests/fixtures/real/marimo_examples_ui_file.py` | `.py` | diff --git a/reports/fixtures-real/marimo/marimo_smoke_cross_cell_md/notebook-policy-nbom.json b/reports/fixtures-real/marimo/marimo_smoke_cross_cell_md/notebook-policy-nbom.json new file mode 100644 index 0000000..50a9fdc --- /dev/null +++ b/reports/fixtures-real/marimo/marimo_smoke_cross_cell_md/notebook-policy-nbom.json @@ -0,0 +1,52 @@ +{ + "dependencies": [], + "files": [ + { + "path": "tests/fixtures/real/marimo_smoke_cross_cell_md.py", + "type": ".py" + } + ], + "generated_at": "2026-05-30T23:07:51Z", + "runtime_seconds": 0.0017, + "schema_version": "0.1", + "settings": { + "dependency_enrichment": false, + "ignore": [], + "jupyter_source": "ipynb", + "max_cell_lines": 80, + "max_code_cells": 20, + "max_inline_definitions": 3, + "select": [ + "J001", + "J002", + "J011", + "J012", + "J013", + "M001", + "M002", + "M003", + "M004", + "M005", + "M006" + ] + }, + "summary": { + "data_sources": 0, + "dependencies": 0, + "file_references": 0, + "files_scanned": 1, + "http_requests": 0, + "imports": 1, + "violations": 0 + }, + "surface": { + "data_sources": [], + "file_references": [], + "http_requests": [], + "imports": [ + "marimo" + ] + }, + "tool": "pytest-notebook-policy", + "violations": [] +} diff --git a/reports/fixtures-real/marimo/marimo_smoke_cross_cell_md/notebook-policy-report.md b/reports/fixtures-real/marimo/marimo_smoke_cross_cell_md/notebook-policy-report.md new file mode 100644 index 0000000..e7a9b70 --- /dev/null +++ b/reports/fixtures-real/marimo/marimo_smoke_cross_cell_md/notebook-policy-report.md @@ -0,0 +1,65 @@ +## 🧪 Notebook policy report + +| Metric | Value | +|---|---| +| Status | ✅ Pass | +| Generated | `2026-05-30 23:07:51 UTC` | +| Runtime | `0.00s` | +| Files scanned | `1` | +| Violations | `0` | + +### 🔎 Findings + +No notebook policy violations were found. + +### 🧭 Notebook surface summary + +| Category | Count | +|---|---:| +| Key imports | `1` | +| File references | `0` | +| HTTP requests | `0` | +| Data sources | `0` | + +#### Key imports + +| Import | +|---| +| `marimo` | + +#### File references + +| File | Reference | +|---|---| +| `(none)` | `(none)` | + +#### HTTP requests + +| File | Endpoint | +|---|---| +| `(none)` | `(none)` | + +#### Data sources + +| File | Source | +|---|---| +| `(none)` | `(none)` | + +## Appendix A — Configuration + +| Setting | Value | +|---|---| +| Enabled rules/prefixes | `J001, J002, J011, J012, J013, M001, M002, M003, M004, M005, M006` | +| Ignored rules/prefixes | `(none)` | +| Jupyter source mode | `ipynb` | +| Jupyter max code cells | `20` | +| Jupyter max cell lines | `80` | +| Jupyter max inline definitions | `3` | +| Dependency enrichment | `False` | +| NBOM output path | `/Users/mjboothaus/code/github/databooth/pytest-notebook-policy/reports/fixtures-real/marimo/marimo_smoke_cross_cell_md/notebook-policy-nbom.json` | + +## Appendix B — Scanned files + +| File | Type | +|---|---| +| `tests/fixtures/real/marimo_smoke_cross_cell_md.py` | `.py` | diff --git a/scripts/clean_release_artifacts.py b/scripts/clean_release_artifacts.py new file mode 100644 index 0000000..bef2413 --- /dev/null +++ b/scripts/clean_release_artifacts.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from pathlib import Path +import shutil + + +REPO_ROOT = Path(__file__).resolve().parents[1] +BUILD_PATHS = ("build", "dist") + + +def _remove_if_exists(path: Path) -> None: + if not path.exists(): + return + if path.is_dir(): + shutil.rmtree(path) + return + path.unlink() + + +def main() -> int: + for relative in BUILD_PATHS: + _remove_if_exists(REPO_ROOT / relative) + + for egg_info in REPO_ROOT.glob("*.egg-info"): + _remove_if_exists(egg_info) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/create_manual_examples.py b/scripts/create_manual_examples.py new file mode 100644 index 0000000..52a69e3 --- /dev/null +++ b/scripts/create_manual_examples.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +import shutil + + +@dataclass(frozen=True) +class FixtureCopy: + source_relative_path: str + destination_relative_path: str + + +REPO_ROOT = Path(__file__).resolve().parents[1] + +FIXTURE_COPIES = ( + FixtureCopy( + source_relative_path="tests/fixtures/real/gallery_matrix.py", + destination_relative_path="manual_checks/marimo/marimo_complex_good.py", + ), + FixtureCopy( + source_relative_path="tests/fixtures/real/jupyter_running_code_v6_4_7.ipynb", + destination_relative_path="manual_checks/jupyter/jupyter_complex_good.ipynb", + ), + FixtureCopy( + source_relative_path="tests/fixtures/real/openai_cookbook_assistants_api_overview_42bc4ed.ipynb", + destination_relative_path="manual_checks/jupyter/jupyter_complex_candidate.ipynb", + ), +) + + +def main() -> int: + for fixture in FIXTURE_COPIES: + source = REPO_ROOT / fixture.source_relative_path + destination = REPO_ROOT / fixture.destination_relative_path + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, destination) + print(f"{fixture.source_relative_path} -> {fixture.destination_relative_path}") + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/refresh_real_fixtures.py b/scripts/refresh_real_fixtures.py new file mode 100644 index 0000000..fe547e4 --- /dev/null +++ b/scripts/refresh_real_fixtures.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +import argparse +from dataclasses import dataclass +from pathlib import Path +from urllib.request import urlopen + +from pytest_notebook_policy.plugin import DEFAULT_RULES, scan_file + + +@dataclass(frozen=True) +class FixtureSource: + filename: str + url: str + + +FIXTURE_SOURCES = ( + FixtureSource( + filename="marimo_examples_ui_file.py", + url="https://raw.githubusercontent.com/marimo-team/marimo/main/examples/ui/file.py", + ), + FixtureSource( + filename="marimo_smoke_cross_cell_md.py", + url=( + "https://raw.githubusercontent.com/marimo-team/marimo/main/" + "marimo/_smoke_tests/markdown/cross_cell_md.py" + ), + ), + FixtureSource( + filename="gallery_matrix.py", + url="https://raw.githubusercontent.com/marimo-team/gallery-examples/main/notebooks/math/matrix.py", + ), + FixtureSource( + filename="gallery_earthquake.py", + url="https://raw.githubusercontent.com/marimo-team/gallery-examples/main/notebooks/geo/earthquake.py", + ), + FixtureSource( + filename="jupyter_running_code_v6_4_7.ipynb", + url=( + "https://raw.githubusercontent.com/jupyter/notebook/v6.4.7/" + "docs/source/examples/Notebook/Running%20Code.ipynb" + ), + ), + FixtureSource( + filename="jupyter_importing_notebooks_v6_4_7.ipynb", + url=( + "https://raw.githubusercontent.com/jupyter/notebook/v6.4.7/" + "docs/source/examples/Notebook/Importing%20Notebooks.ipynb" + ), + ), + FixtureSource( + filename="jupyter_connecting_qt_console_v6_4_7.ipynb", + url=( + "https://raw.githubusercontent.com/jupyter/notebook/v6.4.7/" + "docs/source/examples/Notebook/Connecting%20with%20the%20Qt%20Console.ipynb" + ), + ), + FixtureSource( + filename="openai_cookbook_assistants_api_overview_42bc4ed.ipynb", + url=( + "https://raw.githubusercontent.com/openai/openai-cookbook/" + "42bc4ed33a2d1ee308c1f614c073f1b5f760061b/examples/" + "Assistants_API_overview_python.ipynb" + ), + ), +) + +REPO_ROOT = Path(__file__).resolve().parents[1] +REAL_FIXTURE_DIR = REPO_ROOT / "tests" / "fixtures" / "real" + + +def _download_fixture(source: FixtureSource) -> None: + destination = REAL_FIXTURE_DIR / source.filename + with urlopen(source.url) as response: # noqa: S310 + content = response.read() + destination.write_bytes(content) + + +def _report_rule_codes(source: FixtureSource) -> str: + fixture_path = REAL_FIXTURE_DIR / source.filename + violations = scan_file(fixture_path, select=set(DEFAULT_RULES), ignore=set()) + codes = sorted({violation.code for violation in violations}) + return f"{source.filename}: {codes}" + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Refresh real notebook fixtures from pinned URLs and print observed rule codes." + ) + parser.add_argument( + "--report-only", + action="store_true", + help="Skip downloads and only print currently observed rule-code sets.", + ) + args = parser.parse_args() + + REAL_FIXTURE_DIR.mkdir(parents=True, exist_ok=True) + + if not args.report_only: + for source in FIXTURE_SOURCES: + _download_fixture(source) + + for source in FIXTURE_SOURCES: + print(_report_rule_codes(source)) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/pytest_marimo/plugin.py b/src/pytest_marimo/plugin.py deleted file mode 100644 index cb2a809..0000000 --- a/src/pytest_marimo/plugin.py +++ /dev/null @@ -1,570 +0,0 @@ -from __future__ import annotations - -import ast -from dataclasses import dataclass -from pathlib import Path - -import pytest - -DEFAULT_RULES = ("M001", "M002", "M003", "M004", "M005", "M006") -EXCLUDED_DIRS = {".git", ".hg", ".svn", ".venv", "venv", "__pycache__", ".pytest_cache"} -APP_NAMES = {"app"} -MUTATING_METHODS = { - "append", - "clear", - "discard", - "extend", - "insert", - "pop", - "remove", - "reverse", - "setdefault", - "sort", - "update", -} -NON_IDEMPOTENT_CALLS = { - "datetime.date.today", - "datetime.datetime.now", - "datetime.datetime.today", - "datetime.datetime.utcnow", - "random.betavariate", - "random.choice", - "random.choices", - "random.expovariate", - "random.gauss", - "random.gammavariate", - "random.lognormvariate", - "random.randint", - "random.random", - "random.randrange", - "random.sample", - "random.shuffle", - "random.triangular", - "random.uniform", - "secrets.choice", - "secrets.randbelow", - "secrets.randbits", - "secrets.token_bytes", - "secrets.token_hex", - "secrets.token_urlsafe", - "time.perf_counter", - "time.time", - "uuid.uuid4", -} -NON_IDEMPOTENT_PREFIXES = ("np.random.", "numpy.random.") -_VIOLATIONS_KEY: pytest.StashKey[list["Violation"]] = pytest.StashKey() - - -@dataclass(frozen=True) -class Violation: - path: Path - line: int - code: str - message: str - - def format(self, root: Path) -> str: - relative = self.path.relative_to(root) if self.path.is_relative_to(root) else self.path - return f"{relative}:{self.line}: {self.code} {self.message}" - - -def pytest_addoption(parser: pytest.Parser) -> None: - group = parser.getgroup("marimo") - group.addoption( - "--marimo-check", - action="store_true", - default=False, - help="Enable marimo notebook quality checks.", - ) - group.addoption( - "--marimo-check-select", - action="append", - default=[], - metavar="RULE", - help="Rule code or prefix to enable (repeatable, e.g. M001 or M00).", - ) - group.addoption( - "--marimo-check-ignore", - action="append", - default=[], - metavar="RULE", - help="Rule code or prefix to ignore (repeatable, e.g. M004).", - ) - parser.addini("marimo_check", "Enable pytest-marimo checks.", type="bool", default=False) - parser.addini( - "marimo_check_select", - "Rule code prefixes to enable for marimo checks.", - type="linelist", - default=[], - ) - parser.addini( - "marimo_check_ignore", - "Rule code prefixes to ignore for marimo checks.", - type="linelist", - default=[], - ) - - -def pytest_sessionstart(session: pytest.Session) -> None: - if not _is_enabled(session.config): - return - - select = _resolve_select(session.config) - ignore = _resolve_ignore(session.config) - violations: list[Violation] = [] - for candidate in _iter_candidate_files(session.config): - violations.extend(scan_file(candidate, select=select, ignore=ignore)) - session.config.stash[_VIOLATIONS_KEY] = violations - - -def pytest_terminal_summary( - terminalreporter: pytest.TerminalReporter, - exitstatus: int, # noqa: ARG001 - config: pytest.Config, -) -> None: - violations = config.stash.get(_VIOLATIONS_KEY, []) - if not violations: - return - - terminalreporter.section("pytest-marimo") - for violation in violations: - terminalreporter.line(violation.format(config.rootpath)) - terminalreporter.line(f"{len(violations)} marimo violation(s) found.") - - -def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: - violations = session.config.stash.get(_VIOLATIONS_KEY, []) - if violations and exitstatus == pytest.ExitCode.OK: - session.exitstatus = pytest.ExitCode.TESTS_FAILED - - -def scan_file(path: Path, select: set[str], ignore: set[str]) -> list[Violation]: - try: - source = path.read_text(encoding="utf-8") - except (OSError, UnicodeDecodeError): - return [] - - try: - tree = ast.parse(source, filename=str(path)) - except SyntaxError: - return [] - - if not is_marimo_notebook(tree): - return [] - - violations: list[Violation] = [] - if _rule_enabled("M001", select, ignore): - violations.extend(_check_on_change(path, tree)) - if _rule_enabled("M002", select, ignore): - violations.extend(_check_mixed_test_cells(path, tree)) - if _rule_enabled("M003", select, ignore): - violations.extend(_check_module_assignments(path, tree)) - if _rule_enabled("M004", select, ignore): - violations.extend(_check_fixture_in_notebook(path, tree)) - if _rule_enabled("M005", select, ignore): - violations.extend(_check_cross_cell_mutation(path, tree)) - if _rule_enabled("M006", select, ignore): - violations.extend(_check_non_idempotent_cells(path, tree)) - return violations - - -def is_marimo_notebook(tree: ast.AST) -> bool: - imports_marimo = False - for node in ast.walk(tree): - if isinstance(node, ast.Import): - if any(alias.name == "marimo" for alias in node.names): - imports_marimo = True - elif isinstance(node, ast.ImportFrom): - if node.module and node.module.startswith("marimo"): - imports_marimo = True - - has_cell = any( - _is_cell_function(node) - for node in ast.walk(tree) - if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) - ) - return imports_marimo and has_cell - - -def _is_cell_function(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool: - return any(_is_cell_decorator(decorator) for decorator in node.decorator_list) - - -def _is_cell_decorator(decorator: ast.expr) -> bool: - if isinstance(decorator, ast.Attribute): - return decorator.attr == "cell" - if isinstance(decorator, ast.Call) and isinstance(decorator.func, ast.Attribute): - return decorator.func.attr == "cell" - return False - - -def _iter_cell_functions(tree: ast.AST) -> list[ast.FunctionDef | ast.AsyncFunctionDef]: - return [ - node - for node in ast.walk(tree) - if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and _is_cell_function(node) - ] - - -def _check_on_change(path: Path, tree: ast.AST) -> list[Violation]: - violations: list[Violation] = [] - for node in ast.walk(tree): - if not isinstance(node, ast.Call): - continue - for keyword in node.keywords: - if keyword.arg == "on_change": - violations.append( - Violation( - path=path, - line=node.lineno, - code="M001", - message="Prefer reactive dependencies over on_change handlers.", - ) - ) - return violations - - -def _check_mixed_test_cells(path: Path, tree: ast.AST) -> list[Violation]: - violations: list[Violation] = [] - for node in _iter_cell_functions(tree): - has_test = False - has_other = False - for statement in node.body: - if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef)): - if statement.name.startswith("test_"): - has_test = True - else: - has_other = True - continue - if _is_ignorable_in_test_cell(statement): - continue - has_other = True - - if has_test and has_other: - violations.append( - Violation( - path=path, - line=node.lineno, - code="M002", - message="Keep test cells focused: avoid mixing tests with setup/helper code.", - ) - ) - return violations - - -def _is_ignorable_in_test_cell(statement: ast.stmt) -> bool: - if isinstance(statement, (ast.Return, ast.Pass)): - return True - if isinstance(statement, ast.Expr) and isinstance(statement.value, ast.Constant): - return isinstance(statement.value.value, str) - return False - - -def _check_module_assignments(path: Path, tree: ast.Module) -> list[Violation]: - violations: list[Violation] = [] - for statement in tree.body: - names = _assignment_names(statement) - if not names: - continue - for name, line in names: - if _allowed_module_name(name): - continue - violations.append( - Violation( - path=path, - line=line, - code="M003", - message="Avoid mutable module-level state; keep state inside cells/functions.", - ) - ) - return violations - - -def _assignment_names(statement: ast.stmt) -> list[tuple[str, int]]: - names: list[tuple[str, int]] = [] - if isinstance(statement, ast.Assign): - for target in statement.targets: - names.extend((name, statement.lineno) for name in _target_names(target)) - elif isinstance(statement, ast.AnnAssign): - names.extend((name, statement.lineno) for name in _target_names(statement.target)) - elif isinstance(statement, ast.AugAssign): - names.extend((name, statement.lineno) for name in _target_names(statement.target)) - return names - - -def _target_names(target: ast.expr) -> list[str]: - if isinstance(target, ast.Name): - return [target.id] - if isinstance(target, (ast.Tuple, ast.List)): - names: list[str] = [] - for elt in target.elts: - names.extend(_target_names(elt)) - return names - return [] - - -def _allowed_module_name(name: str) -> bool: - if name in APP_NAMES: - return True - if name.startswith("__"): - return True - if name.isupper(): - return True - return False - - -def _check_fixture_in_notebook(path: Path, tree: ast.AST) -> list[Violation]: - violations: list[Violation] = [] - for node in ast.walk(tree): - if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): - continue - if any(_is_fixture_decorator(decorator) for decorator in node.decorator_list): - violations.append( - Violation( - path=path, - line=node.lineno, - code="M004", - message="Prefer fixtures in conftest.py or helper modules for static collection.", - ) - ) - return violations - - -def _is_fixture_decorator(decorator: ast.expr) -> bool: - if isinstance(decorator, ast.Name): - return decorator.id == "fixture" - if isinstance(decorator, ast.Attribute): - return isinstance(decorator.value, ast.Name) and decorator.value.id == "pytest" and decorator.attr == "fixture" - if isinstance(decorator, ast.Call): - return _is_fixture_decorator(decorator.func) - return False - - -def _check_cross_cell_mutation(path: Path, tree: ast.Module) -> list[Violation]: - violations: list[Violation] = [] - seen: set[tuple[int, str]] = set() - module_mutables = _mutable_module_names(tree) - - for cell in _iter_cell_functions(tree): - tracked_names = module_mutables | set(_cell_parameters(cell)) - - for node in ast.walk(cell): - if not isinstance(node, ast.Global): - continue - for name in node.names: - key = (node.lineno, f"global:{name}") - if key in seen: - continue - seen.add(key) - violations.append( - Violation( - path=path, - line=node.lineno, - code="M005", - message=f"Avoid `global {name}` in cells; keep mutations local to the defining cell.", - ) - ) - - for node in _iter_runtime_nodes(cell): - if isinstance(node, ast.Call) and _is_mutating_method_call(node, tracked_names): - key = (node.lineno, "method") - if key in seen: - continue - seen.add(key) - violations.append( - Violation( - path=path, - line=node.lineno, - code="M005", - message="Avoid cross-cell mutation of shared objects; prefer creating a new value.", - ) - ) - continue - - if _is_subscript_assignment_to_tracked_name(node, tracked_names): - key = (node.lineno, "subscript") - if key in seen: - continue - seen.add(key) - violations.append( - Violation( - path=path, - line=node.lineno, - code="M005", - message="Avoid mutating a shared container from another cell; derive a new object instead.", - ) - ) - - return violations - - -def _mutable_module_names(tree: ast.Module) -> set[str]: - names: set[str] = set() - for statement in tree.body: - if not isinstance(statement, ast.Assign): - continue - if not _looks_mutable_value(statement.value): - continue - for target in statement.targets: - names.update(_target_names(target)) - return names - - -def _looks_mutable_value(value: ast.expr) -> bool: - if isinstance(value, (ast.List, ast.Dict, ast.Set, ast.ListComp, ast.DictComp, ast.SetComp)): - return True - if isinstance(value, ast.Call): - name = _dotted_name(value.func) - return name in {"dict", "list", "set", "collections.defaultdict"} - return False - - -def _cell_parameters(cell: ast.FunctionDef | ast.AsyncFunctionDef) -> list[str]: - names = [arg.arg for arg in cell.args.args] - names.extend(arg.arg for arg in cell.args.posonlyargs) - names.extend(arg.arg for arg in cell.args.kwonlyargs) - if cell.args.vararg: - names.append(cell.args.vararg.arg) - if cell.args.kwarg: - names.append(cell.args.kwarg.arg) - return names - - -def _iter_runtime_nodes(cell: ast.FunctionDef | ast.AsyncFunctionDef): - for statement in cell.body: - if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): - continue - yield statement - yield from _walk_descendants_excluding_nested_functions(statement) - - -def _walk_descendants_excluding_nested_functions(node: ast.AST): - for child in ast.iter_child_nodes(node): - if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)): - continue - yield child - yield from _walk_descendants_excluding_nested_functions(child) - - -def _is_mutating_method_call(call: ast.Call, tracked_names: set[str]) -> bool: - if not isinstance(call.func, ast.Attribute): - return False - if call.func.attr not in MUTATING_METHODS: - return False - if not isinstance(call.func.value, ast.Name): - return False - return call.func.value.id in tracked_names - - -def _is_subscript_assignment_to_tracked_name(node: ast.AST, tracked_names: set[str]) -> bool: - if isinstance(node, ast.Assign): - targets: list[ast.expr] = list(node.targets) - elif isinstance(node, ast.AnnAssign): - targets = [node.target] - elif isinstance(node, ast.AugAssign): - targets = [node.target] - else: - return False - - for target in targets: - if isinstance(target, ast.Subscript) and isinstance(target.value, ast.Name): - if target.value.id in tracked_names: - return True - return False - - -def _check_non_idempotent_cells(path: Path, tree: ast.AST) -> list[Violation]: - violations: list[Violation] = [] - seen: set[tuple[int, str]] = set() - - for cell in _iter_cell_functions(tree): - for node in _iter_runtime_nodes(cell): - if not isinstance(node, ast.Call): - continue - call_name = _dotted_name(node.func) - if call_name is None or not _is_non_idempotent_call(call_name): - continue - key = (node.lineno, call_name) - if key in seen: - continue - seen.add(key) - violations.append( - Violation( - path=path, - line=node.lineno, - code="M006", - message=f"`{call_name}()` can make cell behaviour non-idempotent; gate it behind explicit inputs.", - ) - ) - return violations - - -def _dotted_name(expr: ast.expr) -> str | None: - if isinstance(expr, ast.Name): - return expr.id - if isinstance(expr, ast.Attribute): - root = _dotted_name(expr.value) - if root: - return f"{root}.{expr.attr}" - return expr.attr - return None - - -def _is_non_idempotent_call(call_name: str) -> bool: - if call_name in NON_IDEMPOTENT_CALLS: - return True - return any(call_name.startswith(prefix) for prefix in NON_IDEMPOTENT_PREFIXES) - - -def _is_enabled(config: pytest.Config) -> bool: - return bool(config.getoption("marimo_check")) or bool(config.getini("marimo_check")) - - -def _resolve_select(config: pytest.Config) -> set[str]: - selected = _normalise_rules(config.getini("marimo_check_select")) - selected.update(_normalise_rules(config.getoption("marimo_check_select"))) - if selected: - return selected - return set(DEFAULT_RULES) - - -def _resolve_ignore(config: pytest.Config) -> set[str]: - ignored = _normalise_rules(config.getini("marimo_check_ignore")) - ignored.update(_normalise_rules(config.getoption("marimo_check_ignore"))) - return ignored - - -def _normalise_rules(values: list[str]) -> set[str]: - normalised: set[str] = set() - for raw in values: - for part in raw.split(","): - token = part.strip().upper() - if token: - normalised.add(token) - return normalised - - -def _rule_enabled(code: str, select: set[str], ignore: set[str]) -> bool: - if any(code.startswith(prefix) for prefix in ignore): - return False - return any(code.startswith(prefix) for prefix in select) - - -def _iter_candidate_files(config: pytest.Config) -> list[Path]: - candidates: list[Path] = [] - roots = config.args or [str(config.rootpath)] - - for root in roots: - base = Path(root.split("::", maxsplit=1)[0]) - if not base.is_absolute(): - base = config.rootpath / base - if not base.exists(): - continue - if base.is_file(): - if base.suffix == ".py": - candidates.append(base.resolve()) - continue - for path in base.rglob("*.py"): - if any(part in EXCLUDED_DIRS for part in path.parts): - continue - candidates.append(path.resolve()) - return sorted(set(candidates)) diff --git a/src/pytest_marimo/quality.py b/src/pytest_marimo/quality.py deleted file mode 100644 index 4039465..0000000 --- a/src/pytest_marimo/quality.py +++ /dev/null @@ -1,75 +0,0 @@ -from __future__ import annotations - -import argparse -import subprocess -import sys -from pathlib import Path - -from .plugin import DEFAULT_RULES, EXCLUDED_DIRS, scan_file - - -def main() -> None: - parser = argparse.ArgumentParser( - prog="pytest-marimo-quality", - description="Run Ruff and pytest-marimo semantic checks over notebook paths.", - ) - parser.add_argument( - "paths", - nargs="+", - help="Files or directories to check.", - ) - parser.add_argument( - "--skip-ruff", - action="store_true", - help="Skip Ruff linting and run only pytest-marimo semantic checks.", - ) - args = parser.parse_args() - - path_args = [str(Path(path).expanduser()) for path in args.paths] - - ruff_exit_code = 0 - if not args.skip_ruff: - ruff_exit_code = _run_ruff(path_args) - - violations = _run_marimo_checks(path_args) - if violations: - print("\npytest-marimo semantic violations") - for violation in violations: - print(f"{violation.path}:{violation.line}: {violation.code} {violation.message}") - print(f"{len(violations)} violation(s) found.") - - if ruff_exit_code != 0 or violations: - raise SystemExit(1) - - -def _run_ruff(paths: list[str]) -> int: - command = [sys.executable, "-m", "ruff", "check", *paths] - completed = subprocess.run(command, check=False) - return completed.returncode - - -def _run_marimo_checks(paths: list[str]): - violations = [] - select = set(DEFAULT_RULES) - ignore: set[str] = set() - - for path in _iter_python_files(paths): - violations.extend(scan_file(path, select=select, ignore=ignore)) - - return sorted(violations, key=lambda v: (str(v.path), v.line, v.code)) - - -def _iter_python_files(paths: list[str]): - for raw in paths: - path = Path(raw) - if not path.exists(): - continue - if path.is_file(): - if path.suffix == ".py": - yield path.resolve() - continue - - for candidate in path.rglob("*.py"): - if any(part in EXCLUDED_DIRS for part in candidate.parts): - continue - yield candidate.resolve() diff --git a/src/pytest_marimo/__init__.py b/src/pytest_notebook_policy/__init__.py similarity index 67% rename from src/pytest_marimo/__init__.py rename to src/pytest_notebook_policy/__init__.py index 3ba322c..46b8c57 100644 --- a/src/pytest_marimo/__init__.py +++ b/src/pytest_notebook_policy/__init__.py @@ -1,4 +1,4 @@ -"""pytest-marimo package.""" +"""pytest-notebook-policy package.""" from .plugin import Violation, scan_file diff --git a/src/pytest_notebook_policy/plugin.py b/src/pytest_notebook_policy/plugin.py new file mode 100644 index 0000000..664e9bf --- /dev/null +++ b/src/pytest_notebook_policy/plugin.py @@ -0,0 +1,1224 @@ +from __future__ import annotations + +import ast +import json +import re +from dataclasses import dataclass +from pathlib import Path + +import pytest + +DEFAULT_RULES = ( + "M001", + "M002", + "M003", + "M004", + "M005", + "M006", + "J001", + "J002", + "J011", + "J012", + "J013", +) +EXCLUDED_DIRS = {".git", ".hg", ".svn", ".venv", "venv", "__pycache__", ".pytest_cache"} +APP_NAMES = {"app"} +MUTATING_METHODS = { + "append", + "clear", + "discard", + "extend", + "insert", + "pop", + "remove", + "reverse", + "setdefault", + "sort", + "update", +} +NON_IDEMPOTENT_CALLS = { + "datetime.date.today", + "datetime.datetime.now", + "datetime.datetime.today", + "datetime.datetime.utcnow", + "random.betavariate", + "random.choice", + "random.choices", + "random.expovariate", + "random.gauss", + "random.gammavariate", + "random.lognormvariate", + "random.randint", + "random.random", + "random.randrange", + "random.sample", + "random.shuffle", + "random.triangular", + "random.uniform", + "secrets.choice", + "secrets.randbelow", + "secrets.randbits", + "secrets.token_bytes", + "secrets.token_hex", + "secrets.token_urlsafe", + "time.perf_counter", + "time.time", + "uuid.uuid4", +} +NON_IDEMPOTENT_PREFIXES = ("np.random.", "numpy.random.") +JUPYTER_PARAMETER_CELL_NAMES = { + "config", + "configuration", + "param", + "params", + "parameter", + "parameters", + "settings", +} +JUPYTER_MAX_CODE_CELLS = 20 +JUPYTER_MAX_CELL_LINES = 80 +JUPYTER_MAX_INLINE_DEFINITIONS = 3 +JUPYTER_ENVIRONMENT_LOOKAHEAD_CELLS = 5 +JUPYTER_DATA_CONTRACT_LOOKAHEAD_CELLS = 5 +JUPYTER_MAX_OUTPUT_BYTES_TOTAL = 200_000 +JUPYTER_MAX_OUTPUT_BYTES_PER_CELL = 50_000 +M021_MAX_INLINE_DEFINITIONS = 5 +M021_MAX_INLINE_DEFINITION_LINES = 40 +JUPYTER_SOURCE_CHOICES = ("ipynb", "paired-py") +JUPYTER_ENVIRONMENT_NAME_HINTS = { + "python", + "python_version", + "runtime", + "runtime_version", + "dependencies", + "dependency_versions", + "requirements", + "packages", + "package_versions", +} +JUPYTER_ENVIRONMENT_TEXT_HINTS = ( + "python", + "runtime", + "requirements", + "dependencies", + "packages", + "assumptions", +) +JUPYTER_DATA_INPUT_NAME_HINTS = { + "data_path", + "input_path", + "dataset", + "dataset_path", + "source_uri", + "input_uri", + "inputs", + "table_name", +} +JUPYTER_DATA_SCHEMA_NAME_HINTS = { + "schema", + "dtypes", + "columns", + "expected_columns", + "shape", + "expected_shape", +} +JUPYTER_DATA_CONTRACT_TEXT_HINTS = ( + "schema", + "columns", + "dtype", + "shape", + ".csv", + ".parquet", + ".json", + "s3://", + "gs://", + "table", + "dataset", +) +SIDE_EFFECT_BOUNDARY_PATTERN = re.compile(r"side[\\s\\-_]?effects?", re.IGNORECASE) +SIDE_EFFECT_CALL_NAMES = { + "requests.post", + "requests.put", + "requests.patch", + "requests.delete", + "httpx.post", + "httpx.put", + "httpx.patch", + "httpx.delete", + "subprocess.run", + "subprocess.call", + "subprocess.check_call", + "subprocess.check_output", + "subprocess.Popen", + "os.remove", + "os.rename", + "os.replace", + "shutil.move", + "shutil.copy", + "shutil.copy2", + "shutil.rmtree", +} +SIDE_EFFECT_METHOD_NAMES = { + "write_text", + "write_bytes", + "mkdir", + "unlink", + "rename", + "replace", + "touch", + "to_csv", + "to_parquet", + "to_json", + "to_excel", + "to_sql", + "to_pickle", + "savefig", +} +SECRET_NAME_HINTS = ( + "password", + "passwd", + "pwd", + "token", + "api_key", + "apikey", + "secret", + "client_secret", + "private_key", + "access_key", +) +SECRET_PLACEHOLDER_HINTS = ( + "your_", + "change_me", + "placeholder", + "example", + "dummy", + "xxxx", + "<", + ">", +) +SECRET_VALUE_PATTERNS = ( + re.compile(r"sk-[A-Za-z0-9]{20,}"), + re.compile(r"gh[pousr]_[A-Za-z0-9]{20,}", re.IGNORECASE), + re.compile(r"AKIA[0-9A-Z]{16}"), + re.compile(r"AIza[0-9A-Za-z\\-_]{20,}"), +) +_VIOLATIONS_KEY: pytest.StashKey[list["Violation"]] = pytest.StashKey() + + +@dataclass(frozen=True) +class Violation: + path: Path + line: int + code: str + message: str + + def format(self, root: Path) -> str: + relative = self.path.relative_to(root) if self.path.is_relative_to(root) else self.path + return f"{relative}:{self.line}: {self.code} {self.message}" + + +@dataclass(frozen=True) +class NotebookCell: + index: int + line_start: int + source: str + + +def pytest_addoption(parser: pytest.Parser) -> None: + group = parser.getgroup("notebook_policy") + group.addoption( + "--notebook-check", + action="store_true", + default=False, + help="Enable notebook policy checks.", + ) + group.addoption( + "--notebook-check-select", + action="append", + default=[], + metavar="RULE", + help="Rule code or prefix to enable (repeatable, e.g. M001, J002, J01).", + ) + group.addoption( + "--notebook-check-ignore", + action="append", + default=[], + metavar="RULE", + help="Rule code or prefix to ignore (repeatable, e.g. M004).", + ) + group.addoption( + "--notebook-check-jupyter-source", + action="store", + default=None, + choices=JUPYTER_SOURCE_CHOICES, + metavar="SOURCE", + help="Source used for Jupyter rule checks: ipynb or paired-py.", + ) + group.addoption( + "--notebook-check-jupyter-max-code-cells", + action="store", + type=int, + default=None, + metavar="N", + help="Maximum code-cell count before J012 is reported.", + ) + group.addoption( + "--notebook-check-jupyter-max-cell-lines", + action="store", + type=int, + default=None, + metavar="N", + help="Maximum code-cell length (in lines) before J012 is reported.", + ) + group.addoption( + "--notebook-check-jupyter-max-inline-definitions", + action="store", + type=int, + default=None, + metavar="N", + help="Maximum inline function/class definitions before J013 is reported.", + ) + parser.addini("notebook_check", "Enable notebook policy checks.", type="bool", default=False) + parser.addini( + "notebook_check_select", + "Rule code prefixes to enable for notebook policy checks.", + type="linelist", + default=[], + ) + parser.addini( + "notebook_check_ignore", + "Rule code prefixes to ignore for notebook policy checks.", + type="linelist", + default=[], + ) + parser.addini( + "notebook_check_jupyter_source", + "Source used for Jupyter rule checks: ipynb or paired-py.", + default="ipynb", + ) + parser.addini( + "notebook_check_jupyter_max_code_cells", + "Maximum code-cell count before J012 is reported.", + default=str(JUPYTER_MAX_CODE_CELLS), + ) + parser.addini( + "notebook_check_jupyter_max_cell_lines", + "Maximum code-cell length (in lines) before J012 is reported.", + default=str(JUPYTER_MAX_CELL_LINES), + ) + parser.addini( + "notebook_check_jupyter_max_inline_definitions", + "Maximum inline function/class definitions before J013 is reported.", + default=str(JUPYTER_MAX_INLINE_DEFINITIONS), + ) + + +def pytest_sessionstart(session: pytest.Session) -> None: + if not _is_enabled(session.config): + return + + select = _resolve_select(session.config) + ignore = _resolve_ignore(session.config) + jupyter_source = _resolve_jupyter_source(session.config) + max_code_cells = _resolve_jupyter_max_code_cells(session.config) + max_cell_lines = _resolve_jupyter_max_cell_lines(session.config) + max_inline_definitions = _resolve_jupyter_max_inline_definitions(session.config) + violations: list[Violation] = [] + for candidate in _iter_candidate_files(session.config): + violations.extend( + scan_file( + candidate, + select=select, + ignore=ignore, + jupyter_source=jupyter_source, + max_code_cells=max_code_cells, + max_cell_lines=max_cell_lines, + max_inline_definitions=max_inline_definitions, + ) + ) + session.config.stash[_VIOLATIONS_KEY] = violations + + +def pytest_terminal_summary( + terminalreporter: pytest.TerminalReporter, + exitstatus: int, # noqa: ARG001 + config: pytest.Config, +) -> None: + violations = config.stash.get(_VIOLATIONS_KEY, []) + if not violations: + return + + terminalreporter.section("pytest-notebook-policy") + for violation in violations: + terminalreporter.line(violation.format(config.rootpath)) + terminalreporter.line(f"{len(violations)} notebook policy violation(s) found.") + + +def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None: + violations = session.config.stash.get(_VIOLATIONS_KEY, []) + if violations and exitstatus == pytest.ExitCode.OK: + session.exitstatus = pytest.ExitCode.TESTS_FAILED + + +def scan_file( + path: Path, + select: set[str], + ignore: set[str], + jupyter_source: str = "ipynb", + max_code_cells: int = JUPYTER_MAX_CODE_CELLS, + max_cell_lines: int = JUPYTER_MAX_CELL_LINES, + max_inline_definitions: int = JUPYTER_MAX_INLINE_DEFINITIONS, +) -> list[Violation]: + if path.suffix == ".ipynb": + return _scan_jupyter_file( + path, + select=select, + ignore=ignore, + jupyter_source=jupyter_source, + max_code_cells=max_code_cells, + max_cell_lines=max_cell_lines, + max_inline_definitions=max_inline_definitions, + ) + try: + source = path.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError): + return [] + + try: + tree = ast.parse(source, filename=str(path)) + except SyntaxError: + return [] + + if not is_marimo_notebook(tree): + return [] + + violations: list[Violation] = [] + if _rule_enabled("M001", select, ignore): + violations.extend(_check_on_change(path, tree)) + if _rule_enabled("M002", select, ignore): + violations.extend(_check_mixed_test_cells(path, tree)) + if _rule_enabled("M003", select, ignore): + violations.extend(_check_module_assignments(path, tree)) + if _rule_enabled("M004", select, ignore): + violations.extend(_check_fixture_in_notebook(path, tree)) + if _rule_enabled("M005", select, ignore): + violations.extend(_check_cross_cell_mutation(path, tree)) + if _rule_enabled("M006", select, ignore): + violations.extend(_check_non_idempotent_cells(path, tree)) + return violations + + +def _check_jupyter_parameter_cell(path: Path, cells: list[NotebookCell]) -> list[Violation]: + candidate_cells = [cell for cell in cells if cell.source.strip()][:3] + if not candidate_cells: + return [] + + if any(_looks_like_parameter_cell(cell.source) for cell in candidate_cells): + return [] + + first = candidate_cells[0] + return [ + Violation( + path=path, + line=first.line_start, + code="J011", + message=( + "Add a top-of-notebook parameter/configuration cell (within the first three code cells) " + "to make runs reproducible and easier to automate." + ), + ) + ] + + +def _looks_like_parameter_cell(source: str) -> bool: + try: + tree = ast.parse(source) + except SyntaxError: + return False + + has_assignment = False + for node in tree.body: + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + return False + if isinstance(node, (ast.For, ast.AsyncFor, ast.While, ast.If, ast.With, ast.Try, ast.Match)): + return False + if isinstance(node, (ast.Import, ast.ImportFrom)): + continue + if isinstance(node, (ast.Assign, ast.AnnAssign)): + targets = node.targets if isinstance(node, ast.Assign) else [node.target] + for target in targets: + if not isinstance(target, ast.Name): + continue + if target.id.lower() in JUPYTER_PARAMETER_CELL_NAMES: + has_assignment = True + elif isinstance(getattr(node, "value", None), ast.Constant): + has_assignment = True + elif isinstance(getattr(node, "value", None), (ast.List, ast.Tuple, ast.Dict, ast.Set)): + has_assignment = True + elif isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant) and isinstance(node.value.value, str): + continue + else: + return False + return has_assignment + + +def _check_jupyter_notebook_size( + path: Path, cells: list[NotebookCell], max_code_cells: int, max_cell_lines: int +) -> list[Violation]: + violations: list[Violation] = [] + + if len(cells) > max_code_cells: + violations.append( + Violation( + path=path, + line=cells[0].line_start, + code="J012", + message=( + f"Notebook has {len(cells)} code cells; keep notebooks under {max_code_cells} " + "code cells or split into smaller notebooks/modules." + ), + ) + ) + + for cell in cells: + line_count = len(cell.source.splitlines()) + if line_count <= max_cell_lines: + continue + violations.append( + Violation( + path=path, + line=cell.line_start, + code="J012", + message=( + f"Code cell is {line_count} lines long; keep cells under {max_cell_lines} lines " + "and move complex logic into modules." + ), + ) + ) + return violations + + +def _check_jupyter_inline_definitions( + path: Path, cells: list[NotebookCell], max_inline_definitions: int +) -> list[Violation]: + definition_count = 0 + first_definition_line: int | None = None + + for cell in cells: + if not cell.source.strip(): + continue + try: + tree = ast.parse(cell.source, filename=f"{path}#cell{cell.index}") + except SyntaxError: + continue + + for node in tree.body: + if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + continue + definition_count += 1 + if first_definition_line is None: + first_definition_line = cell.line_start + node.lineno - 1 + + if definition_count <= max_inline_definitions: + return [] + + return [ + Violation( + path=path, + line=first_definition_line or cells[0].line_start, + code="J013", + message=( + f"Notebook defines {definition_count} functions/classes inline; keep at most " + f"{max_inline_definitions} and extract reusable logic into importable modules." + ), + ) + ] + + +def _check_jupytext_sync(path: Path) -> list[Violation]: + script_path = path.with_suffix(".py") + if not script_path.exists(): + return [ + Violation( + path=path, + line=1, + code="J010", + message=( + "No paired .py notebook script found for sync checks; " + "create a paired file or disable J010." + ), + ) + ] + + try: + import jupytext + except ImportError: + return [ + Violation( + path=path, + line=1, + code="J010", + message="Install optional sync dependencies: `uv add --dev 'pytest-notebook-policy[sync]'`.", + ) + ] + + try: + notebook_ipynb = jupytext.read(path) + notebook_script = jupytext.read(script_path) + except Exception as exc: # noqa: BLE001 + return [ + Violation( + path=path, + line=1, + code="J010", + message=f"Unable to parse paired notebook/script for sync check: {exc}", + ) + ] + + ipynb_cells = [cell.source.strip() for cell in notebook_ipynb.cells if cell.cell_type == "code"] + script_cells = [cell.source.strip() for cell in notebook_script.cells if cell.cell_type == "code"] + if ipynb_cells != script_cells: + return [ + Violation( + path=path, + line=1, + code="J010", + message="Paired .ipynb and .py notebook sources are out of sync.", + ) + ] + return [] + + +def _scan_jupyter_file( + path: Path, + select: set[str], + ignore: set[str], + jupyter_source: str, + max_code_cells: int, + max_cell_lines: int, + max_inline_definitions: int, +) -> list[Violation]: + try: + source = path.read_text(encoding="utf-8") + except (OSError, UnicodeDecodeError): + return [] + + try: + notebook = json.loads(source) + except json.JSONDecodeError: + return [] + + ipynb_cells = _extract_notebook_cells(notebook) + if not ipynb_cells: + return [] + rule_cells = _select_jupyter_rule_cells(path, ipynb_cells=ipynb_cells, mode=jupyter_source) + + violations: list[Violation] = [] + if _rule_enabled("J001", select, ignore): + violations.extend(_check_jupyter_magic_usage(path, rule_cells)) + if _rule_enabled("J002", select, ignore): + violations.extend(_check_jupyter_non_idempotent_cells(path, rule_cells)) + if _rule_enabled("J011", select, ignore): + violations.extend(_check_jupyter_parameter_cell(path, rule_cells)) + if _rule_enabled("J012", select, ignore): + violations.extend( + _check_jupyter_notebook_size( + path, + rule_cells, + max_code_cells=max_code_cells, + max_cell_lines=max_cell_lines, + ) + ) + if _rule_enabled("J013", select, ignore): + violations.extend( + _check_jupyter_inline_definitions( + path, rule_cells, max_inline_definitions=max_inline_definitions + ) + ) + if _rule_enabled("J010", select, ignore): + violations.extend(_check_jupytext_sync(path)) + return violations + + +def _select_jupyter_rule_cells(path: Path, ipynb_cells: list[NotebookCell], mode: str) -> list[NotebookCell]: + if mode != "paired-py": + return ipynb_cells + paired_cells = _extract_paired_script_cells(path) + if paired_cells: + return paired_cells + return ipynb_cells + + +def _extract_paired_script_cells(path: Path) -> list[NotebookCell] | None: + script_path = path.with_suffix(".py") + if not script_path.exists(): + return None + + try: + import jupytext + except ImportError: + return None + + try: + notebook_script = jupytext.read(script_path) + except Exception: # noqa: BLE001 + return None + + sources = [cell.source for cell in notebook_script.cells if cell.cell_type == "code"] + if not sources: + return [] + return _cells_from_sources(sources) + + +def _extract_notebook_cells(notebook: dict) -> list[NotebookCell]: + raw_cells = notebook.get("cells") + if not isinstance(raw_cells, list): + return [] + sources: list[str] = [] + for raw_cell in raw_cells: + if not isinstance(raw_cell, dict): + continue + if raw_cell.get("cell_type") != "code": + continue + sources.append(_normalise_cell_source(raw_cell.get("source"))) + return _cells_from_sources(sources) + + +def _cells_from_sources(sources: list[str]) -> list[NotebookCell]: + + cells: list[NotebookCell] = [] + line_cursor = 1 + for index, source in enumerate(sources): + cells.append(NotebookCell(index=index, line_start=line_cursor, source=source)) + line_cursor += source.count("\n") + 2 + return cells + + +def _normalise_cell_source(source: object) -> str: + if isinstance(source, str): + return source + if isinstance(source, list): + return "".join(part for part in source if isinstance(part, str)) + return "" + + +def _check_jupyter_magic_usage(path: Path, cells: list[NotebookCell]) -> list[Violation]: + violations: list[Violation] = [] + for cell in cells: + for offset, line in enumerate(cell.source.splitlines()): + stripped = line.lstrip() + if stripped.startswith("%") or stripped.startswith("!"): + violations.append( + Violation( + path=path, + line=cell.line_start + offset, + code="J001", + message=( + "Avoid notebook magics/shell escapes in policy-checked notebooks; " + "prefer Python code for reliable .ipynb↔.py sync." + ), + ) + ) + return violations + + +def _check_jupyter_non_idempotent_cells(path: Path, cells: list[NotebookCell]) -> list[Violation]: + violations: list[Violation] = [] + seen: set[tuple[int, str]] = set() + + for cell in cells: + if not cell.source.strip(): + continue + try: + tree = ast.parse(cell.source, filename=f"{path}#cell{cell.index}") + except SyntaxError: + continue + + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + call_name = _dotted_name(node.func) + if call_name is None or not _is_non_idempotent_call(call_name): + continue + + line = cell.line_start + node.lineno - 1 + key = (line, call_name) + if key in seen: + continue + seen.add(key) + violations.append( + Violation( + path=path, + line=line, + code="J002", + message=( + f"`{call_name}()` can make notebook behaviour non-idempotent; " + "gate it behind explicit inputs or controlled seeds." + ), + ) + ) + + return violations + + +def is_marimo_notebook(tree: ast.AST) -> bool: + imports_marimo = False + for node in ast.walk(tree): + if isinstance(node, ast.Import): + if any(alias.name == "marimo" for alias in node.names): + imports_marimo = True + elif isinstance(node, ast.ImportFrom): + if node.module and node.module.startswith("marimo"): + imports_marimo = True + + has_cell = any( + _is_cell_function(node) + for node in ast.walk(tree) + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) + ) + return imports_marimo and has_cell + + +def _is_cell_function(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool: + return any(_is_cell_decorator(decorator) for decorator in node.decorator_list) + + +def _is_cell_decorator(decorator: ast.expr) -> bool: + if isinstance(decorator, ast.Attribute): + return decorator.attr == "cell" + if isinstance(decorator, ast.Call) and isinstance(decorator.func, ast.Attribute): + return decorator.func.attr == "cell" + return False + + +def _iter_cell_functions(tree: ast.AST) -> list[ast.FunctionDef | ast.AsyncFunctionDef]: + return [ + node + for node in ast.walk(tree) + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and _is_cell_function(node) + ] + + +def _check_on_change(path: Path, tree: ast.AST) -> list[Violation]: + violations: list[Violation] = [] + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + for keyword in node.keywords: + if keyword.arg == "on_change": + violations.append( + Violation( + path=path, + line=node.lineno, + code="M001", + message="Prefer reactive dependencies over on_change handlers.", + ) + ) + return violations + + +def _check_mixed_test_cells(path: Path, tree: ast.AST) -> list[Violation]: + violations: list[Violation] = [] + for node in _iter_cell_functions(tree): + has_test = False + has_other = False + for statement in node.body: + if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef)): + if statement.name.startswith("test_"): + has_test = True + else: + has_other = True + continue + if _is_ignorable_in_test_cell(statement): + continue + has_other = True + + if has_test and has_other: + violations.append( + Violation( + path=path, + line=node.lineno, + code="M002", + message="Keep test cells focused: avoid mixing tests with setup/helper code.", + ) + ) + return violations + + +def _is_ignorable_in_test_cell(statement: ast.stmt) -> bool: + if isinstance(statement, (ast.Return, ast.Pass)): + return True + if isinstance(statement, ast.Expr) and isinstance(statement.value, ast.Constant): + return isinstance(statement.value.value, str) + return False + + +def _check_module_assignments(path: Path, tree: ast.Module) -> list[Violation]: + violations: list[Violation] = [] + for statement in tree.body: + names = _assignment_names(statement) + if not names: + continue + for name, line in names: + if _allowed_module_name(name): + continue + violations.append( + Violation( + path=path, + line=line, + code="M003", + message="Avoid mutable module-level state; keep state inside cells/functions.", + ) + ) + return violations + + +def _assignment_names(statement: ast.stmt) -> list[tuple[str, int]]: + names: list[tuple[str, int]] = [] + if isinstance(statement, ast.Assign): + for target in statement.targets: + names.extend((name, statement.lineno) for name in _target_names(target)) + elif isinstance(statement, ast.AnnAssign): + names.extend((name, statement.lineno) for name in _target_names(statement.target)) + elif isinstance(statement, ast.AugAssign): + names.extend((name, statement.lineno) for name in _target_names(statement.target)) + return names + + +def _target_names(target: ast.expr) -> list[str]: + if isinstance(target, ast.Name): + return [target.id] + if isinstance(target, (ast.Tuple, ast.List)): + names: list[str] = [] + for elt in target.elts: + names.extend(_target_names(elt)) + return names + return [] + + +def _allowed_module_name(name: str) -> bool: + if name in APP_NAMES: + return True + if name.startswith("__"): + return True + if name.isupper(): + return True + return False + + +def _check_fixture_in_notebook(path: Path, tree: ast.AST) -> list[Violation]: + violations: list[Violation] = [] + for node in ast.walk(tree): + if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): + continue + if any(_is_fixture_decorator(decorator) for decorator in node.decorator_list): + violations.append( + Violation( + path=path, + line=node.lineno, + code="M004", + message="Prefer fixtures in conftest.py or helper modules for static collection.", + ) + ) + return violations + + +def _is_fixture_decorator(decorator: ast.expr) -> bool: + if isinstance(decorator, ast.Name): + return decorator.id == "fixture" + if isinstance(decorator, ast.Attribute): + return isinstance(decorator.value, ast.Name) and decorator.value.id == "pytest" and decorator.attr == "fixture" + if isinstance(decorator, ast.Call): + return _is_fixture_decorator(decorator.func) + return False + + +def _check_cross_cell_mutation(path: Path, tree: ast.Module) -> list[Violation]: + violations: list[Violation] = [] + seen: set[tuple[int, str]] = set() + module_mutables = _mutable_module_names(tree) + + for cell in _iter_cell_functions(tree): + tracked_names = module_mutables | set(_cell_parameters(cell)) + + for node in ast.walk(cell): + if not isinstance(node, ast.Global): + continue + for name in node.names: + key = (node.lineno, f"global:{name}") + if key in seen: + continue + seen.add(key) + violations.append( + Violation( + path=path, + line=node.lineno, + code="M005", + message=f"Avoid `global {name}` in cells; keep mutations local to the defining cell.", + ) + ) + + for node in _iter_runtime_nodes(cell): + if isinstance(node, ast.Call) and _is_mutating_method_call(node, tracked_names): + key = (node.lineno, "method") + if key in seen: + continue + seen.add(key) + violations.append( + Violation( + path=path, + line=node.lineno, + code="M005", + message="Avoid cross-cell mutation of shared objects; prefer creating a new value.", + ) + ) + continue + + if _is_subscript_assignment_to_tracked_name(node, tracked_names): + key = (node.lineno, "subscript") + if key in seen: + continue + seen.add(key) + violations.append( + Violation( + path=path, + line=node.lineno, + code="M005", + message="Avoid mutating a shared container from another cell; derive a new object instead.", + ) + ) + + return violations + + +def _mutable_module_names(tree: ast.Module) -> set[str]: + names: set[str] = set() + for statement in tree.body: + if not isinstance(statement, ast.Assign): + continue + if not _looks_mutable_value(statement.value): + continue + for target in statement.targets: + names.update(_target_names(target)) + return names + + +def _looks_mutable_value(value: ast.expr) -> bool: + if isinstance(value, (ast.List, ast.Dict, ast.Set, ast.ListComp, ast.DictComp, ast.SetComp)): + return True + if isinstance(value, ast.Call): + name = _dotted_name(value.func) + return name in {"dict", "list", "set", "collections.defaultdict"} + return False + + +def _cell_parameters(cell: ast.FunctionDef | ast.AsyncFunctionDef) -> list[str]: + names = [arg.arg for arg in cell.args.args] + names.extend(arg.arg for arg in cell.args.posonlyargs) + names.extend(arg.arg for arg in cell.args.kwonlyargs) + if cell.args.vararg: + names.append(cell.args.vararg.arg) + if cell.args.kwarg: + names.append(cell.args.kwarg.arg) + return names + + +def _iter_runtime_nodes(cell: ast.FunctionDef | ast.AsyncFunctionDef): + for statement in cell.body: + if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): + continue + yield statement + yield from _walk_descendants_excluding_nested_functions(statement) + + +def _walk_descendants_excluding_nested_functions(node: ast.AST): + for child in ast.iter_child_nodes(node): + if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)): + continue + yield child + yield from _walk_descendants_excluding_nested_functions(child) + + +def _is_mutating_method_call(call: ast.Call, tracked_names: set[str]) -> bool: + if not isinstance(call.func, ast.Attribute): + return False + if call.func.attr not in MUTATING_METHODS: + return False + if not isinstance(call.func.value, ast.Name): + return False + return call.func.value.id in tracked_names + + +def _is_subscript_assignment_to_tracked_name(node: ast.AST, tracked_names: set[str]) -> bool: + if isinstance(node, ast.Assign): + targets: list[ast.expr] = list(node.targets) + elif isinstance(node, ast.AnnAssign): + targets = [node.target] + elif isinstance(node, ast.AugAssign): + targets = [node.target] + else: + return False + + for target in targets: + if isinstance(target, ast.Subscript) and isinstance(target.value, ast.Name): + if target.value.id in tracked_names: + return True + return False + + +def _check_non_idempotent_cells(path: Path, tree: ast.AST) -> list[Violation]: + violations: list[Violation] = [] + seen: set[tuple[int, str]] = set() + + for cell in _iter_cell_functions(tree): + for node in _iter_runtime_nodes(cell): + if not isinstance(node, ast.Call): + continue + call_name = _dotted_name(node.func) + if call_name is None or not _is_non_idempotent_call(call_name): + continue + key = (node.lineno, call_name) + if key in seen: + continue + seen.add(key) + violations.append( + Violation( + path=path, + line=node.lineno, + code="M006", + message=f"`{call_name}()` can make cell behaviour non-idempotent; gate it behind explicit inputs.", + ) + ) + return violations + + +def _dotted_name(expr: ast.expr) -> str | None: + if isinstance(expr, ast.Name): + return expr.id + if isinstance(expr, ast.Attribute): + root = _dotted_name(expr.value) + if root: + return f"{root}.{expr.attr}" + return expr.attr + return None + + +def _is_non_idempotent_call(call_name: str) -> bool: + if call_name in NON_IDEMPOTENT_CALLS: + return True + return any(call_name.startswith(prefix) for prefix in NON_IDEMPOTENT_PREFIXES) + + +def _is_enabled(config: pytest.Config) -> bool: + return bool(config.getoption("notebook_check")) or bool(config.getini("notebook_check")) + + +def _resolve_select(config: pytest.Config) -> set[str]: + selected = _normalise_rules(config.getini("notebook_check_select")) + selected.update(_normalise_rules(config.getoption("notebook_check_select"))) + if selected: + return selected + return set(DEFAULT_RULES) + + +def _resolve_ignore(config: pytest.Config) -> set[str]: + ignored = _normalise_rules(config.getini("notebook_check_ignore")) + ignored.update(_normalise_rules(config.getoption("notebook_check_ignore"))) + return ignored + + +def _resolve_jupyter_source(config: pytest.Config) -> str: + cli_value = config.getoption("notebook_check_jupyter_source") + if cli_value in JUPYTER_SOURCE_CHOICES: + return cli_value + + ini_value = str(config.getini("notebook_check_jupyter_source")).strip().lower() + if ini_value in JUPYTER_SOURCE_CHOICES: + return ini_value + return "ipynb" + + +def _resolve_jupyter_max_code_cells(config: pytest.Config) -> int: + return _resolve_positive_int_option( + config, + option_name="notebook_check_jupyter_max_code_cells", + ini_name="notebook_check_jupyter_max_code_cells", + default=JUPYTER_MAX_CODE_CELLS, + ) + + +def _resolve_jupyter_max_cell_lines(config: pytest.Config) -> int: + return _resolve_positive_int_option( + config, + option_name="notebook_check_jupyter_max_cell_lines", + ini_name="notebook_check_jupyter_max_cell_lines", + default=JUPYTER_MAX_CELL_LINES, + ) + + +def _resolve_jupyter_max_inline_definitions(config: pytest.Config) -> int: + return _resolve_positive_int_option( + config, + option_name="notebook_check_jupyter_max_inline_definitions", + ini_name="notebook_check_jupyter_max_inline_definitions", + default=JUPYTER_MAX_INLINE_DEFINITIONS, + ) + + +def _resolve_positive_int_option( + config: pytest.Config, option_name: str, ini_name: str, default: int +) -> int: + cli_value = config.getoption(option_name) + if cli_value is not None: + return _coerce_positive_int(cli_value, default=default) + ini_value = config.getini(ini_name) + return _coerce_positive_int(ini_value, default=default) + + +def _coerce_positive_int(raw_value: object, default: int) -> int: + try: + value = int(raw_value) + except (TypeError, ValueError): + return default + if value <= 0: + return default + return value + + +def _normalise_rules(values: list[str]) -> set[str]: + normalised: set[str] = set() + for raw in values: + for part in raw.split(","): + token = part.strip().upper() + if token: + normalised.add(token) + return normalised + + +def _rule_enabled(code: str, select: set[str], ignore: set[str]) -> bool: + if any(code.startswith(prefix) for prefix in ignore): + return False + return any(code.startswith(prefix) for prefix in select) + + +def _iter_candidate_files(config: pytest.Config) -> list[Path]: + candidates: list[Path] = [] + roots = config.args or [str(config.rootpath)] + + for root in roots: + base = Path(root.split("::", maxsplit=1)[0]) + if not base.is_absolute(): + base = config.rootpath / base + if not base.exists(): + continue + if base.is_file(): + if base.suffix in {".py", ".ipynb"}: + candidates.append(base.resolve()) + continue + for path in base.rglob("*"): + if path.suffix not in {".py", ".ipynb"}: + continue + if any(part in EXCLUDED_DIRS for part in path.parts): + continue + candidates.append(path.resolve()) + return sorted(set(candidates)) diff --git a/src/pytest_notebook_policy/quality.py b/src/pytest_notebook_policy/quality.py index c16072c..f8ea5ab 100644 --- a/src/pytest_notebook_policy/quality.py +++ b/src/pytest_notebook_policy/quality.py @@ -9,6 +9,7 @@ import sys from dataclasses import dataclass from datetime import UTC, datetime +from jinja2 import Environment from pathlib import Path import time import tomllib @@ -23,6 +24,7 @@ Violation, scan_file, ) +from .report_templates import MARKDOWN_REPORT_TEMPLATE RULE_GUIDANCE: dict[str, tuple[str, str]] = { "M001": ( @@ -484,6 +486,12 @@ def _coerce_positive_int(raw: object, fallback: int | None) -> int | None: return value +MARKDOWN_REPORT_RENDERER = Environment( # noqa: S701 + trim_blocks=True, + lstrip_blocks=True, + keep_trailing_newline=True, +).from_string(MARKDOWN_REPORT_TEMPLATE) + def _write_markdown_report( destination: Path, settings: QualitySettings, @@ -494,100 +502,103 @@ def _write_markdown_report( ) -> None: analysis = _analyse_notebooks(scanned_files) dependencies = _collect_dependency_metadata(analysis.imports) if settings.report_dependency_enrichment else [] - - lines: list[str] = [] status = "✅ Pass" if not violations else "⚠️ Findings" - lines.append("## 🧪 Notebook policy report") - lines.append("") - lines.append("| Metric | Value |") - lines.append("|---|---|") - lines.append(f"| Status | {status} |") - lines.append(f"| Generated | `{started_at.strftime('%Y-%m-%d %H:%M:%S UTC')}` |") - lines.append(f"| Runtime | `{runtime_seconds:.2f}s` |") - lines.append(f"| Files scanned | `{len(scanned_files)}` |") - lines.append(f"| Violations | `{len(violations)}` |") - lines.append("") - lines.append("### 🔎 Findings") - lines.append("") - if not violations: - lines.append("No notebook policy violations were found.") + if violations: + exec_summary_text = ( + "Policy findings were detected. This report helps you triage reproducibility and " + "maintainability risks, then apply targeted remediation with clear rationale." + ) else: - lines.append("| File | Rule | Line | What | Why this is undesirable | Suggested fix |") - lines.append("|---|---|---:|---|---|---|") - for violation in violations: - why, remediation = RULE_GUIDANCE.get( - violation.code, - ( - "This rule highlights a notebook policy concern.", - "Review the rule context and apply an appropriate fix.", - ), - ) - lines.append( - f"| `{_display_path(violation.path)}` | {_rule_link(violation.code)} | `{violation.line}` | " - f"{_escape_markdown_table_text(violation.message)} | {_escape_markdown_table_text(why)} | " - f"{_escape_markdown_table_text(remediation)} |" - ) - lines.append("") - lines.append("### 🧭 Notebook surface summary") - lines.append("") - lines.append("| Category | Count |") - lines.append("|---|---:|") - lines.append(f"| Key imports | `{len(analysis.imports)}` |") - lines.append(f"| File references | `{len(analysis.file_references)}` |") - lines.append(f"| HTTP requests | `{len(analysis.http_requests)}` |") - lines.append(f"| Data sources | `{len(analysis.data_sources)}` |") - lines.append("") - lines.append("#### Key imports") - lines.append("") - lines.append("| Import |") - lines.append("|---|") - for name in sorted(analysis.imports): - lines.append(f"| `{name}` |") - if not analysis.imports: - lines.append("| `(none)` |") - lines.append("") - lines.extend(_observed_table("#### File references", "Reference", analysis.file_references)) - lines.extend(_observed_table("#### HTTP requests", "Endpoint", analysis.http_requests)) - lines.extend(_observed_table("#### Data sources", "Source", analysis.data_sources)) - - lines.append("## Appendix A — Configuration") - lines.append("") - lines.append("| Setting | Value |") - lines.append("|---|---|") - lines.append(f"| Enabled rules/prefixes | `{', '.join(sorted(settings.select)) or '(none)'}` |") - lines.append(f"| Ignored rules/prefixes | `{', '.join(sorted(settings.ignore)) or '(none)'}` |") - lines.append(f"| Jupyter source mode | `{settings.jupyter_source}` |") - lines.append(f"| Jupyter max code cells | `{settings.max_code_cells}` |") - lines.append(f"| Jupyter max cell lines | `{settings.max_cell_lines}` |") - lines.append(f"| Jupyter max inline definitions | `{settings.max_inline_definitions}` |") - lines.append(f"| Dependency enrichment | `{settings.report_dependency_enrichment}` |") - lines.append(f"| NBOM output path | `{settings.report_nbom_json or '(disabled)'}` |") - lines.append("") - lines.append("## Appendix B — Scanned files") - lines.append("") - lines.append("| File | Type |") - lines.append("|---|---|") - for scanned_path in scanned_files: - lines.append(f"| `{_display_path(scanned_path)}` | `{scanned_path.suffix or 'n/a'}` |") - if not scanned_files: - lines.append("| `(none)` | `n/a` |") - - if settings.report_dependency_enrichment: - lines.append("") - lines.append("## Appendix C — Dependency enrichment") - lines.append("") - lines.append("| Import | Package | Version | Licence | Homepage |") - lines.append("|---|---|---|---|---|") - for record in dependencies: - lines.append( - f"| `{record[0]}` | `{record[1]}` | `{record[2]}` | " - f"`{_escape_markdown_table_text(record[3])}` | `{_escape_markdown_table_text(record[4])}` |" - ) - if not dependencies: - lines.append("| `(none)` | `(none)` | `(none)` | `(none)` | `(none)` |") + exec_summary_text = ( + "No policy findings were detected. This report provides a reproducible quality record " + "for notebook governance and release confidence." + ) + findings = [] + for violation in violations: + why, remediation = RULE_GUIDANCE.get( + violation.code, + ( + "This rule highlights a notebook policy concern.", + "Review the rule context and apply an appropriate fix.", + ), + ) + findings.append( + { + "path": _display_path(violation.path), + "rule_link": _rule_link(violation.code), + "line": violation.line, + "message": _escape_markdown_table_text(violation.message), + "why": _escape_markdown_table_text(why), + "remediation": _escape_markdown_table_text(remediation), + } + ) + scanned_rows = [ + {"path": _display_path(path), "kind": path.suffix or "n/a"} for path in scanned_files + ] + file_references = [ + { + "path": _display_path(item.path), + "value": _escape_markdown_table_text(item.value), + } + for item in sorted(analysis.file_references, key=lambda item: (_display_path(item.path), item.value)) + ] + http_requests = [ + { + "path": _display_path(item.path), + "value": _escape_markdown_table_text(item.value), + } + for item in sorted(analysis.http_requests, key=lambda item: (_display_path(item.path), item.value)) + ] + data_sources = [ + { + "path": _display_path(item.path), + "value": _escape_markdown_table_text(item.value), + } + for item in sorted(analysis.data_sources, key=lambda item: (_display_path(item.path), item.value)) + ] + dependency_rows = [ + { + "import_name": row[0], + "package": row[1], + "version": row[2], + "licence": _escape_markdown_table_text(row[3]), + "homepage": _escape_markdown_table_text(row[4]), + } + for row in dependencies + ] + nbom_output_path = str(settings.report_nbom_json) if settings.report_nbom_json else "(disabled)" + markdown = MARKDOWN_REPORT_RENDERER.render( + exec_summary_text=exec_summary_text, + status=status, + generated_at=started_at.strftime("%Y-%m-%d %H:%M:%S UTC"), + runtime_seconds=f"{runtime_seconds:.2f}", + files_scanned=len(scanned_files), + violations_count=len(violations), + findings=findings, + imports=sorted(analysis.imports), + imports_count=len(analysis.imports), + file_references=file_references, + file_references_count=len(analysis.file_references), + http_requests=http_requests, + http_requests_count=len(analysis.http_requests), + data_sources=data_sources, + data_sources_count=len(analysis.data_sources), + enabled_rules=", ".join(sorted(settings.select)) or "(none)", + ignored_rules=", ".join(sorted(settings.ignore)) or "(none)", + jupyter_source=settings.jupyter_source, + max_code_cells=settings.max_code_cells, + max_cell_lines=settings.max_cell_lines, + max_inline_definitions=settings.max_inline_definitions, + dependency_enrichment_enabled=settings.report_dependency_enrichment, + scanned_files=scanned_rows, + nbom_requested=settings.report_nbom_json is not None, + nbom_output_path=nbom_output_path, + dependency_count=len(dependencies), + dependencies=dependency_rows, + ) destination.parent.mkdir(parents=True, exist_ok=True) - destination.write_text("\n".join(lines) + "\n", encoding="utf-8") + destination.write_text(markdown, encoding="utf-8") def _observed_table(title: str, value_label: str, values: set[Observed]) -> list[str]: diff --git a/src/pytest_notebook_policy/report_templates.py b/src/pytest_notebook_policy/report_templates.py new file mode 100644 index 0000000..239f25b --- /dev/null +++ b/src/pytest_notebook_policy/report_templates.py @@ -0,0 +1,146 @@ +MARKDOWN_REPORT_TEMPLATE = """## 🧪 Notebook policy report +This report was generated to provide a deterministic notebook quality snapshot for reproducibility, maintainability, and operational confidence. + +### Executive summary +{{ exec_summary_text }} + +| Metric | Value | +|---|---| +| Status | {{ status }} | +| Generated | `{{ generated_at }}` | +| Runtime | `{{ runtime_seconds }}s` | +| Files scanned | `{{ files_scanned }}` | +| Violations | `{{ violations_count }}` | + +### 🔎 Findings +This section lists rule-level findings with concise rationale and practical remediation guidance. + +{% if findings %} +| File | Rule | Line | What | Why this is undesirable | Suggested fix | +|---|---|---:|---|---|---| +{% for finding in findings %} +| `{{ finding.path }}` | {{ finding.rule_link }} | `{{ finding.line }}` | {{ finding.message }} | {{ finding.why }} | {{ finding.remediation }} | +{% endfor %} +{% else %} +No notebook policy violations were found. +{% endif %} + +### 🧭 Notebook surface summary +This section summarises observable notebook touchpoints to support review and downstream governance workflows. + +| Category | Count | +|---|---:| +| Key imports | `{{ imports_count }}` | +| File references | `{{ file_references_count }}` | +| HTTP requests | `{{ http_requests_count }}` | +| Data sources | `{{ data_sources_count }}` | + +#### Key imports +These imports were observed across scanned notebooks. + +| Import | +|---| +{% if imports %} +{% for name in imports %} +| `{{ name }}` | +{% endfor %} +{% else %} +| `(none)` | +{% endif %} + +#### File references +Potential file/path references observed in notebook source literals. + +| File | Reference | +|---|---| +{% if file_references %} +{% for observed in file_references %} +| `{{ observed.path }}` | `{{ observed.value }}` | +{% endfor %} +{% else %} +| `(none)` | `(none)` | +{% endif %} + +#### HTTP requests +Potential HTTP endpoints observed in notebook source literals. + +| File | Endpoint | +|---|---| +{% if http_requests %} +{% for observed in http_requests %} +| `{{ observed.path }}` | `{{ observed.value }}` | +{% endfor %} +{% else %} +| `(none)` | `(none)` | +{% endif %} + +#### Data sources +Potential data source URIs observed in notebook source literals. + +| File | Source | +|---|---| +{% if data_sources %} +{% for observed in data_sources %} +| `{{ observed.path }}` | `{{ observed.value }}` | +{% endfor %} +{% else %} +| `(none)` | `(none)` | +{% endif %} + +## Appendix A — Configuration +This appendix captures the effective policy configuration used for this report run. + +| Setting | Value | +|---|---| +| Enabled rules/prefixes | `{{ enabled_rules }}` | +| Ignored rules/prefixes | `{{ ignored_rules }}` | +| Jupyter source mode | `{{ jupyter_source }}` | +| Jupyter max code cells | `{{ max_code_cells }}` | +| Jupyter max cell lines | `{{ max_cell_lines }}` | +| Jupyter max inline definitions | `{{ max_inline_definitions }}` | +| Dependency enrichment | `{{ dependency_enrichment_enabled }}` | +| NBOM output path | `{{ nbom_output_path }}` | + +## Appendix B — Scanned files +This appendix lists all notebook files included in the report run. + +| File | Type | +|---|---| +{% if scanned_files %} +{% for scanned in scanned_files %} +| `{{ scanned.path }}` | `{{ scanned.kind }}` | +{% endfor %} +{% else %} +| `(none)` | `n/a` | +{% endif %} + +## Appendix C — NBOM alignment +This appendix summarises NBOM JSON linkage so markdown and machine-readable outputs can be reviewed together. + +| NBOM field | Value | +|---|---| +| NBOM requested | `{{ nbom_requested }}` | +| NBOM output path | `{{ nbom_output_path }}` | +| Files scanned | `{{ files_scanned }}` | +| Violations | `{{ violations_count }}` | +| Imports observed | `{{ imports_count }}` | +| File references observed | `{{ file_references_count }}` | +| HTTP requests observed | `{{ http_requests_count }}` | +| Data sources observed | `{{ data_sources_count }}` | +| Dependencies recorded | `{{ dependency_count }}` | + +{% if dependency_enrichment_enabled %} +## Appendix D — Dependency enrichment +This appendix provides import-to-package metadata enrichment for dependency visibility. + +| Import | Package | Version | Licence | Homepage | +|---|---|---|---|---| +{% if dependencies %} +{% for dependency in dependencies %} +| `{{ dependency.import_name }}` | `{{ dependency.package }}` | `{{ dependency.version }}` | `{{ dependency.licence }}` | `{{ dependency.homepage }}` | +{% endfor %} +{% else %} +| `(none)` | `(none)` | `(none)` | `(none)` | `(none)` | +{% endif %} +{% endif %} +""" diff --git a/tests/fixtures/real/SOURCES.txt b/tests/fixtures/real/SOURCES.txt index 2599853..30d1538 100644 --- a/tests/fixtures/real/SOURCES.txt +++ b/tests/fixtures/real/SOURCES.txt @@ -1,4 +1,4 @@ -The files in this folder are sourced from public repositories to provide real-world marimo notebook fixtures. +The files in this folder are sourced from public repositories to provide real-world notebook fixtures. - marimo_examples_ui_file.py Source: https://raw.githubusercontent.com/marimo-team/marimo/main/examples/ui/file.py @@ -11,3 +11,15 @@ The files in this folder are sourced from public repositories to provide real-wo - gallery_earthquake.py Source: https://raw.githubusercontent.com/marimo-team/gallery-examples/main/notebooks/geo/earthquake.py + +- jupyter_running_code_v6_4_7.ipynb + Source: https://raw.githubusercontent.com/jupyter/notebook/v6.4.7/docs/source/examples/Notebook/Running%20Code.ipynb + +- jupyter_importing_notebooks_v6_4_7.ipynb + Source: https://raw.githubusercontent.com/jupyter/notebook/v6.4.7/docs/source/examples/Notebook/Importing%20Notebooks.ipynb + +- jupyter_connecting_qt_console_v6_4_7.ipynb + Source: https://raw.githubusercontent.com/jupyter/notebook/v6.4.7/docs/source/examples/Notebook/Connecting%20with%20the%20Qt%20Console.ipynb + +- openai_cookbook_assistants_api_overview_42bc4ed.ipynb + Source: https://raw.githubusercontent.com/openai/openai-cookbook/42bc4ed33a2d1ee308c1f614c073f1b5f760061b/examples/Assistants_API_overview_python.ipynb diff --git a/tests/fixtures/real/jupyter_connecting_qt_console_v6_4_7.ipynb b/tests/fixtures/real/jupyter_connecting_qt_console_v6_4_7.ipynb new file mode 100644 index 0000000..4794200 --- /dev/null +++ b/tests/fixtures/real/jupyter_connecting_qt_console_v6_4_7.ipynb @@ -0,0 +1,130 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Connecting to an existing IPython kernel using the Qt Console" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The Frontend/Kernel Model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The traditional IPython (`ipython`) consists of a single process that combines a terminal based UI with the process that runs the users code.\n", + "\n", + "While this traditional application still exists, the modern Jupyter consists of two processes:\n", + "\n", + "* Kernel: this is the process that runs the users code.\n", + "* Frontend: this is the process that provides the user interface where the user types code and sees results.\n", + "\n", + "Jupyter currently has 3 frontends:\n", + "\n", + "* Terminal Console (`jupyter console`)\n", + "* Qt Console (`jupyter qtconsole`)\n", + "* Notebook (`jupyter notebook`)\n", + "\n", + "The Kernel and Frontend communicate over a ZeroMQ/JSON based messaging protocol, which allows multiple Frontends (even of different types) to communicate with a single Kernel. This opens the door for all sorts of interesting things, such as connecting a Console or Qt Console to a Notebook's Kernel. For example, you may want to connect a Qt console to your Notebook's Kernel and use it as a help\n", + "browser, calling `??` on objects in the Qt console (whose pager is more flexible than the\n", + "one in the notebook). \n", + "\n", + "This Notebook describes how you would connect another Frontend to an IPython Kernel that is associated with a Notebook.\n", + "The commands currently given here are specific to the IPython kernel." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Manual connection" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To connect another Frontend to a Kernel manually, you first need to find out the connection information for the Kernel using the `%connect_info` magic:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%connect_info" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can see that this magic displays everything you need to connect to this Notebook's Kernel." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Automatic connection using a new Qt Console" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also start a new Qt Console connected to your current Kernel by using the `%qtconsole` magic. This will detect the necessary connection\n", + "information and start the Qt Console for you automatically." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "a = 10" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%qtconsole" + ] + } + ], + "metadata": { + "nbsphinx": { + "execute": "never" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.5.2" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb b/tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb new file mode 100644 index 0000000..c878f0d --- /dev/null +++ b/tests/fixtures/real/jupyter_importing_notebooks_v6_4_7.ipynb @@ -0,0 +1,537 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Importing Jupyter Notebooks as Modules" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It is a common problem that people want to import code from Jupyter Notebooks.\n", + "This is made difficult by the fact that Notebooks are not plain Python files,\n", + "and thus cannot be imported by the regular Python machinery.\n", + "\n", + "Fortunately, Python provides some fairly sophisticated [hooks](https://www.python.org/dev/peps/pep-0302/) into the import machinery,\n", + "so we can actually make Jupyter notebooks importable without much difficulty,\n", + "and only using public APIs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import io, os, sys, types" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "from IPython import get_ipython\n", + "from nbformat import read\n", + "from IPython.core.interactiveshell import InteractiveShell" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Import hooks typically take the form of two objects:\n", + "\n", + "1. a Module **Loader**, which takes a module name (e.g. `'IPython.display'`), and returns a Module\n", + "2. a Module **Finder**, which figures out whether a module might exist, and tells Python what **Loader** to use" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "def find_notebook(fullname, path=None):\n", + " \"\"\"find a notebook, given its fully qualified name and an optional path\n", + " \n", + " This turns \"foo.bar\" into \"foo/bar.ipynb\"\n", + " and tries turning \"Foo_Bar\" into \"Foo Bar\" if Foo_Bar\n", + " does not exist.\n", + " \"\"\"\n", + " name = fullname.rsplit('.', 1)[-1]\n", + " if not path:\n", + " path = ['']\n", + " for d in path:\n", + " nb_path = os.path.join(d, name + \".ipynb\")\n", + " if os.path.isfile(nb_path):\n", + " return nb_path\n", + " # let import Notebook_Name find \"Notebook Name.ipynb\"\n", + " nb_path = nb_path.replace(\"_\", \" \")\n", + " if os.path.isfile(nb_path):\n", + " return nb_path\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Notebook Loader" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here we have our Notebook Loader.\n", + "It's actually quite simple - once we figure out the filename of the module,\n", + "all it does is:\n", + "\n", + "1. load the notebook document into memory\n", + "2. create an empty Module\n", + "3. execute every cell in the Module namespace\n", + "\n", + "Since IPython cells can have extended syntax,\n", + "the IPython transform is applied to turn each of these cells into their pure-Python counterparts before executing them.\n", + "If all of your notebook cells are pure-Python,\n", + "this step is unnecessary." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "class NotebookLoader(object):\n", + " \"\"\"Module Loader for Jupyter Notebooks\"\"\"\n", + " def __init__(self, path=None):\n", + " self.shell = InteractiveShell.instance()\n", + " self.path = path\n", + " \n", + " def load_module(self, fullname):\n", + " \"\"\"import a notebook as a module\"\"\"\n", + " path = find_notebook(fullname, self.path)\n", + " \n", + " print (\"importing Jupyter notebook from %s\" % path)\n", + " \n", + " # load the notebook object\n", + " with io.open(path, 'r', encoding='utf-8') as f:\n", + " nb = read(f, 4)\n", + " \n", + " \n", + " # create the module and add it to sys.modules\n", + " # if name in sys.modules:\n", + " # return sys.modules[name]\n", + " mod = types.ModuleType(fullname)\n", + " mod.__file__ = path\n", + " mod.__loader__ = self\n", + " mod.__dict__['get_ipython'] = get_ipython\n", + " sys.modules[fullname] = mod\n", + " \n", + " # extra work to ensure that magics that would affect the user_ns\n", + " # actually affect the notebook module's ns\n", + " save_user_ns = self.shell.user_ns\n", + " self.shell.user_ns = mod.__dict__\n", + " \n", + " try:\n", + " for cell in nb.cells:\n", + " if cell.cell_type == 'code':\n", + " # transform the input to executable Python\n", + " code = self.shell.input_transformer_manager.transform_cell(cell.source)\n", + " # run the code in themodule\n", + " exec(code, mod.__dict__)\n", + " finally:\n", + " self.shell.user_ns = save_user_ns\n", + " return mod\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## The Module Finder" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The finder is a simple object that tells you whether a name can be imported,\n", + "and returns the appropriate loader.\n", + "All this one does is check, when you do:\n", + "\n", + "```python\n", + "import mynotebook\n", + "```\n", + "\n", + "it checks whether `mynotebook.ipynb` exists.\n", + "If a notebook is found, then it returns a NotebookLoader.\n", + "\n", + "Any extra logic is just for resolving paths within packages." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "class NotebookFinder(object):\n", + " \"\"\"Module finder that locates Jupyter Notebooks\"\"\"\n", + " def __init__(self):\n", + " self.loaders = {}\n", + " \n", + " def find_module(self, fullname, path=None):\n", + " nb_path = find_notebook(fullname, path)\n", + " if not nb_path:\n", + " return\n", + " \n", + " key = path\n", + " if path:\n", + " # lists aren't hashable\n", + " key = os.path.sep.join(path)\n", + " \n", + " if key not in self.loaders:\n", + " self.loaders[key] = NotebookLoader(path)\n", + " return self.loaders[key]\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Register the hook" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we register the `NotebookFinder` with `sys.meta_path`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "sys.meta_path.append(NotebookFinder())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After this point, my notebooks should be importable.\n", + "\n", + "Let's look at what we have in the CWD:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "ls nbpackage" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "So I should be able to `import nbpackage.mynotebook`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import nbpackage.mynotebook" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Aside: displaying notebooks" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here is some simple code to display the contents of a notebook\n", + "with syntax highlighting, etc." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "from pygments import highlight\n", + "from pygments.lexers import PythonLexer\n", + "from pygments.formatters import HtmlFormatter\n", + "\n", + "from IPython.display import display, HTML\n", + "\n", + "formatter = HtmlFormatter()\n", + "lexer = PythonLexer()\n", + "\n", + "# publish the CSS for pygments highlighting\n", + "display(HTML(\"\"\"\n", + "\n", + "\"\"\" % formatter.get_style_defs()\n", + "))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "def show_notebook(fname):\n", + " \"\"\"display a short summary of the cells of a notebook\"\"\"\n", + " with io.open(fname, 'r', encoding='utf-8') as f:\n", + " nb = read(f, 4)\n", + " html = []\n", + " for cell in nb.cells:\n", + " html.append(\"

%s cell

\" % cell.cell_type)\n", + " if cell.cell_type == 'code':\n", + " html.append(highlight(cell.source, lexer, formatter))\n", + " else:\n", + " html.append(\"
%s
\" % cell.source)\n", + " display(HTML('\\n'.join(html)))\n", + "\n", + "show_notebook(os.path.join(\"nbpackage\", \"mynotebook.ipynb\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "So my notebook has some code cells,\n", + "one of which contains some IPython syntax.\n", + "\n", + "Let's see what happens when we import it" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "from nbpackage import mynotebook" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Hooray, it imported! Does it work?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "mynotebook.foo()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Hooray again!\n", + "\n", + "Even the function that contains IPython syntax works:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "mynotebook.has_ip_syntax()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Notebooks in packages" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We also have a notebook inside the `nb` package,\n", + "so let's make sure that works as well." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "ls nbpackage/nbs" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that the `__init__.py` is necessary for `nb` to be considered a package,\n", + "just like usual." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "show_notebook(os.path.join(\"nbpackage\", \"nbs\", \"other.ipynb\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "from nbpackage.nbs import other\n", + "other.bar(5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "So now we have importable notebooks, from both the local directory and inside packages.\n", + "\n", + "I can even put a notebook inside IPython, to further demonstrate that this is working properly:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import shutil\n", + "from IPython.paths import get_ipython_package_dir\n", + "\n", + "utils = os.path.join(get_ipython_package_dir(), 'utils')\n", + "shutil.copy(os.path.join(\"nbpackage\", \"mynotebook.ipynb\"),\n", + " os.path.join(utils, \"inside_ipython.ipynb\")\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "and import the notebook from `IPython.utils`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "from IPython.utils import inside_ipython\n", + "inside_ipython.whatsmyname()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This approach can even import functions and classes that are defined in a notebook using the `%%cython` magic." + ] + } + ], + "metadata": { + "gist_id": "6011986", + "nbsphinx": { + "execute": "never" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.5.1+" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/tests/fixtures/real/jupyter_running_code_v6_4_7.ipynb b/tests/fixtures/real/jupyter_running_code_v6_4_7.ipynb new file mode 100644 index 0000000..c2857ca --- /dev/null +++ b/tests/fixtures/real/jupyter_running_code_v6_4_7.ipynb @@ -0,0 +1,886 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Running Code" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First and foremost, the Jupyter Notebook is an interactive environment for writing and running code. The notebook is capable of running code in a wide range of languages. However, each notebook is associated with a single kernel. This notebook is associated with the IPython kernel, therefore runs Python code." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Code cells allow you to enter and run code" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Run a code cell using `Shift-Enter` or pressing the button in the toolbar above:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "a = 10" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "10\n" + ] + } + ], + "source": [ + "print(a)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are two other keyboard shortcuts for running code:\n", + "\n", + "* `Alt-Enter` runs the current cell and inserts a new one below.\n", + "* `Ctrl-Enter` run the current cell and enters command mode." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Managing the Kernel" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Code is run in a separate process called the Kernel. The Kernel can be interrupted or restarted. Try running the following cell and then hit the button in the toolbar above." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import time\n", + "time.sleep(10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If the Kernel dies you will be prompted to restart it. Here we call the low-level system libc.time routine with the wrong argument via\n", + "ctypes to segfault the Python interpreter:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import sys\n", + "from ctypes import CDLL\n", + "# This will crash a Linux or Mac system\n", + "# equivalent calls can be made on Windows\n", + "\n", + "# Uncomment these lines if you would like to see the segfault\n", + "\n", + "# dll = 'dylib' if sys.platform == 'darwin' else 'so.6'\n", + "# libc = CDLL(\"libc.%s\" % dll) \n", + "# libc.time(-1) # BOOM!!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Cell menu" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The \"Cell\" menu has a number of menu items for running code in different ways. These includes:\n", + "\n", + "* Run and Select Below\n", + "* Run and Insert Below\n", + "* Run All\n", + "* Run All Above\n", + "* Run All Below" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Restarting the kernels" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The kernel maintains the state of a notebook's computations. You can reset this state by restarting the kernel. This is done by clicking on the in the toolbar above." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## sys.stdout and sys.stderr" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The stdout and stderr streams are displayed as text in the output area." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "hi, stdout\n" + ] + } + ], + "source": [ + "print(\"hi, stdout\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "hi, stderr\n" + ] + } + ], + "source": [ + "from __future__ import print_function\n", + "print('hi, stderr', file=sys.stderr)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Output is asynchronous" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All output is displayed asynchronously as it is generated in the Kernel. If you execute the next cell, you will see the output one piece at a time, not all at the end." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0\n", + "1\n", + "2\n", + "3\n", + "4\n", + "5\n", + "6\n", + "7\n" + ] + } + ], + "source": [ + "import time, sys\n", + "for i in range(8):\n", + " print(i)\n", + " time.sleep(0.5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Large outputs" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "To better handle large outputs, the output area can be collapsed. Run the following cell and then single- or double- click on the active area to the left of the output:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0\n", + "1\n", + "2\n", + "3\n", + "4\n", + "5\n", + "6\n", + "7\n", + "8\n", + "9\n", + "10\n", + "11\n", + "12\n", + "13\n", + "14\n", + "15\n", + "16\n", + "17\n", + "18\n", + "19\n", + "20\n", + "21\n", + "22\n", + "23\n", + "24\n", + "25\n", + "26\n", + "27\n", + "28\n", + "29\n", + "30\n", + "31\n", + "32\n", + "33\n", + "34\n", + "35\n", + "36\n", + "37\n", + "38\n", + "39\n", + "40\n", + "41\n", + "42\n", + "43\n", + "44\n", + "45\n", + "46\n", + "47\n", + "48\n", + "49\n" + ] + } + ], + "source": [ + "for i in range(50):\n", + " print(i)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Beyond a certain point, output will scroll automatically:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0\n", + "1\n", + "3\n", + "7\n", + "15\n", + "31\n", + "63\n", + "127\n", + "255\n", + "511\n", + "1023\n", + "2047\n", + "4095\n", + "8191\n", + "16383\n", + "32767\n", + "65535\n", + "131071\n", + "262143\n", + "524287\n", + "1048575\n", + "2097151\n", + "4194303\n", + "8388607\n", + "16777215\n", + "33554431\n", + "67108863\n", + "134217727\n", + "268435455\n", + "536870911\n", + "1073741823\n", + "2147483647\n", + "4294967295\n", + "8589934591\n", + "17179869183\n", + "34359738367\n", + "68719476735\n", + "137438953471\n", + "274877906943\n", + "549755813887\n", + "1099511627775\n", + "2199023255551\n", + "4398046511103\n", + "8796093022207\n", + "17592186044415\n", + "35184372088831\n", + "70368744177663\n", + "140737488355327\n", + "281474976710655\n", + "562949953421311\n", + "1125899906842623\n", + "2251799813685247\n", + "4503599627370495\n", + "9007199254740991\n", + "18014398509481983\n", + "36028797018963967\n", + "72057594037927935\n", + "144115188075855871\n", + "288230376151711743\n", + "576460752303423487\n", + "1152921504606846975\n", + "2305843009213693951\n", + "4611686018427387903\n", + "9223372036854775807\n", + "18446744073709551615\n", + "36893488147419103231\n", + "73786976294838206463\n", + "147573952589676412927\n", + "295147905179352825855\n", + "590295810358705651711\n", + "1180591620717411303423\n", + "2361183241434822606847\n", + "4722366482869645213695\n", + "9444732965739290427391\n", + "18889465931478580854783\n", + "37778931862957161709567\n", + "75557863725914323419135\n", + "151115727451828646838271\n", + "302231454903657293676543\n", + "604462909807314587353087\n", + "1208925819614629174706175\n", + "2417851639229258349412351\n", + "4835703278458516698824703\n", + "9671406556917033397649407\n", + "19342813113834066795298815\n", + "38685626227668133590597631\n", + "77371252455336267181195263\n", + "154742504910672534362390527\n", + "309485009821345068724781055\n", + "618970019642690137449562111\n", + "1237940039285380274899124223\n", + "2475880078570760549798248447\n", + "4951760157141521099596496895\n", + "9903520314283042199192993791\n", + "19807040628566084398385987583\n", + "39614081257132168796771975167\n", + "79228162514264337593543950335\n", + "158456325028528675187087900671\n", + "316912650057057350374175801343\n", + "633825300114114700748351602687\n", + "1267650600228229401496703205375\n", + "2535301200456458802993406410751\n", + "5070602400912917605986812821503\n", + "10141204801825835211973625643007\n", + "20282409603651670423947251286015\n", + "40564819207303340847894502572031\n", + "81129638414606681695789005144063\n", + "162259276829213363391578010288127\n", + "324518553658426726783156020576255\n", + "649037107316853453566312041152511\n", + "1298074214633706907132624082305023\n", + "2596148429267413814265248164610047\n", + "5192296858534827628530496329220095\n", + "10384593717069655257060992658440191\n", + "20769187434139310514121985316880383\n", + "41538374868278621028243970633760767\n", + "83076749736557242056487941267521535\n", + "166153499473114484112975882535043071\n", + "332306998946228968225951765070086143\n", + "664613997892457936451903530140172287\n", + "1329227995784915872903807060280344575\n", + "2658455991569831745807614120560689151\n", + "5316911983139663491615228241121378303\n", + "10633823966279326983230456482242756607\n", + "21267647932558653966460912964485513215\n", + "42535295865117307932921825928971026431\n", + "85070591730234615865843651857942052863\n", + "170141183460469231731687303715884105727\n", + "340282366920938463463374607431768211455\n", + "680564733841876926926749214863536422911\n", + "1361129467683753853853498429727072845823\n", + "2722258935367507707706996859454145691647\n", + "5444517870735015415413993718908291383295\n", + "10889035741470030830827987437816582766591\n", + "21778071482940061661655974875633165533183\n", + "43556142965880123323311949751266331066367\n", + "87112285931760246646623899502532662132735\n", + "174224571863520493293247799005065324265471\n", + "348449143727040986586495598010130648530943\n", + "696898287454081973172991196020261297061887\n", + "1393796574908163946345982392040522594123775\n", + "2787593149816327892691964784081045188247551\n", + "5575186299632655785383929568162090376495103\n", + "11150372599265311570767859136324180752990207\n", + "22300745198530623141535718272648361505980415\n", + "44601490397061246283071436545296723011960831\n", + "89202980794122492566142873090593446023921663\n", + "178405961588244985132285746181186892047843327\n", + "356811923176489970264571492362373784095686655\n", + "713623846352979940529142984724747568191373311\n", + "1427247692705959881058285969449495136382746623\n", + "2854495385411919762116571938898990272765493247\n", + "5708990770823839524233143877797980545530986495\n", + "11417981541647679048466287755595961091061972991\n", + "22835963083295358096932575511191922182123945983\n", + "45671926166590716193865151022383844364247891967\n", + "91343852333181432387730302044767688728495783935\n", + "182687704666362864775460604089535377456991567871\n", + "365375409332725729550921208179070754913983135743\n", + "730750818665451459101842416358141509827966271487\n", + "1461501637330902918203684832716283019655932542975\n", + "2923003274661805836407369665432566039311865085951\n", + "5846006549323611672814739330865132078623730171903\n", + "11692013098647223345629478661730264157247460343807\n", + "23384026197294446691258957323460528314494920687615\n", + "46768052394588893382517914646921056628989841375231\n", + "93536104789177786765035829293842113257979682750463\n", + "187072209578355573530071658587684226515959365500927\n", + "374144419156711147060143317175368453031918731001855\n", + "748288838313422294120286634350736906063837462003711\n", + "1496577676626844588240573268701473812127674924007423\n", + "2993155353253689176481146537402947624255349848014847\n", + "5986310706507378352962293074805895248510699696029695\n", + "11972621413014756705924586149611790497021399392059391\n", + "23945242826029513411849172299223580994042798784118783\n", + "47890485652059026823698344598447161988085597568237567\n", + "95780971304118053647396689196894323976171195136475135\n", + "191561942608236107294793378393788647952342390272950271\n", + "383123885216472214589586756787577295904684780545900543\n", + "766247770432944429179173513575154591809369561091801087\n", + "1532495540865888858358347027150309183618739122183602175\n", + "3064991081731777716716694054300618367237478244367204351\n", + "6129982163463555433433388108601236734474956488734408703\n", + "12259964326927110866866776217202473468949912977468817407\n", + "24519928653854221733733552434404946937899825954937634815\n", + "49039857307708443467467104868809893875799651909875269631\n", + "98079714615416886934934209737619787751599303819750539263\n", + "196159429230833773869868419475239575503198607639501078527\n", + "392318858461667547739736838950479151006397215279002157055\n", + "784637716923335095479473677900958302012794430558004314111\n", + "1569275433846670190958947355801916604025588861116008628223\n", + "3138550867693340381917894711603833208051177722232017256447\n", + "6277101735386680763835789423207666416102355444464034512895\n", + "12554203470773361527671578846415332832204710888928069025791\n", + "25108406941546723055343157692830665664409421777856138051583\n", + "50216813883093446110686315385661331328818843555712276103167\n", + "100433627766186892221372630771322662657637687111424552206335\n", + "200867255532373784442745261542645325315275374222849104412671\n", + "401734511064747568885490523085290650630550748445698208825343\n", + "803469022129495137770981046170581301261101496891396417650687\n", + "1606938044258990275541962092341162602522202993782792835301375\n", + "3213876088517980551083924184682325205044405987565585670602751\n", + "6427752177035961102167848369364650410088811975131171341205503\n", + "12855504354071922204335696738729300820177623950262342682411007\n", + "25711008708143844408671393477458601640355247900524685364822015\n", + "51422017416287688817342786954917203280710495801049370729644031\n", + "102844034832575377634685573909834406561420991602098741459288063\n", + "205688069665150755269371147819668813122841983204197482918576127\n", + "411376139330301510538742295639337626245683966408394965837152255\n", + "822752278660603021077484591278675252491367932816789931674304511\n", + "1645504557321206042154969182557350504982735865633579863348609023\n", + "3291009114642412084309938365114701009965471731267159726697218047\n", + "6582018229284824168619876730229402019930943462534319453394436095\n", + "13164036458569648337239753460458804039861886925068638906788872191\n", + "26328072917139296674479506920917608079723773850137277813577744383\n", + "52656145834278593348959013841835216159447547700274555627155488767\n", + "105312291668557186697918027683670432318895095400549111254310977535\n", + "210624583337114373395836055367340864637790190801098222508621955071\n", + "421249166674228746791672110734681729275580381602196445017243910143\n", + "842498333348457493583344221469363458551160763204392890034487820287\n", + "1684996666696914987166688442938726917102321526408785780068975640575\n", + "3369993333393829974333376885877453834204643052817571560137951281151\n", + "6739986666787659948666753771754907668409286105635143120275902562303\n", + "13479973333575319897333507543509815336818572211270286240551805124607\n", + "26959946667150639794667015087019630673637144422540572481103610249215\n", + "53919893334301279589334030174039261347274288845081144962207220498431\n", + "107839786668602559178668060348078522694548577690162289924414440996863\n", + "215679573337205118357336120696157045389097155380324579848828881993727\n", + "431359146674410236714672241392314090778194310760649159697657763987455\n", + "862718293348820473429344482784628181556388621521298319395315527974911\n", + "1725436586697640946858688965569256363112777243042596638790631055949823\n", + "3450873173395281893717377931138512726225554486085193277581262111899647\n", + "6901746346790563787434755862277025452451108972170386555162524223799295\n", + "13803492693581127574869511724554050904902217944340773110325048447598591\n", + "27606985387162255149739023449108101809804435888681546220650096895197183\n", + "55213970774324510299478046898216203619608871777363092441300193790394367\n", + "110427941548649020598956093796432407239217743554726184882600387580788735\n", + "220855883097298041197912187592864814478435487109452369765200775161577471\n", + "441711766194596082395824375185729628956870974218904739530401550323154943\n", + "883423532389192164791648750371459257913741948437809479060803100646309887\n", + "1766847064778384329583297500742918515827483896875618958121606201292619775\n", + "3533694129556768659166595001485837031654967793751237916243212402585239551\n", + "7067388259113537318333190002971674063309935587502475832486424805170479103\n", + "14134776518227074636666380005943348126619871175004951664972849610340958207\n", + "28269553036454149273332760011886696253239742350009903329945699220681916415\n", + "56539106072908298546665520023773392506479484700019806659891398441363832831\n", + "113078212145816597093331040047546785012958969400039613319782796882727665663\n", + "226156424291633194186662080095093570025917938800079226639565593765455331327\n", + "452312848583266388373324160190187140051835877600158453279131187530910662655\n", + "904625697166532776746648320380374280103671755200316906558262375061821325311\n", + "1809251394333065553493296640760748560207343510400633813116524750123642650623\n", + "3618502788666131106986593281521497120414687020801267626233049500247285301247\n", + "7237005577332262213973186563042994240829374041602535252466099000494570602495\n", + "14474011154664524427946373126085988481658748083205070504932198000989141204991\n", + "28948022309329048855892746252171976963317496166410141009864396001978282409983\n", + "57896044618658097711785492504343953926634992332820282019728792003956564819967\n", + "115792089237316195423570985008687907853269984665640564039457584007913129639935\n", + "231584178474632390847141970017375815706539969331281128078915168015826259279871\n", + "463168356949264781694283940034751631413079938662562256157830336031652518559743\n", + "926336713898529563388567880069503262826159877325124512315660672063305037119487\n", + "1852673427797059126777135760139006525652319754650249024631321344126610074238975\n", + "3705346855594118253554271520278013051304639509300498049262642688253220148477951\n", + "7410693711188236507108543040556026102609279018600996098525285376506440296955903\n", + "14821387422376473014217086081112052205218558037201992197050570753012880593911807\n", + "29642774844752946028434172162224104410437116074403984394101141506025761187823615\n", + "59285549689505892056868344324448208820874232148807968788202283012051522375647231\n", + "118571099379011784113736688648896417641748464297615937576404566024103044751294463\n", + "237142198758023568227473377297792835283496928595231875152809132048206089502588927\n", + "474284397516047136454946754595585670566993857190463750305618264096412179005177855\n", + "948568795032094272909893509191171341133987714380927500611236528192824358010355711\n", + "1897137590064188545819787018382342682267975428761855001222473056385648716020711423\n", + "3794275180128377091639574036764685364535950857523710002444946112771297432041422847\n", + "7588550360256754183279148073529370729071901715047420004889892225542594864082845695\n", + "15177100720513508366558296147058741458143803430094840009779784451085189728165691391\n", + "30354201441027016733116592294117482916287606860189680019559568902170379456331382783\n", + "60708402882054033466233184588234965832575213720379360039119137804340758912662765567\n", + "121416805764108066932466369176469931665150427440758720078238275608681517825325531135\n", + "242833611528216133864932738352939863330300854881517440156476551217363035650651062271\n", + "485667223056432267729865476705879726660601709763034880312953102434726071301302124543\n", + "971334446112864535459730953411759453321203419526069760625906204869452142602604249087\n", + "1942668892225729070919461906823518906642406839052139521251812409738904285205208498175\n", + "3885337784451458141838923813647037813284813678104279042503624819477808570410416996351\n", + "7770675568902916283677847627294075626569627356208558085007249638955617140820833992703\n", + "15541351137805832567355695254588151253139254712417116170014499277911234281641667985407\n", + "31082702275611665134711390509176302506278509424834232340028998555822468563283335970815\n", + "62165404551223330269422781018352605012557018849668464680057997111644937126566671941631\n", + "124330809102446660538845562036705210025114037699336929360115994223289874253133343883263\n", + "248661618204893321077691124073410420050228075398673858720231988446579748506266687766527\n", + "497323236409786642155382248146820840100456150797347717440463976893159497012533375533055\n", + "994646472819573284310764496293641680200912301594695434880927953786318994025066751066111\n", + "1989292945639146568621528992587283360401824603189390869761855907572637988050133502132223\n", + "3978585891278293137243057985174566720803649206378781739523711815145275976100267004264447\n", + "7957171782556586274486115970349133441607298412757563479047423630290551952200534008528895\n", + "15914343565113172548972231940698266883214596825515126958094847260581103904401068017057791\n", + "31828687130226345097944463881396533766429193651030253916189694521162207808802136034115583\n", + "63657374260452690195888927762793067532858387302060507832379389042324415617604272068231167\n", + "127314748520905380391777855525586135065716774604121015664758778084648831235208544136462335\n", + "254629497041810760783555711051172270131433549208242031329517556169297662470417088272924671\n", + "509258994083621521567111422102344540262867098416484062659035112338595324940834176545849343\n", + "1018517988167243043134222844204689080525734196832968125318070224677190649881668353091698687\n", + "2037035976334486086268445688409378161051468393665936250636140449354381299763336706183397375\n", + "4074071952668972172536891376818756322102936787331872501272280898708762599526673412366794751\n", + "8148143905337944345073782753637512644205873574663745002544561797417525199053346824733589503\n", + "16296287810675888690147565507275025288411747149327490005089123594835050398106693649467179007\n", + "32592575621351777380295131014550050576823494298654980010178247189670100796213387298934358015\n", + "65185151242703554760590262029100101153646988597309960020356494379340201592426774597868716031\n", + "130370302485407109521180524058200202307293977194619920040712988758680403184853549195737432063\n", + "260740604970814219042361048116400404614587954389239840081425977517360806369707098391474864127\n", + "521481209941628438084722096232800809229175908778479680162851955034721612739414196782949728255\n", + "1042962419883256876169444192465601618458351817556959360325703910069443225478828393565899456511\n", + "2085924839766513752338888384931203236916703635113918720651407820138886450957656787131798913023\n", + "4171849679533027504677776769862406473833407270227837441302815640277772901915313574263597826047\n", + "8343699359066055009355553539724812947666814540455674882605631280555545803830627148527195652095\n", + "16687398718132110018711107079449625895333629080911349765211262561111091607661254297054391304191\n", + "33374797436264220037422214158899251790667258161822699530422525122222183215322508594108782608383\n", + "66749594872528440074844428317798503581334516323645399060845050244444366430645017188217565216767\n", + "133499189745056880149688856635597007162669032647290798121690100488888732861290034376435130433535\n", + "266998379490113760299377713271194014325338065294581596243380200977777465722580068752870260867071\n", + "533996758980227520598755426542388028650676130589163192486760401955554931445160137505740521734143\n", + "1067993517960455041197510853084776057301352261178326384973520803911109862890320275011481043468287\n", + "2135987035920910082395021706169552114602704522356652769947041607822219725780640550022962086936575\n", + "4271974071841820164790043412339104229205409044713305539894083215644439451561281100045924173873151\n", + "8543948143683640329580086824678208458410818089426611079788166431288878903122562200091848347746303\n", + "17087896287367280659160173649356416916821636178853222159576332862577757806245124400183696695492607\n", + "34175792574734561318320347298712833833643272357706444319152665725155515612490248800367393390985215\n", + "68351585149469122636640694597425667667286544715412888638305331450311031224980497600734786781970431\n", + "136703170298938245273281389194851335334573089430825777276610662900622062449960995201469573563940863\n", + "273406340597876490546562778389702670669146178861651554553221325801244124899921990402939147127881727\n", + "546812681195752981093125556779405341338292357723303109106442651602488249799843980805878294255763455\n", + "1093625362391505962186251113558810682676584715446606218212885303204976499599687961611756588511526911\n", + "2187250724783011924372502227117621365353169430893212436425770606409952999199375923223513177023053823\n", + "4374501449566023848745004454235242730706338861786424872851541212819905998398751846447026354046107647\n", + "8749002899132047697490008908470485461412677723572849745703082425639811996797503692894052708092215295\n", + "17498005798264095394980017816940970922825355447145699491406164851279623993595007385788105416184430591\n", + "34996011596528190789960035633881941845650710894291398982812329702559247987190014771576210832368861183\n", + "69992023193056381579920071267763883691301421788582797965624659405118495974380029543152421664737722367\n", + "139984046386112763159840142535527767382602843577165595931249318810236991948760059086304843329475444735\n", + "279968092772225526319680285071055534765205687154331191862498637620473983897520118172609686658950889471\n", + "559936185544451052639360570142111069530411374308662383724997275240947967795040236345219373317901778943\n", + "1119872371088902105278721140284222139060822748617324767449994550481895935590080472690438746635803557887\n", + "2239744742177804210557442280568444278121645497234649534899989100963791871180160945380877493271607115775\n", + "4479489484355608421114884561136888556243290994469299069799978201927583742360321890761754986543214231551\n", + "8958978968711216842229769122273777112486581988938598139599956403855167484720643781523509973086428463103\n", + "17917957937422433684459538244547554224973163977877196279199912807710334969441287563047019946172856926207\n", + "35835915874844867368919076489095108449946327955754392558399825615420669938882575126094039892345713852415\n", + "71671831749689734737838152978190216899892655911508785116799651230841339877765150252188079784691427704831\n", + "143343663499379469475676305956380433799785311823017570233599302461682679755530300504376159569382855409663\n", + "286687326998758938951352611912760867599570623646035140467198604923365359511060601008752319138765710819327\n", + "573374653997517877902705223825521735199141247292070280934397209846730719022121202017504638277531421638655\n", + "1146749307995035755805410447651043470398282494584140561868794419693461438044242404035009276555062843277311\n", + "2293498615990071511610820895302086940796564989168281123737588839386922876088484808070018553110125686554623\n", + "4586997231980143023221641790604173881593129978336562247475177678773845752176969616140037106220251373109247\n", + "9173994463960286046443283581208347763186259956673124494950355357547691504353939232280074212440502746218495\n", + "18347988927920572092886567162416695526372519913346248989900710715095383008707878464560148424881005492436991\n", + "36695977855841144185773134324833391052745039826692497979801421430190766017415756929120296849762010984873983\n", + "73391955711682288371546268649666782105490079653384995959602842860381532034831513858240593699524021969747967\n", + "146783911423364576743092537299333564210980159306769991919205685720763064069663027716481187399048043939495935\n", + "293567822846729153486185074598667128421960318613539983838411371441526128139326055432962374798096087878991871\n", + "587135645693458306972370149197334256843920637227079967676822742883052256278652110865924749596192175757983743\n", + "1174271291386916613944740298394668513687841274454159935353645485766104512557304221731849499192384351515967487\n", + "2348542582773833227889480596789337027375682548908319870707290971532209025114608443463698998384768703031934975\n", + "4697085165547666455778961193578674054751365097816639741414581943064418050229216886927397996769537406063869951\n", + "9394170331095332911557922387157348109502730195633279482829163886128836100458433773854795993539074812127739903\n", + "18788340662190665823115844774314696219005460391266558965658327772257672200916867547709591987078149624255479807\n", + "37576681324381331646231689548629392438010920782533117931316655544515344401833735095419183974156299248510959615\n", + "75153362648762663292463379097258784876021841565066235862633311089030688803667470190838367948312598497021919231\n", + "150306725297525326584926758194517569752043683130132471725266622178061377607334940381676735896625196994043838463\n", + "300613450595050653169853516389035139504087366260264943450533244356122755214669880763353471793250393988087676927\n", + "601226901190101306339707032778070279008174732520529886901066488712245510429339761526706943586500787976175353855\n", + "1202453802380202612679414065556140558016349465041059773802132977424491020858679523053413887173001575952350707711\n", + "2404907604760405225358828131112281116032698930082119547604265954848982041717359046106827774346003151904701415423\n", + "4809815209520810450717656262224562232065397860164239095208531909697964083434718092213655548692006303809402830847\n", + "9619630419041620901435312524449124464130795720328478190417063819395928166869436184427311097384012607618805661695\n", + "19239260838083241802870625048898248928261591440656956380834127638791856333738872368854622194768025215237611323391\n", + "38478521676166483605741250097796497856523182881313912761668255277583712667477744737709244389536050430475222646783\n", + "76957043352332967211482500195592995713046365762627825523336510555167425334955489475418488779072100860950445293567\n", + "153914086704665934422965000391185991426092731525255651046673021110334850669910978950836977558144201721900890587135\n", + "307828173409331868845930000782371982852185463050511302093346042220669701339821957901673955116288403443801781174271\n", + "615656346818663737691860001564743965704370926101022604186692084441339402679643915803347910232576806887603562348543\n", + "1231312693637327475383720003129487931408741852202045208373384168882678805359287831606695820465153613775207124697087\n", + "2462625387274654950767440006258975862817483704404090416746768337765357610718575663213391640930307227550414249394175\n", + "4925250774549309901534880012517951725634967408808180833493536675530715221437151326426783281860614455100828498788351\n", + "9850501549098619803069760025035903451269934817616361666987073351061430442874302652853566563721228910201656997576703\n", + "19701003098197239606139520050071806902539869635232723333974146702122860885748605305707133127442457820403313995153407\n", + "39402006196394479212279040100143613805079739270465446667948293404245721771497210611414266254884915640806627990306815\n", + "78804012392788958424558080200287227610159478540930893335896586808491443542994421222828532509769831281613255980613631\n", + "157608024785577916849116160400574455220318957081861786671793173616982887085988842445657065019539662563226511961227263\n", + "315216049571155833698232320801148910440637914163723573343586347233965774171977684891314130039079325126453023922454527\n", + "630432099142311667396464641602297820881275828327447146687172694467931548343955369782628260078158650252906047844909055\n", + "1260864198284623334792929283204595641762551656654894293374345388935863096687910739565256520156317300505812095689818111\n", + "2521728396569246669585858566409191283525103313309788586748690777871726193375821479130513040312634601011624191379636223\n", + "5043456793138493339171717132818382567050206626619577173497381555743452386751642958261026080625269202023248382759272447\n", + "10086913586276986678343434265636765134100413253239154346994763111486904773503285916522052161250538404046496765518544895\n", + "20173827172553973356686868531273530268200826506478308693989526222973809547006571833044104322501076808092993531037089791\n", + "40347654345107946713373737062547060536401653012956617387979052445947619094013143666088208645002153616185987062074179583\n", + "80695308690215893426747474125094121072803306025913234775958104891895238188026287332176417290004307232371974124148359167\n", + "161390617380431786853494948250188242145606612051826469551916209783790476376052574664352834580008614464743948248296718335\n", + "322781234760863573706989896500376484291213224103652939103832419567580952752105149328705669160017228929487896496593436671\n", + "645562469521727147413979793000752968582426448207305878207664839135161905504210298657411338320034457858975792993186873343\n", + "1291124939043454294827959586001505937164852896414611756415329678270323811008420597314822676640068915717951585986373746687\n", + "2582249878086908589655919172003011874329705792829223512830659356540647622016841194629645353280137831435903171972747493375\n", + "5164499756173817179311838344006023748659411585658447025661318713081295244033682389259290706560275662871806343945494986751\n", + "10328999512347634358623676688012047497318823171316894051322637426162590488067364778518581413120551325743612687890989973503\n", + "20657999024695268717247353376024094994637646342633788102645274852325180976134729557037162826241102651487225375781979947007\n", + "41315998049390537434494706752048189989275292685267576205290549704650361952269459114074325652482205302974450751563959894015\n", + "82631996098781074868989413504096379978550585370535152410581099409300723904538918228148651304964410605948901503127919788031\n", + "165263992197562149737978827008192759957101170741070304821162198818601447809077836456297302609928821211897803006255839576063\n", + "330527984395124299475957654016385519914202341482140609642324397637202895618155672912594605219857642423795606012511679152127\n", + "661055968790248598951915308032771039828404682964281219284648795274405791236311345825189210439715284847591212025023358304255\n", + "1322111937580497197903830616065542079656809365928562438569297590548811582472622691650378420879430569695182424050046716608511\n", + "2644223875160994395807661232131084159313618731857124877138595181097623164945245383300756841758861139390364848100093433217023\n", + "5288447750321988791615322464262168318627237463714249754277190362195246329890490766601513683517722278780729696200186866434047\n", + "10576895500643977583230644928524336637254474927428499508554380724390492659780981533203027367035444557561459392400373732868095\n", + "21153791001287955166461289857048673274508949854856999017108761448780985319561963066406054734070889115122918784800747465736191\n", + "42307582002575910332922579714097346549017899709713998034217522897561970639123926132812109468141778230245837569601494931472383\n", + "84615164005151820665845159428194693098035799419427996068435045795123941278247852265624218936283556460491675139202989862944767\n", + "169230328010303641331690318856389386196071598838855992136870091590247882556495704531248437872567112920983350278405979725889535\n", + "338460656020607282663380637712778772392143197677711984273740183180495765112991409062496875745134225841966700556811959451779071\n", + "676921312041214565326761275425557544784286395355423968547480366360991530225982818124993751490268451683933401113623918903558143\n", + "1353842624082429130653522550851115089568572790710847937094960732721983060451965636249987502980536903367866802227247837807116287\n", + "2707685248164858261307045101702230179137145581421695874189921465443966120903931272499975005961073806735733604454495675614232575\n", + "5415370496329716522614090203404460358274291162843391748379842930887932241807862544999950011922147613471467208908991351228465151\n", + "10830740992659433045228180406808920716548582325686783496759685861775864483615725089999900023844295226942934417817982702456930303\n", + "21661481985318866090456360813617841433097164651373566993519371723551728967231450179999800047688590453885868835635965404913860607\n", + "43322963970637732180912721627235682866194329302747133987038743447103457934462900359999600095377180907771737671271930809827721215\n", + "86645927941275464361825443254471365732388658605494267974077486894206915868925800719999200190754361815543475342543861619655442431\n", + "173291855882550928723650886508942731464777317210988535948154973788413831737851601439998400381508723631086950685087723239310884863\n", + "346583711765101857447301773017885462929554634421977071896309947576827663475703202879996800763017447262173901370175446478621769727\n", + "693167423530203714894603546035770925859109268843954143792619895153655326951406405759993601526034894524347802740350892957243539455\n", + "1386334847060407429789207092071541851718218537687908287585239790307310653902812811519987203052069789048695605480701785914487078911\n", + "2772669694120814859578414184143083703436437075375816575170479580614621307805625623039974406104139578097391210961403571828974157823\n", + "5545339388241629719156828368286167406872874150751633150340959161229242615611251246079948812208279156194782421922807143657948315647\n", + "11090678776483259438313656736572334813745748301503266300681918322458485231222502492159897624416558312389564843845614287315896631295\n", + "22181357552966518876627313473144669627491496603006532601363836644916970462445004984319795248833116624779129687691228574631793262591\n", + "44362715105933037753254626946289339254982993206013065202727673289833940924890009968639590497666233249558259375382457149263586525183\n", + "88725430211866075506509253892578678509965986412026130405455346579667881849780019937279180995332466499116518750764914298527173050367\n", + "177450860423732151013018507785157357019931972824052260810910693159335763699560039874558361990664932998233037501529828597054346100735\n", + "354901720847464302026037015570314714039863945648104521621821386318671527399120079749116723981329865996466075003059657194108692201471\n", + "709803441694928604052074031140629428079727891296209043243642772637343054798240159498233447962659731992932150006119314388217384402943\n", + "1419606883389857208104148062281258856159455782592418086487285545274686109596480318996466895925319463985864300012238628776434768805887\n", + "2839213766779714416208296124562517712318911565184836172974571090549372219192960637992933791850638927971728600024477257552869537611775\n", + "5678427533559428832416592249125035424637823130369672345949142181098744438385921275985867583701277855943457200048954515105739075223551\n", + "11356855067118857664833184498250070849275646260739344691898284362197488876771842551971735167402555711886914400097909030211478150447103\n", + "22713710134237715329666368996500141698551292521478689383796568724394977753543685103943470334805111423773828800195818060422956300894207\n", + "45427420268475430659332737993000283397102585042957378767593137448789955507087370207886940669610222847547657600391636120845912601788415\n", + "90854840536950861318665475986000566794205170085914757535186274897579911014174740415773881339220445695095315200783272241691825203576831\n", + "181709681073901722637330951972001133588410340171829515070372549795159822028349480831547762678440891390190630401566544483383650407153663\n", + "363419362147803445274661903944002267176820680343659030140745099590319644056698961663095525356881782780381260803133088966767300814307327\n", + "726838724295606890549323807888004534353641360687318060281490199180639288113397923326191050713763565560762521606266177933534601628614655\n", + "1453677448591213781098647615776009068707282721374636120562980398361278576226795846652382101427527131121525043212532355867069203257229311\n", + "2907354897182427562197295231552018137414565442749272241125960796722557152453591693304764202855054262243050086425064711734138406514458623\n", + "5814709794364855124394590463104036274829130885498544482251921593445114304907183386609528405710108524486100172850129423468276813028917247\n", + "11629419588729710248789180926208072549658261770997088964503843186890228609814366773219056811420217048972200345700258846936553626057834495\n", + "23258839177459420497578361852416145099316523541994177929007686373780457219628733546438113622840434097944400691400517693873107252115668991\n", + "46517678354918840995156723704832290198633047083988355858015372747560914439257467092876227245680868195888801382801035387746214504231337983\n", + "93035356709837681990313447409664580397266094167976711716030745495121828878514934185752454491361736391777602765602070775492429008462675967\n", + "186070713419675363980626894819329160794532188335953423432061490990243657757029868371504908982723472783555205531204141550984858016925351935\n", + "372141426839350727961253789638658321589064376671906846864122981980487315514059736743009817965446945567110411062408283101969716033850703871\n", + "744282853678701455922507579277316643178128753343813693728245963960974631028119473486019635930893891134220822124816566203939432067701407743\n", + "1488565707357402911845015158554633286356257506687627387456491927921949262056238946972039271861787782268441644249633132407878864135402815487\n", + "2977131414714805823690030317109266572712515013375254774912983855843898524112477893944078543723575564536883288499266264815757728270805630975\n", + "5954262829429611647380060634218533145425030026750509549825967711687797048224955787888157087447151129073766576998532529631515456541611261951\n", + "11908525658859223294760121268437066290850060053501019099651935423375594096449911575776314174894302258147533153997065059263030913083222523903\n", + "23817051317718446589520242536874132581700120107002038199303870846751188192899823151552628349788604516295066307994130118526061826166445047807\n", + "47634102635436893179040485073748265163400240214004076398607741693502376385799646303105256699577209032590132615988260237052123652332890095615\n", + "95268205270873786358080970147496530326800480428008152797215483387004752771599292606210513399154418065180265231976520474104247304665780191231\n", + "190536410541747572716161940294993060653600960856016305594430966774009505543198585212421026798308836130360530463953040948208494609331560382463\n", + "381072821083495145432323880589986121307201921712032611188861933548019011086397170424842053596617672260721060927906081896416989218663120764927\n", + "762145642166990290864647761179972242614403843424065222377723867096038022172794340849684107193235344521442121855812163792833978437326241529855\n", + "1524291284333980581729295522359944485228807686848130444755447734192076044345588681699368214386470689042884243711624327585667956874652483059711\n", + "3048582568667961163458591044719888970457615373696260889510895468384152088691177363398736428772941378085768487423248655171335913749304966119423\n", + "6097165137335922326917182089439777940915230747392521779021790936768304177382354726797472857545882756171536974846497310342671827498609932238847\n", + "12194330274671844653834364178879555881830461494785043558043581873536608354764709453594945715091765512343073949692994620685343654997219864477695\n", + "24388660549343689307668728357759111763660922989570087116087163747073216709529418907189891430183531024686147899385989241370687309994439728955391\n", + "48777321098687378615337456715518223527321845979140174232174327494146433419058837814379782860367062049372295798771978482741374619988879457910783\n", + "97554642197374757230674913431036447054643691958280348464348654988292866838117675628759565720734124098744591597543956965482749239977758915821567\n", + "195109284394749514461349826862072894109287383916560696928697309976585733676235351257519131441468248197489183195087913930965498479955517831643135\n", + "390218568789499028922699653724145788218574767833121393857394619953171467352470702515038262882936496394978366390175827861930996959911035663286271\n", + "780437137578998057845399307448291576437149535666242787714789239906342934704941405030076525765872992789956732780351655723861993919822071326572543\n", + "1560874275157996115690798614896583152874299071332485575429578479812685869409882810060153051531745985579913465560703311447723987839644142653145087\n", + "3121748550315992231381597229793166305748598142664971150859156959625371738819765620120306103063491971159826931121406622895447975679288285306290175\n", + "6243497100631984462763194459586332611497196285329942301718313919250743477639531240240612206126983942319653862242813245790895951358576570612580351\n", + "12486994201263968925526388919172665222994392570659884603436627838501486955279062480481224412253967884639307724485626491581791902717153141225160703\n", + "24973988402527937851052777838345330445988785141319769206873255677002973910558124960962448824507935769278615448971252983163583805434306282450321407\n", + "49947976805055875702105555676690660891977570282639538413746511354005947821116249921924897649015871538557230897942505966327167610868612564900642815\n", + "99895953610111751404211111353381321783955140565279076827493022708011895642232499843849795298031743077114461795885011932654335221737225129801285631\n", + "199791907220223502808422222706762643567910281130558153654986045416023791284464999687699590596063486154228923591770023865308670443474450259602571263\n", + "399583814440447005616844445413525287135820562261116307309972090832047582568929999375399181192126972308457847183540047730617340886948900519205142527\n", + "799167628880894011233688890827050574271641124522232614619944181664095165137859998750798362384253944616915694367080095461234681773897801038410285055\n", + "1598335257761788022467377781654101148543282249044465229239888363328190330275719997501596724768507889233831388734160190922469363547795602076820570111\n", + "3196670515523576044934755563308202297086564498088930458479776726656380660551439995003193449537015778467662777468320381844938727095591204153641140223\n", + "6393341031047152089869511126616404594173128996177860916959553453312761321102879990006386899074031556935325554936640763689877454191182408307282280447\n", + "12786682062094304179739022253232809188346257992355721833919106906625522642205759980012773798148063113870651109873281527379754908382364816614564560895\n", + "25573364124188608359478044506465618376692515984711443667838213813251045284411519960025547596296126227741302219746563054759509816764729633229129121791\n", + "51146728248377216718956089012931236753385031969422887335676427626502090568823039920051095192592252455482604439493126109519019633529459266458258243583\n", + "102293456496754433437912178025862473506770063938845774671352855253004181137646079840102190385184504910965208878986252219038039267058918532916516487167\n", + "204586912993508866875824356051724947013540127877691549342705710506008362275292159680204380770369009821930417757972504438076078534117837065833032974335\n", + "409173825987017733751648712103449894027080255755383098685411421012016724550584319360408761540738019643860835515945008876152157068235674131666065948671\n", + "818347651974035467503297424206899788054160511510766197370822842024033449101168638720817523081476039287721671031890017752304314136471348263332131897343\n", + "1636695303948070935006594848413799576108321023021532394741645684048066898202337277441635046162952078575443342063780035504608628272942696526664263794687\n" + ] + } + ], + "source": [ + "for i in range(500):\n", + " print(2**i - 1)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.5.1" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/tests/fixtures/real/openai_cookbook_assistants_api_overview_42bc4ed.ipynb b/tests/fixtures/real/openai_cookbook_assistants_api_overview_42bc4ed.ipynb new file mode 100644 index 0000000..dc3a74a --- /dev/null +++ b/tests/fixtures/real/openai_cookbook_assistants_api_overview_42bc4ed.ipynb @@ -0,0 +1,1768 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Assistants API Overview (Python SDK)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The new [Assistants API](https://platform.openai.com/docs/assistants/overview) is a stateful evolution of our [Chat Completions API](https://platform.openai.com/docs/guides/text-generation/chat-completions-api) meant to simplify the creation of assistant-like experiences, and enable developer access to powerful tools like Code Interpreter and File Search." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Assistants API Diagram](../images/assistants_overview_diagram.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Chat Completions API vs Assistants API\n", + "\n", + "The primitives of the **Chat Completions API** are `Messages`, on which you perform a `Completion` with a `Model` (`gpt-4o`, `gpt-4o-mini`, etc). It is lightweight and powerful, but inherently stateless, which means you have to manage conversation state, tool definitions, retrieval documents, and code execution manually.\n", + "\n", + "The primitives of the **Assistants API** are\n", + "\n", + "- `Assistants`, which encapsulate a base model, instructions, tools, and (context) documents,\n", + "- `Threads`, which represent the state of a conversation, and\n", + "- `Runs`, which power the execution of an `Assistant` on a `Thread`, including textual responses and multi-step tool use.\n", + "\n", + "We'll take a look at how these can be used to create powerful, stateful experiences.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "### Python SDK\n", + "\n", + "> **Note**\n", + "> We've updated our [Python SDK](https://github.com/openai/openai-python) to add support for the Assistants API, so you'll need to update it to the latest version (`1.59.4` at time of writing).\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: openai in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (1.59.4)\n", + "Requirement already satisfied: anyio<5,>=3.5.0 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (3.7.1)\n", + "Requirement already satisfied: distro<2,>=1.7.0 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (1.9.0)\n", + "Requirement already satisfied: httpx<1,>=0.23.0 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (0.27.0)\n", + "Requirement already satisfied: jiter<1,>=0.4.0 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (0.7.0)\n", + "Requirement already satisfied: pydantic<3,>=1.9.0 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (2.8.2)\n", + "Requirement already satisfied: sniffio in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (1.3.1)\n", + "Requirement already satisfied: tqdm>4 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (4.66.4)\n", + "Requirement already satisfied: typing-extensions<5,>=4.11 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from openai) (4.12.2)\n", + "Requirement already satisfied: idna>=2.8 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from anyio<5,>=3.5.0->openai) (3.7)\n", + "Requirement already satisfied: certifi in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from httpx<1,>=0.23.0->openai) (2024.7.4)\n", + "Requirement already satisfied: httpcore==1.* in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from httpx<1,>=0.23.0->openai) (1.0.5)\n", + "Requirement already satisfied: h11<0.15,>=0.13 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from httpcore==1.*->httpx<1,>=0.23.0->openai) (0.14.0)\n", + "Requirement already satisfied: annotated-types>=0.4.0 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from pydantic<3,>=1.9.0->openai) (0.7.0)\n", + "Requirement already satisfied: pydantic-core==2.20.1 in /Users/lee.spacagna/myenv/lib/python3.12/site-packages (from pydantic<3,>=1.9.0->openai) (2.20.1)\n" + ] + } + ], + "source": [ + "!pip install --upgrade openai" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And make sure it's up to date by running:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Version: 1.59.4\n" + ] + } + ], + "source": [ + "!pip show openai | grep Version" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Pretty Printing Helper\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "\n", + "def show_json(obj):\n", + " display(json.loads(obj.model_dump_json()))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Complete Example with Assistants API\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Assistants\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The easiest way to get started with the Assistants API is through the [Assistants Playground](https://platform.openai.com/playground).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Assistants Playground](../images/assistants_overview_assistants_playground.png)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's begin by creating an assistant! We'll create a Math Tutor just like in our [docs](https://platform.openai.com/docs/assistants/overview).\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Creating New Assistant](../images/assistants_overview_new_assistant.png)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can also create Assistants directly through the Assistants API, like so:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',\n", + " 'created_at': 1736340398,\n", + " 'description': None,\n", + " 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',\n", + " 'metadata': {},\n", + " 'model': 'gpt-4o',\n", + " 'name': 'Math Tutor',\n", + " 'object': 'assistant',\n", + " 'tools': [],\n", + " 'response_format': 'auto',\n", + " 'temperature': 1.0,\n", + " 'tool_resources': {'code_interpreter': None, 'file_search': None},\n", + " 'top_p': 1.0}", + " 'tools': [],\n", + " 'response_format': 'auto',\n", + " 'temperature': 1.0,\n", + " 'tool_resources': {'code_interpreter': None, 'file_search': None},\n", + " 'top_p': 1.0}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from openai import OpenAI\n", + "import os\n", + "\n", + "client = OpenAI(api_key=os.environ.get(\"OPENAI_API_KEY\", \"\"))\n", + "\n", + "\n", + "assistant = client.beta.assistants.create(\n", + " name=\"Math Tutor\",\n", + " instructions=\"You are a personal math tutor. Answer questions briefly, in a sentence or less.\",\n", + " model=\"gpt-4o\",\n", + ")\n", + "show_json(assistant)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Regardless of whether you create your Assistant through the Dashboard or with the API, you'll want to keep track of the Assistant ID. This is how you'll refer to your Assistant throughout Threads and Runs.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we'll create a new Thread and add a Message to it. This will hold the state of our conversation, so we don't have re-send the entire message history each time.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Threads\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create a new thread:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': 'thread_j4dc1TiHPfkviKUHNi4aAsA6',\n", + " 'created_at': 1736340398,\n", + " 'metadata': {},\n", + " 'object': 'thread',\n", + " 'tool_resources': {'code_interpreter': None, 'file_search': None}}", + " 'object': 'thread',\n", + " 'tool_resources': {'code_interpreter': None, 'file_search': None}}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "thread = client.beta.threads.create()\n", + "show_json(thread)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then add the Message to the thread:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': 'msg_1q4Y7ZZ9gIcPoAKSx9UtrrKJ',\n", + " 'assistant_id': None,\n", + " 'attachments': [],\n", + " 'completed_at': None,\n", + " 'attachments': [],\n", + " 'completed_at': None,\n", + " 'content': [{'text': {'annotations': [],\n", + " 'value': 'I need to solve the equation `3x + 11 = 14`. Can you help me?'},\n", + " 'type': 'text'}],\n", + " 'created_at': 1736340400,\n", + " 'incomplete_at': None,\n", + " 'incomplete_details': None,\n", + " 'metadata': {},\n", + " 'object': 'thread.message',\n", + " 'role': 'user',\n", + " 'run_id': None,\n", + " 'status': None,\n", + " 'thread_id': 'thread_j4dc1TiHPfkviKUHNi4aAsA6'}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "message = client.beta.threads.messages.create(\n", + " thread_id=thread.id,\n", + " role=\"user\",\n", + " content=\"I need to solve the equation `3x + 11 = 14`. Can you help me?\",\n", + ")\n", + "show_json(message)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> **Note**\n", + "> Even though you're no longer sending the entire history each time, you will still be charged for the tokens of the entire conversation history with each Run.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Runs\n", + "\n", + "Notice how the Thread we created is **not** associated with the Assistant we created earlier! Threads exist independently from Assistants, which may be different from what you'd expect if you've used ChatGPT (where a thread is tied to a model/GPT).\n", + "\n", + "To get a completion from an Assistant for a given Thread, we must create a Run. Creating a Run will indicate to an Assistant it should look at the messages in the Thread and take action: either by adding a single response, or using tools.\n", + "\n", + "> **Note**\n", + "> Runs are a key difference between the Assistants API and Chat Completions API. While in Chat Completions the model will only ever respond with a single message, in the Assistants API a Run may result in an Assistant using one or multiple tools, and potentially adding multiple messages to the Thread.\n", + "\n", + "To get our Assistant to respond to the user, let's create the Run. As mentioned earlier, you must specify _both_ the Assistant and the Thread.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': 'run_qVYsWok6OCjHxkajpIrdHuVP',\n", + " 'assistant_id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',\n", + " 'cancelled_at': None,\n", + " 'completed_at': None,\n", + " 'created_at': 1736340403,\n", + " 'expires_at': 1736341003,\n", + " 'failed_at': None,\n", + " 'incomplete_details': None,\n", + " 'incomplete_details': None,\n", + " 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',\n", + " 'last_error': None,\n", + " 'max_completion_tokens': None,\n", + " 'max_prompt_tokens': None,\n", + " 'max_completion_tokens': None,\n", + " 'max_prompt_tokens': None,\n", + " 'metadata': {},\n", + " 'model': 'gpt-4o',\n", + " 'object': 'thread.run',\n", + " 'parallel_tool_calls': True,\n", + " 'parallel_tool_calls': True,\n", + " 'required_action': None,\n", + " 'response_format': 'auto',\n", + " 'response_format': 'auto',\n", + " 'started_at': None,\n", + " 'status': 'queued',\n", + " 'thread_id': 'thread_j4dc1TiHPfkviKUHNi4aAsA6',\n", + " 'tool_choice': 'auto',\n", + " 'tools': [],\n", + " 'truncation_strategy': {'type': 'auto', 'last_messages': None},\n", + " 'usage': None,\n", + " 'temperature': 1.0,\n", + " 'top_p': 1.0,\n", + " 'tool_resources': {}}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "run = client.beta.threads.runs.create(\n", + " thread_id=thread.id,\n", + " assistant_id=assistant.id,\n", + ")\n", + "show_json(run)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Unlike creating a completion in the Chat Completions API, **creating a Run is an asynchronous operation**. It will return immediately with the Run's metadata, which includes a `status` that will initially be set to `queued`. The `status` will be updated as the Assistant performs operations (like using tools and adding messages).\n", + "\n", + "To know when the Assistant has completed processing, we can poll the Run in a loop. (Support for streaming is coming soon!) While here we are only checking for a `queued` or `in_progress` status, in practice a Run may undergo a [variety of status changes](https://platform.openai.com/docs/api-reference/runs/object#runs/object-status) which you can choose to surface to the user. (These are called Steps, and will be covered later.)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "\n", + "def wait_on_run(run, thread):\n", + " while run.status == \"queued\" or run.status == \"in_progress\":\n", + " run = client.beta.threads.runs.retrieve(\n", + " thread_id=thread.id,\n", + " run_id=run.id,\n", + " )\n", + " time.sleep(0.5)\n", + " return run" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': 'run_qVYsWok6OCjHxkajpIrdHuVP',\n", + " 'assistant_id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',\n", + " 'cancelled_at': None,\n", + " 'completed_at': 1736340406,\n", + " 'created_at': 1736340403,\n", + " 'expires_at': None,\n", + " 'failed_at': None,\n", + " 'incomplete_details': None,\n", + " 'incomplete_details': None,\n", + " 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',\n", + " 'last_error': None,\n", + " 'max_completion_tokens': None,\n", + " 'max_prompt_tokens': None,\n", + " 'max_completion_tokens': None,\n", + " 'max_prompt_tokens': None,\n", + " 'metadata': {},\n", + " 'model': 'gpt-4o',\n", + " 'object': 'thread.run',\n", + " 'parallel_tool_calls': True,\n", + " 'parallel_tool_calls': True,\n", + " 'required_action': None,\n", + " 'response_format': 'auto',\n", + " 'started_at': 1736340405,\n", + " 'status': 'completed',\n", + " 'thread_id': 'thread_j4dc1TiHPfkviKUHNi4aAsA6',\n", + " 'tool_choice': 'auto',\n", + " 'tools': [],\n", + " 'truncation_strategy': {'type': 'auto', 'last_messages': None},\n", + " 'usage': {'completion_tokens': 35,\n", + " 'prompt_tokens': 66,\n", + " 'total_tokens': 101,\n", + " 'prompt_token_details': {'cached_tokens': 0},\n", + " 'completion_tokens_details': {'reasoning_tokens': 0}},\n", + " 'temperature': 1.0,\n", + " 'top_p': 1.0,\n", + " 'tool_resources': {}}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "run = wait_on_run(run, thread)\n", + "show_json(run)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Messages\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that the Run has completed, we can list the Messages in the Thread to see what got added by the Assistant.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'data': [{'id': 'msg_A5eAN6ZAJDmFBOYutEm5DFCy',\n", + " 'assistant_id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',\n", + " 'attachments': [],\n", + " 'completed_at': None,\n", + " 'content': [{'text': {'annotations': [],\n", + " 'value': 'Sure! Subtract 11 from both sides to get \\\\(3x = 3\\\\), then divide by 3 to find \\\\(x = 1\\\\).'},\n", + " 'type': 'text'}],\n", + " 'created_at': 1736340405,\n", + " 'incomplete_at': None,\n", + " 'incomplete_details': None,\n", + " 'metadata': {},\n", + " 'object': 'thread.message',\n", + " 'role': 'assistant',\n", + " 'run_id': 'run_qVYsWok6OCjHxkajpIrdHuVP',\n", + " 'status': None,\n", + " 'thread_id': 'thread_j4dc1TiHPfkviKUHNi4aAsA6'},\n", + " {'id': 'msg_1q4Y7ZZ9gIcPoAKSx9UtrrKJ',\n", + " 'assistant_id': None,\n", + " 'attachments': [],\n", + " 'completed_at': None,\n", + " 'attachments': [],\n", + " 'completed_at': None,\n", + " 'content': [{'text': {'annotations': [],\n", + " 'value': 'I need to solve the equation `3x + 11 = 14`. Can you help me?'},\n", + " 'type': 'text'}],\n", + " 'created_at': 1736340400,\n", + " 'incomplete_at': None,\n", + " 'incomplete_details': None,\n", + " 'metadata': {},\n", + " 'object': 'thread.message',\n", + " 'role': 'user',\n", + " 'run_id': None,\n", + " 'status': None,\n", + " 'thread_id': 'thread_j4dc1TiHPfkviKUHNi4aAsA6'}],\n", + " 'object': 'list',\n", + " 'first_id': 'msg_A5eAN6ZAJDmFBOYutEm5DFCy',\n", + " 'last_id': 'msg_1q4Y7ZZ9gIcPoAKSx9UtrrKJ',\n", + " 'has_more': False}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "messages = client.beta.threads.messages.list(thread_id=thread.id)\n", + "show_json(messages)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you can see, Messages are ordered in reverse-chronological order – this was done so the most recent results are always on the first `page` (since results can be paginated). Do keep a look out for this, since this is the opposite order to messages in the Chat Completions API.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's ask our Assistant to explain the result a bit further!\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'data': [{'id': 'msg_wSHHvaMnaWktZWsKs6gyoPUB',\n", + " 'assistant_id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',\n", + " 'attachments': [],\n", + " 'completed_at': None,\n", + " 'content': [{'text': {'annotations': [],\n", + " 'value': 'Certainly! To isolate \\\\(x\\\\), first subtract 11 from both sides of the equation \\\\(3x + 11 = 14\\\\), resulting in \\\\(3x = 3\\\\). Then, divide both sides by 3 to solve for \\\\(x\\\\), giving you \\\\(x = 1\\\\).'},\n", + " 'type': 'text'}],\n", + " 'created_at': 1736340414,\n", + " 'incomplete_at': None,\n", + " 'incomplete_details': None,\n", + " 'metadata': {},\n", + " 'object': 'thread.message',\n", + " 'role': 'assistant',\n", + " 'run_id': 'run_lJsumsDtPTmdG3Enx2CfYrrq',\n", + " 'status': None,\n", + " 'thread_id': 'thread_j4dc1TiHPfkviKUHNi4aAsA6'}],\n", + " 'object': 'list',\n", + " 'first_id': 'msg_wSHHvaMnaWktZWsKs6gyoPUB',\n", + " 'last_id': 'msg_wSHHvaMnaWktZWsKs6gyoPUB',\n", + " 'has_more': False}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Create a message to append to our thread\n", + "message = client.beta.threads.messages.create(\n", + " thread_id=thread.id, role=\"user\", content=\"Could you explain this to me?\"\n", + ")\n", + "\n", + "# Execute our run\n", + "run = client.beta.threads.runs.create(\n", + " thread_id=thread.id,\n", + " assistant_id=assistant.id,\n", + ")\n", + "\n", + "# Wait for completion\n", + "wait_on_run(run, thread)\n", + "\n", + "# Retrieve all the messages added after our last user message\n", + "messages = client.beta.threads.messages.list(\n", + " thread_id=thread.id, order=\"asc\", after=message.id\n", + ")\n", + "show_json(messages)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This may feel like a lot of steps to get a response back, especially for this simple example. However, you'll soon see how we can add very powerful functionality to our Assistant without changing much code at all!\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Example\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's take a look at how we could potentially put all of this together. Below is all the code you need to use an Assistant you've created.\n", + "\n", + "Since we've already created our Math Assistant, I've saved its ID in `MATH_ASSISTANT_ID`. I then defined two functions:\n", + "\n", + "- `submit_message`: create a Message on a Thread, then start (and return) a new Run\n", + "- `get_response`: returns the list of Messages in a Thread\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "from openai import OpenAI\n", + "\n", + "MATH_ASSISTANT_ID = assistant.id # or a hard-coded ID like \"asst-...\"\n", + "\n", + "client = OpenAI(api_key=os.environ.get(\"OPENAI_API_KEY\", \"\"))\n", + "\n", + "def submit_message(assistant_id, thread, user_message):\n", + " client.beta.threads.messages.create(\n", + " thread_id=thread.id, role=\"user\", content=user_message\n", + " )\n", + " return client.beta.threads.runs.create(\n", + " thread_id=thread.id,\n", + " assistant_id=assistant_id,\n", + " )\n", + "\n", + "\n", + "def get_response(thread):\n", + " return client.beta.threads.messages.list(thread_id=thread.id, order=\"asc\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "I've also defined a `create_thread_and_run` function that I can re-use (which is actually almost identical to the [`client.beta.threads.create_and_run`](https://platform.openai.com/docs/api-reference/runs/createThreadAndRun) compound function in our API ;) ). Finally, we can submit our mock user requests each to a new Thread.\n", + "\n", + "Notice how all of these API calls are asynchronous operations; this means we actually get async behavior in our code without the use of async libraries! (e.g. `asyncio`)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "def create_thread_and_run(user_input):\n", + " thread = client.beta.threads.create()\n", + " run = submit_message(MATH_ASSISTANT_ID, thread, user_input)\n", + " return thread, run\n", + "\n", + "\n", + "# Emulating concurrent user requests\n", + "thread1, run1 = create_thread_and_run(\n", + " \"I need to solve the equation `3x + 11 = 14`. Can you help me?\"\n", + ")\n", + "thread2, run2 = create_thread_and_run(\"Could you explain linear algebra to me?\")\n", + "thread3, run3 = create_thread_and_run(\"I don't like math. What can I do?\")\n", + "\n", + "# Now all Runs are executing..." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once all Runs are going, we can wait on each and get the responses.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "# Messages\n", + "user: I need to solve the equation `3x + 11 = 14`. Can you help me?\n", + "assistant: Sure! Subtract 11 from both sides to get \\(3x = 3\\), then divide by 3 to find \\(x = 1\\).\n", + "\n", + "# Messages\n", + "user: Could you explain linear algebra to me?\n", + "assistant: Linear algebra is the branch of mathematics concerning vector spaces, linear transformations, and systems of linear equations, often represented with matrices.\n", + "\n", + "# Messages\n", + "user: I don't like math. What can I do?\n", + "assistant: Try relating math to real-life interests or hobbies, practice with fun games or apps, and gradually build confidence with easier problems.\n", + "\n", + "# Messages\n", + "user: I don't like math. What can I do?\n", + "assistant: Try relating math to real-life interests or hobbies, practice with fun games or apps, and gradually build confidence with easier problems.\n", + "user: Thank you!\n", + "assistant: You're welcome! If you have any more questions, feel free to ask!\n", + "\n" + ] + } + ], + "source": [ + "import time\n", + "\n", + "# Pretty printing helper\n", + "def pretty_print(messages):\n", + " print(\"# Messages\")\n", + " for m in messages:\n", + " print(f\"{m.role}: {m.content[0].text.value}\")\n", + " print()\n", + "\n", + "\n", + "# Waiting in a loop\n", + "def wait_on_run(run, thread):\n", + " while run.status == \"queued\" or run.status == \"in_progress\":\n", + " run = client.beta.threads.runs.retrieve(\n", + " thread_id=thread.id,\n", + " run_id=run.id,\n", + " )\n", + " time.sleep(0.5)\n", + " return run\n", + "\n", + "\n", + "# Wait for Run 1\n", + "run1 = wait_on_run(run1, thread1)\n", + "pretty_print(get_response(thread1))\n", + "\n", + "# Wait for Run 2\n", + "run2 = wait_on_run(run2, thread2)\n", + "pretty_print(get_response(thread2))\n", + "\n", + "# Wait for Run 3\n", + "run3 = wait_on_run(run3, thread3)\n", + "pretty_print(get_response(thread3))\n", + "\n", + "# Thank our assistant on Thread 3 :)\n", + "run4 = submit_message(MATH_ASSISTANT_ID, thread3, \"Thank you!\")\n", + "run4 = wait_on_run(run4, thread3)\n", + "pretty_print(get_response(thread3))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Et voilà!\n", + "\n", + "You may have noticed that this code is not actually specific to our math Assistant at all... this code will work for any new Assistant you create simply by changing the Assistant ID! That is the power of the Assistants API.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Tools\n", + "\n", + "A key feature of the Assistants API is the ability to equip our Assistants with Tools, like Code Interpreter, File Search, and custom Functions. Let's take a look at each.\n", + "\n", + "### Code Interpreter\n", + "\n", + "Let's equip our Math Tutor with the [Code Interpreter](https://platform.openai.com/docs/assistants/tools/code-interpreter) tool, which we can do from the Dashboard...\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Enabling code interpreter](../images/assistants_overview_enable_code_interpreter.png)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "...or the API, using the Assistant ID.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',\n", + " 'created_at': 1736340398,\n", + " 'description': None,\n", + " 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',\n", + " 'metadata': {},\n", + " 'model': 'gpt-4o',\n", + " 'name': 'Math Tutor',\n", + " 'object': 'assistant',\n", + " 'tools': [{'type': 'code_interpreter'}],\n", + " 'response_format': 'auto',\n", + " 'temperature': 1.0,\n", + " 'tool_resources': {'code_interpreter': {'file_ids': []}, 'file_search': None},\n", + " 'top_p': 1.0}", + " 'tools': [{'type': 'code_interpreter'}],\n", + " 'response_format': 'auto',\n", + " 'temperature': 1.0,\n", + " 'tool_resources': {'code_interpreter': {'file_ids': []}, 'file_search': None},\n", + " 'top_p': 1.0}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "assistant = client.beta.assistants.update(\n", + " MATH_ASSISTANT_ID,\n", + " tools=[{\"type\": \"code_interpreter\"}],\n", + ")\n", + "show_json(assistant)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's ask the Assistant to use its new tool.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "# Messages\n", + "user: Generate the first 20 fibbonaci numbers with code.\n", + "assistant: The first 20 Fibonacci numbers are: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181.\n", + "\n" + ] + } + ], + "source": [ + "thread, run = create_thread_and_run(\n", + " \"Generate the first 20 fibbonaci numbers with code.\"\n", + ")\n", + "run = wait_on_run(run, thread)\n", + "pretty_print(get_response(thread))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And that's it! The Assistant used Code Interpreter in the background, and gave us a final response.\n", + "\n", + "For some use cases this may be enough – however, if we want more details on what precisely an Assistant is doing we can take a look at a Run's Steps.\n", + "\n", + "### Steps\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A Run is composed of one or more Steps. Like a Run, each Step has a `status` that you can query. This is useful for surfacing the progress of a Step to a user (e.g. a spinner while the Assistant is writing code or performing retrieval).\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "run_steps = client.beta.threads.runs.steps.list(\n", + " thread_id=thread.id, run_id=run.id, order=\"asc\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's take a look at each Step's `step_details`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'tool_calls': [{'id': 'call_E1EE1loDmcWoc7FpkOMKYj6n',\n", + " 'code_interpreter': {'input': 'def generate_fibonacci(n):\\n fib_sequence = [0, 1]\\n while len(fib_sequence) < n:\\n next_value = fib_sequence[-1] + fib_sequence[-2]\\n fib_sequence.append(next_value)\\n return fib_sequence\\n\\n# Generate the first 20 Fibonacci numbers\\nfirst_20_fibonacci = generate_fibonacci(20)\\nfirst_20_fibonacci',\n", + " 'outputs': []},\n", + " 'type': 'code_interpreter'}],\n", + " 'type': 'tool_calls'}" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "null\n" + ] + }, + { + "data": { + "text/plain": [ + "{'message_creation': {'message_id': 'msg_RzTnbBMmzDYHk79a0x9qM5uU'},\n", + " 'type': 'message_creation'}" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "null\n" + ] + } + ], + "source": [ + "for step in run_steps.data:\n", + " step_details = step.step_details\n", + " print(json.dumps(show_json(step_details), indent=4))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see the `step_details` for two Steps:\n", + "\n", + "1. `tool_calls` (plural, since it could be more than one in a single Step)\n", + "2. `message_creation`\n", + "\n", + "The first Step is a `tool_calls`, specifically using the `code_interpreter` which contains:\n", + "\n", + "- `input`, which was the Python code generated before the tool was called, and\n", + "- `output`, which was the result of running the Code Interpreter.\n", + "\n", + "The second Step is a `message_creation`, which contains the `message` that was added to the Thread to communicate the results to the user.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### File search\n", + "\n", + "Another powerful tool in the Assistants API is [File search](https://platform.openai.com/docs/assistants/tools/file-search). This allows the uploading of files to the Assistant to be used as a knowledge base when answering questions.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Enabling retrieval](../images/assistants_overview_enable_retrieval.png)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "File added to vector store\n" + ] + }, + { + "data": { + "text/plain": [ + "{'id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',\n", + " 'created_at': 1736340398,\n", + " 'description': None,\n", + " 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',\n", + " 'metadata': {},\n", + " 'model': 'gpt-4o',\n", + " 'name': 'Math Tutor',\n", + " 'object': 'assistant',\n", + " 'tools': [{'type': 'code_interpreter'},\n", + " {'type': 'file_search',\n", + " 'file_search': {'max_num_results': None,\n", + " 'ranking_options': {'score_threshold': 0.0,\n", + " 'ranker': 'default_2024_08_21'}}}],\n", + " 'response_format': 'auto',\n", + " 'temperature': 1.0,\n", + " 'tool_resources': {'code_interpreter': {'file_ids': ['file-GQFm2i7N8LrAQatefWKEsE']},\n", + " 'file_search': {'vector_store_ids': ['vs_dEArILZSJh7J799QACi3QhuU']}},\n", + " 'top_p': 1.0}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Upload the file\n", + "file = client.files.create(\n", + " file=open(\n", + " \"data/language_models_are_unsupervised_multitask_learners.pdf\",\n", + " \"rb\",\n", + " ),\n", + " purpose=\"assistants\",\n", + ")\n", + "\n", + "# Create a vector store\n", + "vector_store = client.beta.vector_stores.create(\n", + " name=\"language_models_are_unsupervised_multitask_learners\",\n", + ")\n", + "\n", + "# Add the file to the vector store\n", + "vector_store_file = client.beta.vector_stores.files.create_and_poll(\n", + " vector_store_id=vector_store.id,\n", + " file_id=file.id,\n", + ")\n", + "\n", + "# Confirm the file was added\n", + "while vector_store_file.status == \"in_progress\":\n", + " time.sleep(1)\n", + "if vector_store_file.status == \"completed\":\n", + " print(\"File added to vector store\")\n", + "elif vector_store_file.status == \"failed\":\n", + " raise Exception(\"Failed to add file to vector store\")\n", + "\n", + "# Update Assistant\n", + "assistant = client.beta.assistants.update(\n", + " MATH_ASSISTANT_ID,\n", + " tools=[{\"type\": \"code_interpreter\"}, {\"type\": \"file_search\"}],\n", + " tool_resources={\n", + " \"file_search\":{\n", + " \"vector_store_ids\": [vector_store.id]\n", + " },\n", + " \"code_interpreter\": {\n", + " \"file_ids\": [file.id]\n", + " }\n", + " },\n", + ")\n", + "show_json(assistant)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "# Messages\n", + "user: What are some cool math concepts behind this ML paper pdf? Explain in two sentences.\n", + "assistant: The paper explores the concept of multitask learning where a single model is used to perform various tasks, modeling the conditional distribution \\( p(\\text{output} | \\text{input, task}) \\), inspired by probabilistic approaches【6:10†source】. It also discusses the use of Transformer-based architectures and parallel corpus substitution in language models, enhancing their ability to generalize across domain tasks without explicit task-specific supervision【6:2†source】【6:5†source】.\n", + "\n" + ] + } + ], + "source": [ + "thread, run = create_thread_and_run(\n", + " \"What are some cool math concepts behind this ML paper pdf? Explain in two sentences.\"\n", + ")\n", + "run = wait_on_run(run, thread)\n", + "pretty_print(get_response(thread))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> **Note**\n", + "> There are more intricacies in File Search, like [Annotations](https://platform.openai.com/docs/assistants/how-it-works/managing-threads-and-messages), which may be covered in another cookbook.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "VectorStoreDeleted(id='vs_dEArILZSJh7J799QACi3QhuU', deleted=True, object='vector_store.deleted')" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Delete the vector store\n", + "client.beta.vector_stores.delete(vector_store.id)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Functions\n", + "\n", + "As a final powerful tool for your Assistant, you can specify custom [Functions](https://platform.openai.com/docs/assistants/tools/function-calling) (much like the [Function Calling](https://platform.openai.com/docs/guides/function-calling) in the Chat Completions API). During a Run, the Assistant can then indicate it wants to call one or more functions you specified. You are then responsible for calling the Function, and providing the output back to the Assistant.\n", + "\n", + "Let's take a look at an example by defining a `display_quiz()` Function for our Math Tutor.\n", + "\n", + "This function will take a `title` and an array of `question`s, display the quiz, and get input from the user for each:\n", + "\n", + "- `title`\n", + "- `questions`\n", + " - `question_text`\n", + " - `question_type`: [`MULTIPLE_CHOICE`, `FREE_RESPONSE`]\n", + " - `choices`: [\"choice 1\", \"choice 2\", ...]\n", + "\n", + "I'll mocking out responses with `get_mock_response...`. This is where you'd get the user's actual input.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "def get_mock_response_from_user_multiple_choice():\n", + " return \"a\"\n", + "\n", + "\n", + "def get_mock_response_from_user_free_response():\n", + " return \"I don't know.\"\n", + "\n", + "\n", + "def display_quiz(title, questions):\n", + " print(\"Quiz:\", title)\n", + " print()\n", + " responses = []\n", + "\n", + " for q in questions:\n", + " print(q[\"question_text\"])\n", + " response = \"\"\n", + "\n", + " # If multiple choice, print options\n", + " if q[\"question_type\"] == \"MULTIPLE_CHOICE\":\n", + " for i, choice in enumerate(q[\"choices\"]):\n", + " print(f\"{i}. {choice}\")\n", + " response = get_mock_response_from_user_multiple_choice()\n", + "\n", + " # Otherwise, just get response\n", + " elif q[\"question_type\"] == \"FREE_RESPONSE\":\n", + " response = get_mock_response_from_user_free_response()\n", + "\n", + " responses.append(response)\n", + " print()\n", + "\n", + " return responses" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here's what a sample quiz would look like:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Quiz: Sample Quiz\n", + "\n", + "What is your name?\n", + "\n", + "What is your favorite color?\n", + "0. Red\n", + "1. Blue\n", + "2. Green\n", + "3. Yellow\n", + "\n", + "Responses: [\"I don't know.\", 'a']\n" + ] + } + ], + "source": [ + "responses = display_quiz(\n", + " \"Sample Quiz\",\n", + " [\n", + " {\"question_text\": \"What is your name?\", \"question_type\": \"FREE_RESPONSE\"},\n", + " {\n", + " \"question_text\": \"What is your favorite color?\",\n", + " \"question_type\": \"MULTIPLE_CHOICE\",\n", + " \"choices\": [\"Red\", \"Blue\", \"Green\", \"Yellow\"],\n", + " },\n", + " ],\n", + ")\n", + "print(\"Responses:\", responses)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, let's define the interface of this function in JSON format, so our Assistant can call it:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [], + "source": [ + "function_json = {\n", + " \"name\": \"display_quiz\",\n", + " \"description\": \"Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.\",\n", + " \"parameters\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"title\": {\"type\": \"string\"},\n", + " \"questions\": {\n", + " \"type\": \"array\",\n", + " \"description\": \"An array of questions, each with a title and potentially options (if multiple choice).\",\n", + " \"items\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\n", + " \"question_text\": {\"type\": \"string\"},\n", + " \"question_type\": {\n", + " \"type\": \"string\",\n", + " \"enum\": [\"MULTIPLE_CHOICE\", \"FREE_RESPONSE\"]\n", + " },\n", + " \"choices\": {\"type\": \"array\", \"items\": {\"type\": \"string\"}}\n", + " },\n", + " \"required\": [\"question_text\"]\n", + " }\n", + " }\n", + " },\n", + " \"required\": [\"title\", \"questions\"]\n", + " }\n", + "}\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Once again, let's update our Assistant either through the Dashboard or the API.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Enabling custom function](../images/assistants_overview_enable_function.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "> **Note**\n", + "> Pasting the function JSON into the Dashboard was a bit finicky due to indentation, etc. I just asked ChatGPT to format my function the same as one of the examples on the Dashboard :).\n" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',\n", + " 'created_at': 1736340398,\n", + " 'description': None,\n", + " 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',\n", + " 'metadata': {},\n", + " 'model': 'gpt-4o',\n", + " 'name': 'Math Tutor',\n", + " 'object': 'assistant',\n", + " 'tools': [{'type': 'code_interpreter'},\n", + " {'type': 'file_search',\n", + " 'file_search': {'max_num_results': None,\n", + " 'ranking_options': {'score_threshold': 0.0,\n", + " 'ranker': 'default_2024_08_21'}}},\n", + " {'function': {'name': 'display_quiz',\n", + " 'description': \"Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.\",\n", + " 'description': \"Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.\",\n", + " 'parameters': {'type': 'object',\n", + " 'properties': {'title': {'type': 'string'},\n", + " 'questions': {'type': 'array',\n", + " 'description': 'An array of questions, each with a title and potentially options (if multiple choice).',\n", + " 'items': {'type': 'object',\n", + " 'properties': {'question_text': {'type': 'string'},\n", + " 'question_type': {'type': 'string',\n", + " 'enum': ['MULTIPLE_CHOICE', 'FREE_RESPONSE']},\n", + " 'choices': {'type': 'array', 'items': {'type': 'string'}}},\n", + " 'required': ['question_text']}}},\n", + " 'required': ['title', 'questions']},\n", + " 'strict': False},\n", + " 'type': 'function'}],\n", + " 'response_format': 'auto',\n", + " 'temperature': 1.0,\n", + " 'tool_resources': {'code_interpreter': {'file_ids': ['file-GQFm2i7N8LrAQatefWKEsE']},\n", + " 'file_search': {'vector_store_ids': []}},\n", + " 'top_p': 1.0}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "assistant = client.beta.assistants.update(\n", + " MATH_ASSISTANT_ID,\n", + " tools=[\n", + " {\"type\": \"code_interpreter\"},\n", + " {\"type\": \"file_search\"},\n", + " {\"type\": \"function\", \"function\": function_json},\n", + " ],\n", + ")\n", + "show_json(assistant)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And now, we ask for a quiz.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'requires_action'" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "thread, run = create_thread_and_run(\n", + " \"Make a quiz with 2 questions: One open ended, one multiple choice. Then, give me feedback for the responses.\"\n", + ")\n", + "run = wait_on_run(run, thread)\n", + "run.status" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, however, when we check the Run's `status` we see `requires_action`! Let's take a closer.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': 'run_ekMRSI2h35asEzKirRf4BTwZ',\n", + " 'assistant_id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',\n", + " 'cancelled_at': None,\n", + " 'completed_at': None,\n", + " 'created_at': 1736341020,\n", + " 'expires_at': 1736341620,\n", + " 'failed_at': None,\n", + " 'incomplete_details': None,\n", + " 'incomplete_details': None,\n", + " 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',\n", + " 'last_error': None,\n", + " 'max_completion_tokens': None,\n", + " 'max_prompt_tokens': None,\n", + " 'max_completion_tokens': None,\n", + " 'max_prompt_tokens': None,\n", + " 'metadata': {},\n", + " 'model': 'gpt-4o',\n", + " 'object': 'thread.run',\n", + " 'parallel_tool_calls': True,\n", + " 'required_action': {'submit_tool_outputs': {'tool_calls': [{'id': 'call_uvJEn0fxM4sgmzek8wahBGLi',\n", + " 'function': {'arguments': '{\"title\":\"Math Quiz\",\"questions\":[{\"question_text\":\"What is the derivative of the function f(x) = 3x^2 + 2x - 5?\",\"question_type\":\"FREE_RESPONSE\"},{\"question_text\":\"What is the value of \\\\\\\\( \\\\\\\\int_{0}^{1} 2x \\\\\\\\, dx \\\\\\\\)?\",\"question_type\":\"MULTIPLE_CHOICE\",\"choices\":[\"0\",\"1\",\"2\",\"3\"]}]}',\n", + " 'name': 'display_quiz'},\n", + " 'type': 'function'}]},\n", + " 'type': 'submit_tool_outputs'},\n", + " 'response_format': 'auto',\n", + " 'started_at': 1736341022,\n", + " 'status': 'requires_action',\n", + " 'thread_id': 'thread_8bK2PXfoeijEHBVEzYuJXt17',\n", + " 'tool_choice': 'auto',\n", + " 'tools': [{'type': 'code_interpreter'},\n", + " {'type': 'file_search',\n", + " 'file_search': {'max_num_results': None,\n", + " 'ranking_options': {'score_threshold': 0.0,\n", + " 'ranker': 'default_2024_08_21'}}},\n", + " {'function': {'name': 'display_quiz',\n", + " 'description': \"Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.\",\n", + " 'description': \"Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.\",\n", + " 'parameters': {'type': 'object',\n", + " 'properties': {'title': {'type': 'string'},\n", + " 'questions': {'type': 'array',\n", + " 'description': 'An array of questions, each with a title and potentially options (if multiple choice).',\n", + " 'items': {'type': 'object',\n", + " 'properties': {'question_text': {'type': 'string'},\n", + " 'question_type': {'type': 'string',\n", + " 'enum': ['MULTIPLE_CHOICE', 'FREE_RESPONSE']},\n", + " 'choices': {'type': 'array', 'items': {'type': 'string'}}},\n", + " 'required': ['question_text']}}},\n", + " 'required': ['title', 'questions']},\n", + " 'strict': False},\n", + " 'type': 'function'}],\n", + " 'truncation_strategy': {'type': 'auto', 'last_messages': None},\n", + " 'usage': None,\n", + " 'temperature': 1.0,\n", + " 'top_p': 1.0,\n", + " 'tool_resources': {}}", + " 'strict': False},\n", + " 'type': 'function'}],\n", + " 'truncation_strategy': {'type': 'auto', 'last_messages': None},\n", + " 'usage': None,\n", + " 'temperature': 1.0,\n", + " 'top_p': 1.0,\n", + " 'tool_resources': {}}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "show_json(run)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `required_action` field indicates a Tool is waiting for us to run it and submit its output back to the Assistant. Specifically, the `display_quiz` function! Let's start by parsing the `name` and `arguments`.\n", + "\n", + "> **Note**\n", + "> While in this case we know there is only one Tool call, in practice the Assistant may choose to call multiple tools.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Function Name: display_quiz\n", + "Function Arguments:\n" + ] + }, + { + "data": { + "text/plain": [ + "{'title': 'Math Quiz',\n", + " 'questions': [{'question_text': 'What is the derivative of the function f(x) = 3x^2 + 2x - 5?',\n", + " 'question_type': 'FREE_RESPONSE'},\n", + " {'question_text': 'What is the value of \\\\( \\\\int_{0}^{1} 2x \\\\, dx \\\\)?',\n", + " 'question_type': 'MULTIPLE_CHOICE',\n", + " 'choices': ['0', '1', '2', '3']}]}" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Extract single tool call\n", + "tool_call = run.required_action.submit_tool_outputs.tool_calls[0]\n", + "name = tool_call.function.name\n", + "arguments = json.loads(tool_call.function.arguments)\n", + "\n", + "print(\"Function Name:\", name)\n", + "print(\"Function Arguments:\")\n", + "arguments" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's actually call our `display_quiz` function with the arguments provided by the Assistant:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Quiz: Math Quiz\n", + "Quiz: Math Quiz\n", + "\n", + "What is the derivative of the function f(x) = 3x^2 + 2x - 5?\n", + "\n", + "What is the value of \\( \\int_{0}^{1} 2x \\, dx \\)?\n", + "0. 0\n", + "1. 1\n", + "2. 2\n", + "3. 3\n", + "\n", + "Responses: [\"I don't know.\", 'a']\n" + ] + } + ], + "source": [ + "responses = display_quiz(arguments[\"title\"], arguments[\"questions\"])\n", + "print(\"Responses:\", responses)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Great! (Remember these responses are the one's we mocked earlier. In reality, we'd be getting input from the back from this function call.)\n", + "\n", + "Now that we have our responses, let's submit them back to the Assistant. We'll need the `tool_call` ID, found in the `tool_call` we parsed out earlier. We'll also need to encode our `list`of responses into a `str`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'id': 'run_ekMRSI2h35asEzKirRf4BTwZ',\n", + " 'assistant_id': 'asst_qvXmYlZV8zhABI2RtPzDfV6z',\n", + " 'cancelled_at': None,\n", + " 'completed_at': None,\n", + " 'created_at': 1736341020,\n", + " 'expires_at': 1736341620,\n", + " 'failed_at': None,\n", + " 'incomplete_details': None,\n", + " 'incomplete_details': None,\n", + " 'instructions': 'You are a personal math tutor. Answer questions briefly, in a sentence or less.',\n", + " 'last_error': None,\n", + " 'max_completion_tokens': None,\n", + " 'max_prompt_tokens': None,\n", + " 'max_completion_tokens': None,\n", + " 'max_prompt_tokens': None,\n", + " 'metadata': {},\n", + " 'model': 'gpt-4o',\n", + " 'object': 'thread.run',\n", + " 'parallel_tool_calls': True,\n", + " 'parallel_tool_calls': True,\n", + " 'required_action': None,\n", + " 'response_format': 'auto',\n", + " 'started_at': 1736341022,\n", + " 'status': 'queued',\n", + " 'thread_id': 'thread_8bK2PXfoeijEHBVEzYuJXt17',\n", + " 'tool_choice': 'auto',\n", + " 'tools': [{'type': 'code_interpreter'},\n", + " {'type': 'file_search',\n", + " 'file_search': {'max_num_results': None,\n", + " 'ranking_options': {'score_threshold': 0.0,\n", + " 'ranker': 'default_2024_08_21'}}},\n", + " {'function': {'name': 'display_quiz',\n", + " 'description': \"Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.\",\n", + " 'description': \"Displays a quiz to the student, and returns the student's response. A single quiz can have multiple questions.\",\n", + " 'parameters': {'type': 'object',\n", + " 'properties': {'title': {'type': 'string'},\n", + " 'questions': {'type': 'array',\n", + " 'description': 'An array of questions, each with a title and potentially options (if multiple choice).',\n", + " 'items': {'type': 'object',\n", + " 'properties': {'question_text': {'type': 'string'},\n", + " 'question_type': {'type': 'string',\n", + " 'enum': ['MULTIPLE_CHOICE', 'FREE_RESPONSE']},\n", + " 'choices': {'type': 'array', 'items': {'type': 'string'}}},\n", + " 'required': ['question_text']}}},\n", + " 'required': ['title', 'questions']},\n", + " 'strict': False},\n", + " 'type': 'function'}],\n", + " 'truncation_strategy': {'type': 'auto', 'last_messages': None},\n", + " 'usage': None,\n", + " 'temperature': 1.0,\n", + " 'top_p': 1.0,\n", + " 'tool_resources': {}}", + " 'strict': False},\n", + " 'type': 'function'}],\n", + " 'truncation_strategy': {'type': 'auto', 'last_messages': None},\n", + " 'usage': None,\n", + " 'temperature': 1.0,\n", + " 'top_p': 1.0,\n", + " 'tool_resources': {}}" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "run = client.beta.threads.runs.submit_tool_outputs(\n", + " thread_id=thread.id,\n", + " run_id=run.id,\n", + " tool_outputs=tool_outputs\n", + ")\n", + "show_json(run)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now wait for the Run to complete once again, and check our Thread!\n" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "# Messages\n", + "user: Make a quiz with 2 questions: One open ended, one multiple choice. Then, give me feedback for the responses.\n", + "assistant: Since no specific information was found in the uploaded file, I'll create a general math quiz for you:\n", + "\n", + "1. **Open-ended Question**: What is the derivative of the function \\( f(x) = 3x^2 + 2x - 5 \\)?\n", + "\n", + "2. **Multiple Choice Question**: What is the value of \\( \\int_{0}^{1} 2x \\, dx \\)?\n", + " - A) 0\n", + " - B) 1\n", + " - C) 2\n", + " - D) 3\n", + "\n", + "I will now present the quiz to you for response.\n", + "assistant: Here is the feedback for your responses:\n", + "\n", + "1. **Derivative Question**: \n", + " - Your Response: \"I don't know.\"\n", + " - Feedback: The derivative of \\( f(x) = 3x^2 + 2x - 5 \\) is \\( f'(x) = 6x + 2 \\).\n", + "\n", + "2. **Integration Question**: \n", + " - Your Response: A) 0\n", + " - Feedback: The correct answer is B) 1. The integration \\(\\int_{0}^{1} 2x \\, dx \\) evaluates to 1.\n", + "\n" + ] + } + ], + "source": [ + "run = wait_on_run(run, thread)\n", + "pretty_print(get_response(thread))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Woohoo 🎉\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "We covered a lot of ground in this notebook, give yourself a high-five! Hopefully you should now have a strong foundation to build powerful, stateful experiences with tools like Code Interpreter, Retrieval, and Functions!\n", + "\n", + "There's a few sections we didn't cover for the sake of brevity, so here's a few resources to explore further:\n", + "\n", + "- [Annotations](https://platform.openai.com/docs/assistants/how-it-works/managing-threads-and-messages): parsing file citations\n", + "- [Files](https://platform.openai.com/docs/api-reference/assistants/file-object): Thread scoped vs Assistant scoped\n", + "- [Parallel Function Calls](https://platform.openai.com/docs/guides/function-calling/parallel-function-calling): calling multiple tools in a single Step\n", + "- Multi-Assistant Thread Runs: single Thread with Messages from multiple Assistants\n", + "- Streaming: coming soon!\n", + "\n", + "Now go off and build something ama[zing](https://www.youtube.com/watch?v=xvFZjo5PgG0&pp=ygUQcmljayByb2xsIG5vIGFkcw%3D%3D)!\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "openai", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tests/fixtures/synthetic/bad_fixture_in_notebook.py b/tests/fixtures/synthetic/bad_fixture_in_notebook.py new file mode 100644 index 0000000..da14728 --- /dev/null +++ b/tests/fixtures/synthetic/bad_fixture_in_notebook.py @@ -0,0 +1,18 @@ +import marimo +import pytest + +app = marimo.App() + + +@pytest.fixture +def sample_values(): + return [1, 2, 3] + + +@app.cell +def _(): + return + + +if __name__ == "__main__": + app.run() diff --git a/tests/fixtures/synthetic/jupyter_bad_inline_defs.ipynb b/tests/fixtures/synthetic/jupyter_bad_inline_defs.ipynb new file mode 100644 index 0000000..dd62d9b --- /dev/null +++ b/tests/fixtures/synthetic/jupyter_bad_inline_defs.ipynb @@ -0,0 +1,61 @@ +{ + "cells": [ + { + "cell_type": "code", + "id": "cell-0000", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "params = {'seed': 42}\n" + ] + }, + { + "cell_type": "code", + "id": "cell-0001", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def helper_a():\n", + " return 1\n" + ] + }, + { + "cell_type": "code", + "id": "cell-0002", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def helper_b():\n", + " return 2\n" + ] + }, + { + "cell_type": "code", + "id": "cell-0003", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def helper_c():\n", + " return 3\n" + ] + }, + { + "cell_type": "code", + "id": "cell-0004", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "class HelperD:\n", + " pass\n" + ] + } + ], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tests/fixtures/synthetic/jupyter_bad_large_notebook.ipynb b/tests/fixtures/synthetic/jupyter_bad_large_notebook.ipynb new file mode 100644 index 0000000..0752f82 --- /dev/null +++ b/tests/fixtures/synthetic/jupyter_bad_large_notebook.ipynb @@ -0,0 +1,28 @@ +{ + "cells": [ + {"cell_type": "code", "id": "cell-0000", "execution_count": 1, "metadata": {}, "outputs": [], "source": ["params = {'seed': 42}\n"]}, + {"cell_type": "code", "id": "cell-0001", "execution_count": 2, "metadata": {}, "outputs": [], "source": ["value_01 = 1\n"]}, + {"cell_type": "code", "id": "cell-0002", "execution_count": 3, "metadata": {}, "outputs": [], "source": ["value_02 = 2\n"]}, + {"cell_type": "code", "id": "cell-0003", "execution_count": 4, "metadata": {}, "outputs": [], "source": ["value_03 = 3\n"]}, + {"cell_type": "code", "id": "cell-0004", "execution_count": 5, "metadata": {}, "outputs": [], "source": ["value_04 = 4\n"]}, + {"cell_type": "code", "id": "cell-0005", "execution_count": 6, "metadata": {}, "outputs": [], "source": ["value_05 = 5\n"]}, + {"cell_type": "code", "id": "cell-0006", "execution_count": 7, "metadata": {}, "outputs": [], "source": ["value_06 = 6\n"]}, + {"cell_type": "code", "id": "cell-0007", "execution_count": 8, "metadata": {}, "outputs": [], "source": ["value_07 = 7\n"]}, + {"cell_type": "code", "id": "cell-0008", "execution_count": 9, "metadata": {}, "outputs": [], "source": ["value_08 = 8\n"]}, + {"cell_type": "code", "id": "cell-0009", "execution_count": 10, "metadata": {}, "outputs": [], "source": ["value_09 = 9\n"]}, + {"cell_type": "code", "id": "cell-0010", "execution_count": 11, "metadata": {}, "outputs": [], "source": ["value_10 = 10\n"]}, + {"cell_type": "code", "id": "cell-0011", "execution_count": 12, "metadata": {}, "outputs": [], "source": ["value_11 = 11\n"]}, + {"cell_type": "code", "id": "cell-0012", "execution_count": 13, "metadata": {}, "outputs": [], "source": ["value_12 = 12\n"]}, + {"cell_type": "code", "id": "cell-0013", "execution_count": 14, "metadata": {}, "outputs": [], "source": ["value_13 = 13\n"]}, + {"cell_type": "code", "id": "cell-0014", "execution_count": 15, "metadata": {}, "outputs": [], "source": ["value_14 = 14\n"]}, + {"cell_type": "code", "id": "cell-0015", "execution_count": 16, "metadata": {}, "outputs": [], "source": ["value_15 = 15\n"]}, + {"cell_type": "code", "id": "cell-0016", "execution_count": 17, "metadata": {}, "outputs": [], "source": ["value_16 = 16\n"]}, + {"cell_type": "code", "id": "cell-0017", "execution_count": 18, "metadata": {}, "outputs": [], "source": ["value_17 = 17\n"]}, + {"cell_type": "code", "id": "cell-0018", "execution_count": 19, "metadata": {}, "outputs": [], "source": ["value_18 = 18\n"]}, + {"cell_type": "code", "id": "cell-0019", "execution_count": 20, "metadata": {}, "outputs": [], "source": ["value_19 = 19\n"]}, + {"cell_type": "code", "id": "cell-0020", "execution_count": 21, "metadata": {}, "outputs": [], "source": ["value_20 = 20\n"]} + ], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tests/fixtures/synthetic/jupyter_bad_magic.ipynb b/tests/fixtures/synthetic/jupyter_bad_magic.ipynb new file mode 100644 index 0000000..b08a2fc --- /dev/null +++ b/tests/fixtures/synthetic/jupyter_bad_magic.ipynb @@ -0,0 +1,18 @@ +{ + "cells": [ + { + "cell_type": "code", + "id": "cell-0000", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "value = 1\n" + ] + } + ], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tests/fixtures/synthetic/jupyter_bad_non_idempotent.ipynb b/tests/fixtures/synthetic/jupyter_bad_non_idempotent.ipynb new file mode 100644 index 0000000..9885780 --- /dev/null +++ b/tests/fixtures/synthetic/jupyter_bad_non_idempotent.ipynb @@ -0,0 +1,18 @@ +{ + "cells": [ + { + "cell_type": "code", + "id": "cell-0000", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "value = random.random()\n" + ] + } + ], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tests/test_notebook_fixtures.py b/tests/test_notebook_fixtures.py index f3f00a8..5cd5c27 100644 --- a/tests/test_notebook_fixtures.py +++ b/tests/test_notebook_fixtures.py @@ -2,7 +2,7 @@ from pathlib import Path -from pytest_marimo.plugin import DEFAULT_RULES, scan_file +from pytest_notebook_policy.plugin import DEFAULT_RULES, scan_file FIXTURE_ROOT = Path(__file__).parent / "fixtures" @@ -17,6 +17,11 @@ def test_synthetic_notebook_fixtures() -> None: assert _scan("synthetic/good_reactive.py") == set() assert _scan("synthetic/bad_cross_cell_mutation.py") == {"M005"} assert _scan("synthetic/bad_non_idempotent.py") == {"M006"} + assert _scan("synthetic/bad_fixture_in_notebook.py") == {"M004"} + assert _scan("synthetic/jupyter_bad_magic.ipynb") == {"J001", "J011"} + assert _scan("synthetic/jupyter_bad_non_idempotent.ipynb") == {"J002", "J011"} + assert _scan("synthetic/jupyter_bad_large_notebook.ipynb") == {"J012"} + assert _scan("synthetic/jupyter_bad_inline_defs.ipynb") == {"J013"} def test_real_world_notebook_fixtures() -> None: @@ -24,3 +29,12 @@ def test_real_world_notebook_fixtures() -> None: assert _scan("real/marimo_smoke_cross_cell_md.py") == set() assert _scan("real/gallery_matrix.py") == set() assert _scan("real/gallery_earthquake.py") == {"M001"} + assert _scan("real/jupyter_running_code_v6_4_7.ipynb") == set() + assert _scan("real/jupyter_importing_notebooks_v6_4_7.ipynb") == {"J001", "J011", "J013"} + assert _scan("real/jupyter_connecting_qt_console_v6_4_7.ipynb") == {"J001"} + assert _scan("real/openai_cookbook_assistants_api_overview_42bc4ed.ipynb") == { + "J001", + "J011", + "J012", + "J013", + } diff --git a/tests/test_plugin_rules.py b/tests/test_plugin_rules.py index 7cadafb..f0775eb 100644 --- a/tests/test_plugin_rules.py +++ b/tests/test_plugin_rules.py @@ -1,8 +1,10 @@ from __future__ import annotations +import json from pathlib import Path +import pytest -from pytest_marimo.plugin import DEFAULT_RULES, scan_file +from pytest_notebook_policy.plugin import DEFAULT_RULES, scan_file def _write(tmp_path: Path, name: str, source: str) -> Path: @@ -11,8 +13,36 @@ def _write(tmp_path: Path, name: str, source: str) -> Path: return path -def _scan(path: Path, select: set[str] | None = None, ignore: set[str] | None = None): - return scan_file(path, select=select or set(DEFAULT_RULES), ignore=ignore or set()) +def _scan( + path: Path, + select: set[str] | None = None, + ignore: set[str] | None = None, + jupyter_source: str = "ipynb", + max_code_cells: int = 20, + max_cell_lines: int = 80, + max_inline_definitions: int = 3, +): + return scan_file( + path, + select=select or set(DEFAULT_RULES), + ignore=ignore or set(), + jupyter_source=jupyter_source, + max_code_cells=max_code_cells, + max_cell_lines=max_cell_lines, + max_inline_definitions=max_inline_definitions, + ) + + +def _write_ipynb(tmp_path: Path, name: str, cells: list[dict]) -> Path: + path = tmp_path / name + normalised_cells: list[dict] = [] + for index, cell in enumerate(cells): + normalised_cell = dict(cell) + normalised_cell.setdefault("id", f"cell-{index:04d}") + normalised_cells.append(normalised_cell) + notebook = {"cells": normalised_cells, "metadata": {}, "nbformat": 4, "nbformat_minor": 5} + path.write_text(json.dumps(notebook), encoding="utf-8") + return path def test_non_marimo_file_is_ignored(tmp_path: Path) -> None: @@ -175,3 +205,300 @@ def _(): ) codes = [violation.code for violation in _scan(path)] assert "M006" in codes + + +def test_flags_jupyter_magic_and_shell_usage(tmp_path: Path) -> None: + path = _write_ipynb( + tmp_path, + "test_notebook.ipynb", + cells=[ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": ["%matplotlib inline\n", "value = 1\n"], + } + ], + ) + codes = {violation.code for violation in _scan(path)} + assert "J001" in codes + + +def test_flags_jupyter_non_idempotent_call(tmp_path: Path) -> None: + path = _write_ipynb( + tmp_path, + "test_non_idempotent.ipynb", + cells=[ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": ["import random\n", "value = random.random()\n"], + } + ], + ) + codes = {violation.code for violation in _scan(path)} + assert "J002" in codes + + +def test_j010_flags_missing_paired_script(tmp_path: Path) -> None: + path = _write_ipynb( + tmp_path, + "sync_target.ipynb", + cells=[ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": ["x = 1\n"], + } + ], + ) + codes = {violation.code for violation in _scan(path, select={"J010"}, ignore=set())} + assert "J010" in codes + + +def test_j010_passes_when_ipynb_and_script_match(tmp_path: Path) -> None: + path = _write_ipynb( + tmp_path, + "paired.ipynb", + cells=[ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": ["x = 1\n"], + } + ], + ) + paired_script = tmp_path / "paired.py" + paired_script.write_text( + """# ---\n# jupyter:\n# jupytext:\n# formats: ipynb,py:percent\n# ---\n\n# %%\nx = 1\n""", + encoding="utf-8", + ) + + violations = _scan(path, select={"J010"}, ignore=set()) + assert violations == [] + + +def test_paired_py_source_mode_uses_paired_script_cells_for_j_rules(tmp_path: Path) -> None: + pytest.importorskip("jupytext") + path = _write_ipynb( + tmp_path, + "source_switch.ipynb", + cells=[ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": ["%matplotlib inline\n", "value = 1\n"], + } + ], + ) + paired_script = tmp_path / "source_switch.py" + paired_script.write_text( + """# --- +# jupyter: +# jupytext: +# formats: ipynb,py:percent +# --- + +# %% +value = 1 +""", + encoding="utf-8", + ) + + ipynb_scan = _scan(path, select={"J001"}, ignore=set(), jupyter_source="ipynb") + paired_scan = _scan(path, select={"J001"}, ignore=set(), jupyter_source="paired-py") + + assert {violation.code for violation in ipynb_scan} == {"J001"} + assert paired_scan == [] + + +def test_paired_py_source_mode_falls_back_to_ipynb_when_no_pair(tmp_path: Path) -> None: + path = _write_ipynb( + tmp_path, + "no_pair.ipynb", + cells=[ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": ["%time value = 1\n"], + } + ], + ) + + violations = _scan(path, select={"J001"}, ignore=set(), jupyter_source="paired-py") + assert {violation.code for violation in violations} == {"J001"} + + +def test_j011_flags_when_no_parameter_cell_near_top(tmp_path: Path) -> None: + path = _write_ipynb( + tmp_path, + "missing_params.ipynb", + cells=[ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": ["import pandas as pd\n"], + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": ["df = pd.DataFrame({'x': [1, 2, 3]})\n"], + }, + ], + ) + codes = {violation.code for violation in _scan(path, select={"J011"}, ignore=set())} + assert "J011" in codes + + +def test_j011_passes_with_parameter_cell_near_top(tmp_path: Path) -> None: + path = _write_ipynb( + tmp_path, + "with_params.ipynb", + cells=[ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": ["params = {'seed': 42, 'dataset': 'train'}\n"], + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": ["print(params['dataset'])\n"], + }, + ], + ) + assert _scan(path, select={"J011"}, ignore=set()) == [] + + +def test_j012_flags_large_notebook(tmp_path: Path) -> None: + cells = [ + { + "cell_type": "code", + "execution_count": i + 1, + "metadata": {}, + "outputs": [], + "source": [f"value_{i} = {i}\n"], + } + for i in range(21) + ] + path = _write_ipynb(tmp_path, "large_notebook.ipynb", cells=cells) + codes = {violation.code for violation in _scan(path, select={"J012"}, ignore=set())} + assert "J012" in codes + + +def test_j012_respects_custom_max_code_cells_threshold(tmp_path: Path) -> None: + path = _write_ipynb( + tmp_path, + "small_notebook.ipynb", + cells=[ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": ["x = 1\n"], + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": ["y = 2\n"], + }, + ], + ) + assert _scan(path, select={"J012"}, ignore=set()) == [] + codes = { + violation.code + for violation in _scan(path, select={"J012"}, ignore=set(), max_code_cells=1) + } + assert codes == {"J012"} + + +def test_j013_flags_excessive_inline_definitions(tmp_path: Path) -> None: + path = _write_ipynb( + tmp_path, + "inline_defs.ipynb", + cells=[ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": ["def a():\n", " return 1\n"], + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": ["def b():\n", " return 2\n"], + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": ["def c():\n", " return 3\n"], + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": ["class D:\n", " pass\n"], + }, + ], + ) + codes = {violation.code for violation in _scan(path, select={"J013"}, ignore=set())} + assert "J013" in codes + + +def test_j013_respects_custom_inline_definition_threshold(tmp_path: Path) -> None: + path = _write_ipynb( + tmp_path, + "few_inline_defs.ipynb", + cells=[ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": ["def a():\n", " return 1\n"], + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": ["def b():\n", " return 2\n"], + }, + ], + ) + assert _scan(path, select={"J013"}, ignore=set()) == [] + codes = { + violation.code + for violation in _scan( + path, select={"J013"}, ignore=set(), max_inline_definitions=1 + ) + } + assert codes == {"J013"} diff --git a/tests/test_quality.py b/tests/test_quality.py index 7fdd4a7..fe1c275 100644 --- a/tests/test_quality.py +++ b/tests/test_quality.py @@ -154,6 +154,8 @@ def test_write_markdown_report_includes_guidance(tmp_path: Path) -> None: content = report_path.read_text(encoding="utf-8") assert "## 🧪 Notebook policy report" in content + assert "### Executive summary" in content + assert "deterministic notebook quality snapshot" in content assert "| Runtime | `1.23s` |" in content assert "### 🔎 Findings" in content assert "### 🧭 Notebook surface summary" in content @@ -162,6 +164,7 @@ def test_write_markdown_report_includes_guidance(tmp_path: Path) -> None: assert "s3://bucket/object.parquet" in content assert "## Appendix A — Configuration" in content assert "## Appendix B — Scanned files" in content + assert "## Appendix C — NBOM alignment" in content assert "bad.py" in content assert "[`M001`](https://github.com/DataBooth/pytest-notebook-policy/blob/main/docs/RULES.md#m001)" in content assert "| File | Rule | Line | What | Why this is undesirable | Suggested fix |" in content @@ -181,7 +184,8 @@ def test_write_markdown_report_dependency_enrichment_appendix(tmp_path: Path) -> runtime_seconds=0.5, ) content = report_path.read_text(encoding="utf-8") - assert "## Appendix C — Dependency enrichment" in content + assert "## Appendix C — NBOM alignment" in content + assert "## Appendix D — Dependency enrichment" in content def test_write_nbom_manifest(tmp_path: Path) -> None: diff --git a/tests/test_report_regressions.py b/tests/test_report_regressions.py new file mode 100644 index 0000000..c1187b4 --- /dev/null +++ b/tests/test_report_regressions.py @@ -0,0 +1,151 @@ +from __future__ import annotations + +import argparse +import json +from datetime import UTC, datetime +from pathlib import Path + +from pytest_notebook_policy.plugin import Violation +from pytest_notebook_policy.quality import ( + NotebookAnalysis, + Observed, + _resolve_quality_settings, + _write_markdown_report, + _write_nbom_manifest, +) + + +def _namespace(**overrides: object) -> argparse.Namespace: + base: dict[str, object] = { + "notebook_check_select": [], + "notebook_check_ignore": [], + "notebook_check_jupyter_source": None, + "notebook_check_jupyter_max_code_cells": None, + "notebook_check_jupyter_max_cell_lines": None, + "notebook_check_jupyter_max_inline_definitions": None, + "report_md": None, + "report_dependency_enrichment": False, + "report_nbom_json": None, + } + base.update(overrides) + return argparse.Namespace(**base) + + +def test_markdown_report_contract_structure_is_stable(tmp_path: Path) -> None: + report_path = tmp_path / "reports" / "policy.md" + scanned_file = tmp_path / "sample.py" + scanned_file.write_text("import requests\n", encoding="utf-8") + settings = _resolve_quality_settings([], _namespace(report_dependency_enrichment=True)) + violations = [ + Violation( + path=scanned_file, + line=3, + code="M001", + message="Prefer reactive dependencies over on_change handlers.", + ) + ] + + _write_markdown_report( + report_path, + settings=settings, + violations=violations, + scanned_files=[scanned_file], + started_at=datetime(2026, 6, 1, 0, 0, 0, tzinfo=UTC), + runtime_seconds=1.11, + ) + + content = report_path.read_text(encoding="utf-8") + required_sections = [ + "## 🧪 Notebook policy report", + "### Executive summary", + "### 🔎 Findings", + "### 🧭 Notebook surface summary", + "## Appendix A — Configuration", + "## Appendix B — Scanned files", + "## Appendix C — NBOM alignment", + "## Appendix D — Dependency enrichment", + ] + for section in required_sections: + assert section in content + section_positions = [content.index(section) for section in required_sections] + assert section_positions == sorted(section_positions) + assert "| File | Rule | Line | What | Why this is undesirable | Suggested fix |" in content + assert "[`M001`](https://github.com/DataBooth/pytest-notebook-policy/blob/main/docs/RULES.md#m001)" in content + assert "| Runtime | `1.11s` |" in content + assert "deterministic notebook quality snapshot" in content + + +def test_markdown_report_dependency_appendix_is_optional(tmp_path: Path) -> None: + report_path = tmp_path / "reports" / "policy.md" + scanned_file = tmp_path / "sample.py" + scanned_file.write_text("x = 1\n", encoding="utf-8") + settings = _resolve_quality_settings([], _namespace(report_dependency_enrichment=False)) + + _write_markdown_report( + report_path, + settings=settings, + violations=[], + scanned_files=[scanned_file], + started_at=datetime(2026, 6, 1, 0, 0, 0, tzinfo=UTC), + runtime_seconds=0.25, + ) + + content = report_path.read_text(encoding="utf-8") + assert "## Appendix C — NBOM alignment" in content + assert "## Appendix D — Dependency enrichment" not in content + assert "| Dependency enrichment | `False` |" in content + + +def test_nbom_manifest_contract_schema_is_stable(tmp_path: Path) -> None: + nbom_path = tmp_path / "reports" / "manifest.json" + scanned_file = tmp_path / "sample.ipynb" + scanned_file.write_text('{"cells": [], "nbformat": 4, "nbformat_minor": 5}', encoding="utf-8") + settings = _resolve_quality_settings([], _namespace(report_dependency_enrichment=True)) + analysis = NotebookAnalysis( + imports={"requests"}, + file_references={Observed(path=scanned_file, value="data/input.csv")}, + http_requests={Observed(path=scanned_file, value="https://example.com/api")}, + data_sources={Observed(path=scanned_file, value="s3://bucket/object.parquet")}, + ) + violations = [ + Violation( + path=scanned_file, + line=1, + code="J011", + message="Add a top-of-notebook parameter/configuration cell.", + ) + ] + dependency_rows = [ + ("requests", "requests", "2.32.0", "Apache-2.0", "https://requests.readthedocs.io") + ] + + _write_nbom_manifest( + destination=nbom_path, + settings=settings, + violations=violations, + scanned_files=[scanned_file], + analysis=analysis, + dependency_rows=dependency_rows, + started_at=datetime(2026, 6, 1, 0, 0, 0, tzinfo=UTC), + runtime_seconds=0.5, + ) + + manifest = json.loads(nbom_path.read_text(encoding="utf-8")) + assert set(manifest) == { + "schema_version", + "generated_at", + "runtime_seconds", + "tool", + "settings", + "files", + "violations", + "surface", + "dependencies", + "summary", + } + assert manifest["schema_version"] == "0.1" + assert manifest["tool"] == "pytest-notebook-policy" + assert manifest["summary"]["violations"] == 1 + assert manifest["summary"]["dependencies"] == 1 + assert manifest["dependencies"][0]["import"] == "requests" + assert "http_requests" in manifest["dependencies"][0]["capabilities"] diff --git a/tmp/manual_checks/experiments/.python-version b/tmp/manual_checks/experiments/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/tmp/manual_checks/experiments/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/tmp/manual_checks/experiments/README.md b/tmp/manual_checks/experiments/README.md new file mode 100644 index 0000000..e69de29 diff --git a/tmp/manual_checks/experiments/main.py b/tmp/manual_checks/experiments/main.py new file mode 100644 index 0000000..95d00ae --- /dev/null +++ b/tmp/manual_checks/experiments/main.py @@ -0,0 +1,6 @@ +def main(): + print("Hello from experiments!") + + +if __name__ == "__main__": + main()