diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 970e181..32bbed7 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,6 +12,17 @@ updates: default-days: 7 semver-major-days: 14 + - package-ecosystem: "github-actions" + directory: "/" + open-pull-requests-limit: 5 + labels: + - "dependencies" + - "github-actions" + schedule: + interval: "daily" + cooldown: + default-days: 7 + - package-ecosystem: "cargo" directory: "/pycrockford_rs" open-pull-requests-limit: 5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..55f0d5e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,119 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + lint-test: + runs-on: ubuntu-latest + env: + CS_ACCESS_TOKEN: ${{ secrets.CS_ACCESS_TOKEN }} + CODESCENE_CLI_SHA256: ${{ vars.CODESCENE_CLI_SHA256 }} + + WHITAKER_INSTALLER_VERSION: '0.2.5' + CARGO_TERM_COLOR: always + BUILD_PROFILE: debug + + steps: + - name: Check out repository + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: '3.13' + + - name: Install uv + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5 + + - name: Set up Rust + uses: leynos/shared-actions/.github/actions/setup-rust@455d9ed03477c0026da96c2541ca26569a74acac + with: + toolchain: stable + + - name: Cache Rust lint and test tools + uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2 + with: + cache-directories: | + ~/.cargo/bin/cargo-nextest + ~/.cargo/bin/cargo-audit + ~/.cargo/bin/whitaker-installer + ~/.local/bin/whitaker + ~/.local/share/whitaker + + - name: Install Rust lint and test tools + env: + RUSTFLAGS: "" + run: | + if ! command -v whitaker-installer >/dev/null 2>&1; then + if cargo binstall --version >/dev/null 2>&1; then + cargo binstall --no-confirm --locked \ + whitaker-installer@"${WHITAKER_INSTALLER_VERSION}" + else + echo "cargo-binstall unavailable; building whitaker-installer from crates.io" + cargo install --locked \ + whitaker-installer --version "${WHITAKER_INSTALLER_VERSION}" + fi + fi + whitaker-installer --cranelift + cargo install --locked cargo-audit + + - name: Install CLI tools + run: | + for tool in mbake; do uv tool install ${tool}; done + npm install -g markdownlint-cli2 + + - name: Validate Makefile + run: mbake validate Makefile + + - name: Install code + run: make build + + - name: Check formatting + run: make check-fmt + + - name: Run ruff + run: make lint + + - name: Run typechecker + run: make typecheck + + - name: Audit dependencies + run: make audit + + - name: Test and Measure Coverage + uses: leynos/shared-actions/.github/actions/generate-coverage@455d9ed03477c0026da96c2541ca26569a74acac + with: + output-path: coverage.xml + format: cobertura + artefact-name-suffix: msgspec-crockford + + cargo-manifest: pycrockford_rs/Cargo.toml + + - name: Install CodeScene Coverage CLI + if: env.CS_ACCESS_TOKEN != '' + run: | + curl -fsSL --output install-cs-coverage-tool.sh \ + https://downloads.codescene.io/enterprise/cli/install-cs-coverage-tool.sh + if [ -n "${CODESCENE_CLI_SHA256:-}" ]; then + echo "${CODESCENE_CLI_SHA256} install-cs-coverage-tool.sh" | sha256sum -c - + else + echo "::notice::CODESCENE_CLI_SHA256 is not set; skipping installer checksum verification" + fi + bash install-cs-coverage-tool.sh -y + + - name: Upload coverage to CodeScene + if: env.CS_ACCESS_TOKEN != '' + run: | + if [ ! -f coverage.xml ]; then + echo "coverage.xml not found!" + exit 1 + fi + cs-coverage upload \ + --format "cobertura" \ + --metric "line-coverage" \ + coverage.xml diff --git a/.gitignore b/.gitignore index 94d20c6..54543f1 100644 --- a/.gitignore +++ b/.gitignore @@ -200,3 +200,7 @@ target/ # Generated by cargo mutants # Contains mutation testing data **/mutants.out*/ + +# uv tooling caches used by the Makefile +.uv-cache/ +.uv-tools/ diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc new file mode 100644 index 0000000..deee43f --- /dev/null +++ b/.markdownlint-cli2.jsonc @@ -0,0 +1,22 @@ +{ + "config": { + "MD004": { "style": "dash" }, + "MD010": { "code_blocks": false }, + "MD013": { + "line_length": 80, + "code_block_line_length": 120, + "tables": false, + "headings": false + }, + "MD029": { "style": "ordered" } + }, + "ignores": [ + "**/.venv/**", + ".node_modules/**", + "**/node_modules/**", + "**/target/**", + ".terraform/**", + ".uv-cache/**", + "CRUSH.md" + ] +} diff --git a/AGENTS.md b/AGENTS.md index 0a795cd..fe46512 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,107 +2,175 @@ ## Code Style and Structure -* **Code is for humans.** Write your code with clarity and empathy—assume a tired teammate will need to debug it at 3 a.m. -* **Comment *why*, not *what*.** Explain assumptions, edge cases, trade-offs, or complexity. Don't echo the obvious. -* **Clarity over cleverness.** Be concise, but favour explicit over terse or obscure idioms. Prefer code that's easy to follow. -* **Use functions and composition.** Avoid repetition by extracting reusable logic. Prefer generators or comprehensions to imperative repetition when readable. -* **Name things precisely.** Use clear, descriptive variable and function names. For booleans, prefer names with `is`, `has`, or `should`. -* **Structure logically.** Each file should encapsulate a coherent module. Group related code (e.g., models + utilities + fixtures) close together. -* **Group by feature, not layer.** Colocate views, logic, fixtures, and helpers related to a domain concept rather than splitting by type. +- **Code is for humans.** Write your code with clarity and empathy—assume a + tired teammate will need to debug it at 3 a.m. +- **Comment *why*, not *what*.** Explain assumptions, edge cases, trade-offs, + or complexity. Don't echo the obvious. +- **Clarity over cleverness.** Be concise, but favour explicit over terse or + obscure idioms. Prefer code that's easy to follow. +- **Use functions and composition.** Avoid repetition by extracting reusable + logic. Prefer generators or comprehensions to imperative repetition when + readable. +- **Name things precisely.** Use clear, descriptive variable and function + names. For booleans, prefer names with `is`, `has`, or `should`. +- **Structure logically.** Each file should encapsulate a coherent module. + Group related code (e.g., models + utilities + fixtures) close together. +- **Group by feature, not layer.** Colocate views, logic, fixtures, and helpers + related to a domain concept rather than splitting by type. ## Documentation Maintenance -* **Reference:** Use the markdown files within the `docs/` directory as a knowledge base and source of truth for project requirements, dependency choices, and architectural decisions. -* **Update:** When new decisions are made, requirements change, libraries are added/removed, or architectural patterns evolve, **proactively update** the relevant file(s) in the `docs/` directory to reflect the latest state. Ensure the documentation remains accurate and current. +- **Reference:** Use the markdown files within the `docs/` directory as a + knowledge base and source of truth for project requirements, dependency + choices, and architectural decisions. +- **Update:** When new decisions are made, requirements change, libraries are + added/removed, or architectural patterns evolve, **proactively update** the + relevant file(s) in the `docs/` directory to reflect the latest state. + Ensure the documentation remains accurate and current. ## Guidelines for Code Changes & Testing When implementing changes, adhere to the following testing procedures: -* **New Functionality:** - * Implement unit tests covering all new code units (functions, components, classes). Implement tests **before** implementing the unit. - * Implement behavioral tests that verify the end-to-end behavior of the new feature from a user interaction perspective. - * Ensure both unit and behavioral tests pass before considering the functionality complete. -* **Bug Fixes:** - * Before fixing the bug, write a new test (unit or behavioral, whichever is most appropriate) that specifically targets and reproduces the bug. This test should initially fail. - * Implement the bug fix. - * Verify that the new test now passes, along with all existing tests. -* **Modifying Existing Functionality:** - * Identify the existing behavioral and unit tests relevant to the functionality being changed. - * **First, modify the tests** to reflect the new requirements or behavior. - * Run the tests; they should now fail. - * Implement the code changes to the functionality. - * Verify that the modified tests (and all other existing tests) now pass. -* **Refactoring:** - * Identify or create a behavioral test that covers the functionality being refactored. Ensure this test passes **before** starting the refactor. - * Perform the refactoring (e.g., extracting logic into a new unit). - * If new units are created (e.g., a new function or component), add unit tests for these extracted units. - * After the refactor, ensure the original behavioral test **still passes** without modification. Also ensure any new unit tests pass. +- **New Functionality:** + - Implement unit tests covering all new code units (functions, components, + classes). Implement tests **before** implementing the unit. + - Implement behavioral tests that verify the end-to-end behavior of the new + feature from a user interaction perspective. + - Ensure both unit and behavioral tests pass before considering the + functionality complete. +- **Bug Fixes:** + - Before fixing the bug, write a new test (unit or behavioral, whichever is + most appropriate) that specifically targets and reproduces the bug. This + test should initially fail. + - Implement the bug fix. + - Verify that the new test now passes, along with all existing tests. +- **Modifying Existing Functionality:** + - Identify the existing behavioral and unit tests relevant to the + functionality being changed. + - **First, modify the tests** to reflect the new requirements or behavior. + - Run the tests; they should now fail. + - Implement the code changes to the functionality. + - Verify that the modified tests (and all other existing tests) now pass. +- **Refactoring:** + - Identify or create a behavioral test that covers the functionality being + refactored. Ensure this test passes **before** starting the refactor. + - Perform the refactoring (e.g., extracting logic into a new unit). + - If new units are created (e.g., a new function or component), add unit + tests for these extracted units. + - After the refactor, ensure the original behavioral test **still passes** + without modification. Also ensure any new unit tests pass. ## Change Quality & Committing -* **Atomicity:** Aim for small, focused, atomic changes. Each change (and subsequent commit) should represent a single logical unit of work. -* **Quality Gates:** Before considering a change complete or proposing a commit, ensure it meets the following criteria: - * For Python files: - * **Testing:** Passes all relevant unit and behavioral tests according to the guidelines above. - * **Linting:** Passes lint checks (`ruff check` or integrated editor linting). - * **Formatting:** Adheres to formatting standards (`ruff format` or integrated editor formatting). - * **Typechecking:** Passes type checking (`pyright` or integrated editor type checking). - * For TypeScript files: - * **Testing:** Passes all relevant unit and behavioral tests according to the guidelines above. - * **Linting:** Passes lint checks (`biome check .` or integrated editor linting). - * **Formatting:** Adheres to formatting standards (`biome check --apply .` or integrated editor formatting). - * **TypeScript Compilation:** Compiles successfully without TypeScript errors (`tsc --noEmit`). - * For Rust files: - * **Testing:** Passes all relevant unit and behavioral tests according to the guidelines above. - * **Linting:** Passes lint checks (`cargo clippy` or integrated editor linting). - * **Formatting:** Adheres to formatting standards (`cargo fmt` or integrated editor formatting). - * For Markdown files (`.md` only): - * **Linting:** Passes lint checks (`markdownlint filename.md` or integrated editor linting). - * **Mermaid diagrams:** Passes validation using nixie (`nixie filename.md`) -* **Committing:** - * Only changes that meet all the quality gates above should be committed. - * Write clear, descriptive commit messages summarizing the change, following these formatting guidelines: - * **Imperative Mood:** Use the imperative mood in the subject line (e.g., "Fix bug", "Add feature" instead of "Fixed bug", "Added feature"). - * **Subject Line:** The first line should be a concise summary of the change (ideally 50 characters or less). - * **Body:** Separate the subject from the body with a blank line. Subsequent lines should explain the *what* and *why* of the change in more detail, including rationale, goals, and scope. Wrap the body at 72 characters. - * **Formatting:** Use Markdown for any formatted text (like bullet points or code snippets) within the commit message body. - * Do not commit changes that fail any of the quality gates. +- **Atomicity:** Aim for small, focused, atomic changes. Each change (and + subsequent commit) should represent a single logical unit of work. +- **Quality Gates:** Before considering a change complete or proposing a + commit, ensure it meets the following criteria: + - For Python files: + - **Testing:** Passes all relevant unit and behavioral tests according to + the guidelines above. + - **Linting:** Passes lint checks (`ruff check` or integrated editor + linting). + - **Formatting:** Adheres to formatting standards (`ruff format` or + integrated editor formatting). + - **Typechecking:** Passes type checking (`pyright` or integrated editor + type checking). + - For TypeScript files: + - **Testing:** Passes all relevant unit and behavioral tests according to + the guidelines above. + - **Linting:** Passes lint checks (`biome check .` or integrated editor + linting). + - **Formatting:** Adheres to formatting standards (`biome check --apply .` + or integrated editor formatting). + - **TypeScript Compilation:** Compiles successfully without TypeScript + errors (`tsc --noEmit`). + - For Rust files: + - **Testing:** Passes all relevant unit and behavioral tests according to + the guidelines above. + - **Linting:** Passes lint checks (`cargo clippy` or integrated editor + linting). + - **Formatting:** Adheres to formatting standards (`cargo fmt` or integrated + editor formatting). + - For Markdown files (`.md` only): + - **Linting:** Passes lint checks (`markdownlint filename.md` or integrated + editor linting). + - **Mermaid diagrams:** Passes validation using nixie (`nixie filename.md`) +- **Committing:** + - Only changes that meet all the quality gates above should be committed. + - Write clear, descriptive commit messages summarizing the change, following + these formatting guidelines: + - **Imperative Mood:** Use the imperative mood in the subject line (e.g., + "Fix bug", "Add feature" instead of "Fixed bug", "Added feature"). + - **Subject Line:** The first line should be a concise summary of the change + (ideally 50 characters or less). + - **Body:** Separate the subject from the body with a blank line. Subsequent + lines should explain the *what* and *why* of the change in more detail, + including rationale, goals, and scope. Wrap the body at 72 characters. + - **Formatting:** Use Markdown for any formatted text (like bullet points or + code snippets) within the commit message body. + - Do not commit changes that fail any of the quality gates. ## Refactoring Heuristics & Workflow -* **Recognizing Refactoring Needs:** Regularly assess the codebase for potential refactoring opportunities. Consider refactoring when you observe: - * **Long Methods/Functions:** Functions or methods that are excessively long or try to do too many things. - * **Duplicated Code:** Identical or very similar code blocks appearing in multiple places. - * **Complex Conditionals:** Deeply nested or overly complex `if`/`else` or `switch` statements (high cyclomatic complexity). - * **Large Code Blocks for Single Values:** Significant chunks of logic dedicated solely to calculating or deriving a single value. - * **Primitive Obsession / Data Clumps:** Groups of simple variables (strings, numbers, booleans) that are frequently passed around together, often indicating a missing class or object structure. - * **Excessive Parameters:** Functions or methods requiring a very long list of parameters. - * **Feature Envy:** Methods that seem more interested in the data of another class/object than their own. - * **Shotgun Surgery:** A single change requiring small modifications in many different classes or functions. -* **Post-Commit Review:** After committing a functional change or bug fix (that meets all quality gates), review the changed code and surrounding areas using the heuristics above. -* **Separate Atomic Refactors:** If refactoring is deemed necessary: - * Perform the refactoring as a **separate, atomic commit** *after* the functional change commit. - * Ensure the refactoring adheres to the testing guidelines (behavioral tests pass before and after, unit tests added for new units). - * Ensure the refactoring commit itself passes all quality gates. +- **Recognizing Refactoring Needs:** Regularly assess the codebase for + potential refactoring opportunities. Consider refactoring when you observe: + - **Long Methods/Functions:** Functions or methods that are excessively long + or try to do too many things. + - **Duplicated Code:** Identical or very similar code blocks appearing in + multiple places. + - **Complex Conditionals:** Deeply nested or overly complex `if`/`else` or + `switch` statements (high cyclomatic complexity). + - **Large Code Blocks for Single Values:** Significant chunks of logic + dedicated solely to calculating or deriving a single value. + - **Primitive Obsession / Data Clumps:** Groups of simple variables (strings, + numbers, booleans) that are frequently passed around together, often + indicating a missing class or object structure. + - **Excessive Parameters:** Functions or methods requiring a very long list + of parameters. + - **Feature Envy:** Methods that seem more interested in the data of another + class/object than their own. + - **Shotgun Surgery:** A single change requiring small modifications in many + different classes or functions. +- **Post-Commit Review:** After committing a functional change or bug fix (that + meets all quality gates), review the changed code and surrounding areas using + the heuristics above. +- **Separate Atomic Refactors:** If refactoring is deemed necessary: + - Perform the refactoring as a **separate, atomic commit** *after* the + functional change commit. + - Ensure the refactoring adheres to the testing guidelines (behavioral tests + pass before and after, unit tests added for new units). + - Ensure the refactoring commit itself passes all quality gates. ## Rust Development Guidelines -This repository is written in part using Rust and uses Cargo for building and dependency management. Contributors should follow these best practices when working on the project: +This repository is written in part using Rust and uses Cargo for building and +dependency management. Contributors should follow these best practices when +working on the project: -1. **Run `cargo fmt` and `cargo clippy`** before committing to ensure consistent code style and catch common mistakes. -2. **Write unit tests** for new functionality. Run `cargo test` in both the root crate and the `validator` crate. -3. **Document public APIs** using Rustdoc comments (`///`) so documentation can be generated with `cargo doc`. +1. **Run `cargo fmt` and `cargo clippy`** before committing to ensure + consistent code style and catch common mistakes. +2. **Write unit tests** for new functionality. Run `cargo test` in both the + root crate and the `validator` crate. +3. **Document public APIs** using Rustdoc comments (`///`) so documentation can + be generated with `cargo doc`. 4. **Prefer immutable data** and avoid unnecessary `mut` bindings. 5. **Handle errors with the `Result` type** instead of panicking where feasible. -6. **Use explicit version ranges** in `Cargo.toml` and keep dependencies up-to-date. -7. **Avoid unsafe code** unless absolutely necessary and document any usage clearly. -8. **Keep functions small and focused**; if a function grows too large, consider splitting it into helpers. -9. **Commit messages should be descriptive**, explaining what was changed and why. -10. **Check for `TODO` comments** and convert them into issues if more work is required. -11. **Validate Markdown Mermaid diagrams** with `nixie ` before submitting documentation changes. - -These practices will help maintain a high-quality codebase and make collaboration easier. +6. **Use explicit version ranges** in `Cargo.toml` and keep dependencies + up-to-date. +7. **Avoid unsafe code** unless absolutely necessary and document any usage + clearly. +8. **Keep functions small and focused**; if a function grows too large, + consider splitting it into helpers. +9. **Commit messages should be descriptive**, explaining what was changed and + why. +10. **Check for `TODO` comments** and convert them into issues if more work is + required. +11. **Validate Markdown Mermaid diagrams** with `nixie ` before + submitting documentation changes. + +These practices will help maintain a high-quality codebase and make +collaboration easier. ## Python Development Guidelines diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3c85a1b --- /dev/null +++ b/Makefile @@ -0,0 +1,180 @@ +MDLINT ?= markdownlint-cli2 +NIXIE ?= nixie +MDFORMAT_ALL ?= mdformat-all +export PATH := $(HOME)/.local/bin:$(HOME)/.bun/bin:$(PATH) +UV ?= $(shell command -v uv 2>/dev/null || printf '%s/.local/bin/uv' "$$HOME") +USER_CARGO := $(HOME)/.cargo/bin/cargo +USER_WHITAKER := $(HOME)/.local/bin/whitaker +USER_BIN_PATH := $(HOME)/.cargo/bin:$(HOME)/.local/bin:$(HOME)/.bun/bin +TOOLS = $(MDFORMAT_ALL) $(MDLINT) +VENV_TOOLS = pytest +UV_ENV = PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 UV_CACHE_DIR=.uv-cache UV_TOOL_DIR=.uv-tools +PYTEST_XDIST_WORKERS ?= auto +PYTHON_TARGETS ?= msgspec_crockford +PYLINT_PYTHON ?= pypy +PYLINT_TARGETS ?= $(PYTHON_TARGETS) +PYLINT_PYPY_SHIM_REF ?= 726d09f968b4d729ee4b29c71fc732e744854f3b +PYLINT_PYPY_SHIM = git+https://github.com/leynos/pylint-pypy-shim.git@$(PYLINT_PYPY_SHIM_REF) +PYLINT = $(UV_ENV) $(UV) tool run --python $(PYLINT_PYTHON) --from '$(PYLINT_PYPY_SHIM)' pylint-pypy + +CARGO ?= $(or $(shell command -v cargo 2>/dev/null),$(wildcard $(USER_CARGO)),cargo) +CARGO_AVAILABLE := $(shell command -v $(CARGO) 2>/dev/null) +BUILD_JOBS ?= +RUST_FLAGS ?= +RUST_FLAGS := -D warnings $(RUST_FLAGS) +RUSTDOC_FLAGS ?= +RUSTDOC_FLAGS := -D warnings $(RUSTDOC_FLAGS) +RUST_CRATE_DIR ?= pycrockford_rs +CARGO_FLAGS ?= --manifest-path $(RUST_CRATE_DIR)/Cargo.toml --all-targets --all-features +CLIPPY_FLAGS ?= $(CARGO_FLAGS) -- $(RUST_FLAGS) +TEST_FLAGS ?= $(CARGO_FLAGS) +TEST_CMD := $(if $(CARGO_AVAILABLE),$(if $(shell $(CARGO) nextest --version 2>/dev/null),nextest run,test),) +WHITAKER_CARGO_FLAGS ?= --all-targets --all-features +WHITAKER_INSTALLER_VERSION ?= 0.2.5 +WHITAKER ?= $(or $(shell command -v whitaker 2>/dev/null),$(wildcard $(USER_WHITAKER)),whitaker) + + +.PHONY: help all audit clean build build-release lint lint-python fmt check-fmt \ + markdownlint nixie test typecheck $(TOOLS) $(VENV_TOOLS) lint-rust rust-audit whitaker + +.DEFAULT_GOAL := all + +all: build check-fmt lint typecheck test + +define ensure_uv + @command -v $(UV) >/dev/null 2>&1 || { \ + printf "Error: uv is required, but '%s' was not found or is not executable\n" "$(UV)" >&2; \ + exit 1; \ + } +endef + +.venv: pyproject.toml + $(call ensure_uv) + $(UV_ENV) $(UV) venv --clear + +build: .venv ## Build virtual-env and install deps + $(UV_ENV) $(UV) sync --group dev + +build-release: ## Build artefacts (sdist & wheel) + $(call ensure_uv) + $(UV_ENV) $(UV) run python -m build --sdist --wheel + +clean: ## Remove build artifacts + rm -rf build dist *.egg-info \ + .mypy_cache .pytest_cache .coverage coverage.* \ + lcov.info htmlcov .venv .uv-cache .uv-tools $(RUST_CRATE_DIR)/target + find . -type d -name '__pycache__' -print0 | xargs -0 -r rm -rf + +define ensure_tool + @command -v $(1) >/dev/null 2>&1 || { \ + printf "Error: '%s' is required, but not installed\n" "$(1)" >&2; \ + exit 1; \ + } +endef + +define ensure_tool_venv + @$(UV_ENV) $(UV) run which $(1) >/dev/null 2>&1 || { \ + printf "Error: '%s' is required in the virtualenv, but is not installed\n" "$(1)" >&2; \ + exit 1; \ + } +endef + +ifneq ($(strip $(TOOLS)),) +$(TOOLS): ## Verify required CLI tools + $(call ensure_tool,$@) +endif + + +ifneq ($(strip $(VENV_TOOLS)),) +.PHONY: $(VENV_TOOLS) +$(VENV_TOOLS): build ## Verify required CLI tools in venv + $(call ensure_tool_venv,$@) +endif + + +define ensure_cargo + @test -n "$(CARGO_AVAILABLE)" || { \ + printf "Error: cargo is required for Rust targets, but '%s' was not found on PATH\n" "$(CARGO)" >&2; \ + exit 1; \ + } +endef + +whitaker: ## Install Whitaker when the Rust lint target needs it + @if ! command -v $(WHITAKER) >/dev/null 2>&1; then \ + test -n "$(CARGO_AVAILABLE)" || { \ + printf "Error: cargo is required to install Whitaker, but '%s' was not found on PATH\n" "$(CARGO)" >&2; \ + exit 1; \ + }; \ + $(CARGO) install --locked \ + whitaker-installer --version "$(WHITAKER_INSTALLER_VERSION)"; \ + PATH="$(HOME)/.cargo/bin:$$PATH" whitaker-installer --cranelift; \ + fi + + +fmt: build $(MDFORMAT_ALL) ## Format sources + $(UV_ENV) $(UV) run ruff format $(PYTHON_TARGETS) + $(UV_ENV) $(UV) run ruff check --select I --fix $(PYTHON_TARGETS) + + $(call ensure_cargo) + $(CARGO) fmt --manifest-path $(RUST_CRATE_DIR)/Cargo.toml --all + + $(MDFORMAT_ALL) + +check-fmt: build ## Verify formatting + $(UV_ENV) $(UV) run ruff format --check $(PYTHON_TARGETS) + + $(call ensure_cargo) + $(CARGO) fmt --manifest-path $(RUST_CRATE_DIR)/Cargo.toml --all -- --check + + # mdformat-all doesn't currently do checking + +lint: lint-python lint-rust ## Run linters + +lint-python: build ## Run Python linters + $(UV_ENV) $(UV) run ruff check $(PYTHON_TARGETS) + $(UV_ENV) $(UV) run interrogate --fail-under 100 $(PYTHON_TARGETS) + $(PYLINT) $(PYLINT_TARGETS) + + +lint-rust: build whitaker ## Run Rust linters (Clippy and the Whitaker Dylint suite) + $(call ensure_cargo) + RUSTDOCFLAGS="$(RUSTDOC_FLAGS)" $(CARGO) doc --no-deps --manifest-path $(RUST_CRATE_DIR)/Cargo.toml + $(CARGO) clippy $(CLIPPY_FLAGS) + cd $(RUST_CRATE_DIR) && PATH="$(USER_BIN_PATH):$(PATH)" RUSTFLAGS="$(RUST_FLAGS)" $(WHITAKER) --all -- $(WHITAKER_CARGO_FLAGS) + + +typecheck: build ## Run typechecking + $(UV_ENV) $(UV) run ty --version + $(UV_ENV) $(UV) run ty check $(PYTHON_TARGETS) + +audit: build ## Audit dependencies for known vulnerabilities + $(UV_ENV) $(UV) run pip-audit + + $(MAKE) rust-audit + +rust-audit: ## Audit Rust extension dependencies for known vulnerabilities + $(call ensure_cargo) + cd $(RUST_CRATE_DIR) && $(CARGO) audit + + +markdownlint: $(MDLINT) ## Lint Markdown files + env -u NO_COLOR $(MDLINT) '**/*.md' + +nixie: ## Validate Mermaid diagrams + $(call ensure_tool,$(NIXIE)) + $(NIXIE) --no-sandbox + +test: build $(VENV_TOOLS) ## Run tests + $(UV_ENV) $(UV) run pytest -v -n $(PYTEST_XDIST_WORKERS) + + @test -n "$(CARGO_AVAILABLE)" || { \ + printf "Error: cargo is required for Rust tests, but '%s' was not found on PATH\n" "$(CARGO)" >&2; \ + exit 1; \ + } + RUSTFLAGS="$(RUST_FLAGS)" $(CARGO) $(TEST_CMD) $(TEST_FLAGS) $(BUILD_JOBS) + RUSTFLAGS="$(RUST_FLAGS)" $(CARGO) test --doc --manifest-path $(RUST_CRATE_DIR)/Cargo.toml --all-features + + +help: ## Show available targets + @grep -E '^[a-zA-Z_-]+:.*?##' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS=":.*##"; printf "Available targets:\n"} {gsub(/^[[:space:]]+/, "", $$2); printf " %-20s %s\n", $$1, $$2}' diff --git a/_pycrockford_rs_bindings.pyi b/_pycrockford_rs_bindings.pyi new file mode 100644 index 0000000..236cd7c --- /dev/null +++ b/_pycrockford_rs_bindings.pyi @@ -0,0 +1,20 @@ +"""Type stubs for the Rust extension module.""" + +import uuid + +class CrockfordUUID: + """UUID held as 16 raw bytes, rendered as Crockford Base32.""" + + def __init__(self, value: str | bytes | uuid.UUID) -> None: ... + @property + def bytes(self) -> bytes: ... + @property + def uuid(self) -> uuid.UUID: ... + def __bytes__(self) -> bytes: ... + @classmethod + def generate_v4(cls) -> CrockfordUUID: ... + @classmethod + def generate_v7(cls) -> CrockfordUUID: ... + +def encode_crockford(b: bytes) -> str: ... +def decode_crockford(s: str) -> bytes: ... diff --git a/docs/crockford-uuids-with-msgspec-rust.md b/docs/crockford-uuids-with-msgspec-rust.md index fac579a..27364aa 100644 --- a/docs/crockford-uuids-with-msgspec-rust.md +++ b/docs/crockford-uuids-with-msgspec-rust.md @@ -4,109 +4,206 @@ ### A. Project Goal -This report outlines a design proposal for integrating Crockford Base32 encoded Universally Unique Identifiers (UUIDs) with the Python `msgspec` library. The primary objective is to achieve an implementation that is both highly efficient in terms of processing speed and ergonomic for Python developers to use within `msgspec`'s serialization and validation workflows.1 +This report outlines a design proposal for integrating Crockford Base32 encoded +Universally Unique Identifiers (UUIDs) with the Python `msgspec` library. The +primary objective is to achieve an implementation that is both highly efficient +in terms of processing speed and ergonomic for Python developers to use within +`msgspec`'s serialization and validation workflows.1 ### B. Core Strategy -The proposed strategy leverages the performance capabilities of Rust for the computationally intensive parts of Crockford UUID encoding and decoding. This Rust core will be exposed to Python using PyO3 2, creating a native Python extension. The resulting product will be a pip-installable package, with `uv` 4 utilized for modern packaging and environment management, and `pytest` employed for comprehensive testing. +The proposed strategy leverages the performance capabilities of Rust for the +computationally intensive parts of Crockford UUID encoding and decoding. This +Rust core will be exposed to Python using PyO3 2, creating a native Python +extension. The resulting product will be a pip-installable package, with `uv` 4 +utilized for modern packaging and environment management, and `pytest` employed +for comprehensive testing. ### C. Key Benefits This approach is anticipated to yield several key benefits: -1. **Performance:** By implementing the Crockford Base32 logic in Rust, which is known for its speed and efficiency 6, the solution can significantly outperform pure Python implementations, especially critical in high-throughput applications where `msgspec` is often chosen for its speed.1 -2. **Ergonomics:** A carefully designed Python API, centered around a custom `CrockfordUUID` type, will provide a seamless and intuitive experience for developers, integrating naturally with Python's type system and `msgspec`'s schema definitions.1 -3. **Reliability:** Leveraging `msgspec` for serialization and validation ensures that the Crockford UUIDs are handled correctly within message structures, benefiting from `msgspec`'s strict compliance and robust error reporting.1 +1. **Performance:** By implementing the Crockford Base32 logic in Rust, which + is known for its speed and efficiency 6, the solution can significantly + outperform pure Python implementations, especially critical in + high-throughput applications where `msgspec` is often chosen for its speed.1 +2. **Ergonomics:** A carefully designed Python API, centered around a custom + `CrockfordUUID` type, will provide a seamless and intuitive experience for + developers, integrating naturally with Python's type system and `msgspec`'s + schema definitions.1 +3. **Reliability:** Leveraging `msgspec` for serialization and validation + ensures that the Crockford UUIDs are handled correctly within message + structures, benefiting from `msgspec`'s strict compliance and robust error + reporting.1 ## II. Understanding Crockford Base32 UUIDs ### A. Crockford Base32 Specification -Crockford Base32 is a Base32 encoding scheme designed by Douglas Crockford for human readability and error resistance.8 Its key features include: - -- **Character Set:** It uses a specific set of 32 symbols, comprising 10 digits (0-9) and 22 uppercase letters, excluding I, L, O, and U to prevent visual confusion and accidental obscenities.8 -- **Case Insensitivity in Decoding:** When decoding, uppercase and lowercase letters are accepted. Specifically, 'i' and 'l' are treated as '1', and 'o' is treated as '0'.8 Encoding, however, uses only uppercase letters. -- **Hyphen Handling:** Hyphens (`-`) can be inserted into encoded strings to improve readability by breaking long strings into manageable chunks. These hyphens are ignored during the decoding process.8 -- **Error Resistance:** The choice of character set and the exclusion of similar-looking characters aim to reduce transcription errors.8 -- **Optional Checksum:** The specification includes an optional check symbol mechanism (modulo 37) to detect wrong-symbol and transposed-symbol errors, using 5 additional symbols (`*`, `~`, `$`, `=`, `U`) for this purpose.8 - -Each symbol in Crockford Base32 represents 5 bits of data, making it more compact than hexadecimal (Base16) representation.8 +Crockford Base32 is a Base32 encoding scheme designed by Douglas Crockford for +human readability and error resistance.8 Its key features include: + +- **Character Set:** It uses a specific set of 32 symbols, comprising 10 digits + (0-9) and 22 uppercase letters, excluding I, L, O, and U to prevent visual + confusion and accidental obscenities.8 +- **Case Insensitivity in Decoding:** When decoding, uppercase and lowercase + letters are accepted. Specifically, 'i' and 'l' are treated as '1', and 'o' + is treated as '0'.8 Encoding, however, uses only uppercase letters. +- **Hyphen Handling:** Hyphens (`-`) can be inserted into encoded strings to + improve readability by breaking long strings into manageable chunks. These + hyphens are ignored during the decoding process.8 +- **Error Resistance:** The choice of character set and the exclusion of + similar-looking characters aim to reduce transcription errors.8 +- **Optional Checksum:** The specification includes an optional check symbol + mechanism (modulo 37) to detect wrong-symbol and transposed-symbol errors, + using 5 additional symbols (`*`, `~`, `$`, `=`, `U`) for this purpose.8 + +Each symbol in Crockford Base32 represents 5 bits of data, making it more +compact than hexadecimal (Base16) representation.8 ### B. UUIDs in General -Universally Unique Identifiers (UUIDs) are 128-bit numbers used to uniquely identify information in computer systems.9 They are designed to be unique across space and time without requiring a central allocating authority, making them particularly useful in distributed systems.9 Standard UUIDs are typically represented as a 32-character hexadecimal string, often displayed in five groups separated by hyphens (e.g., `67e55044-10b1-426f-9247-bb680e5fe0c8`).9 +Universally Unique Identifiers (UUIDs) are 128-bit numbers used to uniquely +identify information in computer systems.9 They are designed to be unique +across space and time without requiring a central allocating authority, making +them particularly useful in distributed systems.9 Standard UUIDs are typically +represented as a 32-character hexadecimal string, often displayed in five +groups separated by hyphens (e.g., `67e55044-10b1-426f-9247-bb680e5fe0c8`).9 Several UUID versions exist, each with different generation strategies 11: -- **UUIDv4:** These are generated using purely random or pseudo-random numbers. They offer a very low probability of collision.11 -- **UUIDv7:** A more recent version, UUIDv7 is time-based (Unix Epoch timestamp with millisecond precision) and includes random bits to ensure uniqueness. They are k-sortable, which can be beneficial for database indexing.11 UUIDv7 aims to improve upon v1 by not using MAC addresses and offering better randomness characteristics. +- **UUIDv4:** These are generated using purely random or pseudo-random numbers. + They offer a very low probability of collision.11 +- **UUIDv7:** A more recent version, UUIDv7 is time-based (Unix Epoch timestamp + with millisecond precision) and includes random bits to ensure uniqueness. + They are k-sortable, which can be beneficial for database indexing.11 UUIDv7 + aims to improve upon v1 by not using MAC addresses and offering better + randomness characteristics. -For applications like API keys or other identifiers where readability and sortability are valued, UUIDv7 is increasingly preferred, while UUIDv4 remains a simple and robust choice for general uniqueness.11 +For applications like API keys or other identifiers where readability and +sortability are valued, UUIDv7 is increasingly preferred, while UUIDv4 remains +a simple and robust choice for general uniqueness.11 ### C. Why Crockford for UUIDs? -Encoding standard UUIDs (which are 128-bit values) using Crockford Base32 offers several advantages: - -- **Readability:** The restricted character set and case-insensitivity during input make Crockford-encoded UUIDs easier for humans to read, transcribe, and communicate compared to hexadecimal strings.8 -- **Reduced Length:** A 128-bit UUID requires 32 hexadecimal characters. In Crockford Base32, since each character represents 5 bits, a 128-bit value can be represented in 128/5=25.6 characters, typically rounded up to 26 characters (by padding the input to a multiple of 5 bits). This is shorter than the 32 characters of hex encoding.11 +Encoding standard UUIDs (which are 128-bit values) using Crockford Base32 +offers several advantages: + +- **Readability:** The restricted character set and case-insensitivity during + input make Crockford-encoded UUIDs easier for humans to read, transcribe, and + communicate compared to hexadecimal strings.8 +- **Reduced Length:** A 128-bit UUID requires 32 hexadecimal characters. In + Crockford Base32, since each character represents 5 bits, a 128-bit value can + be represented in 128/5=25.6 characters, typically rounded up to 26 + characters (by padding the input to a multiple of 5 bits). This is shorter + than the 32 characters of hex encoding.11 - **URL-Friendliness:** The character set is generally URL-safe. -- **Common Usage:** Crockford Base32 is sometimes used for API keys and other identifiers where human interaction or display is a factor.11 +- **Common Usage:** Crockford Base32 is sometimes used for API keys and other + identifiers where human interaction or display is a factor.11 -The combination of standard UUID generation (ensuring global uniqueness) with Crockford Base32 encoding (enhancing usability) provides a robust and user-friendly identifier system. +The combination of standard UUID generation (ensuring global uniqueness) with +Crockford Base32 encoding (enhancing usability) provides a robust and +user-friendly identifier system. ## III. Rust Implementation Core (`pycrockford_rs`) -The heart of the efficient Crockford UUID handling will reside in a Rust library, provisionally named `pycrockford_rs`. This component will be responsible for the core logic of encoding UUID bytes into Crockford Base32 strings and decoding Crockford Base32 strings back into UUID bytes. +The heart of the efficient Crockford UUID handling will reside in a Rust +library, provisionally named `pycrockford_rs`. This component will be +responsible for the core logic of encoding UUID bytes into Crockford Base32 +strings and decoding Crockford Base32 strings back into UUID bytes. ### A. Choice of Rust Crates and Logic -For generating and manipulating standard UUIDs (e.g., v4, v7), the widely adopted `uuid` Rust crate 9 will be utilized. This crate provides robust functionality for UUID parsing, generation, and handling of different versions. It is well-maintained and feature-rich, including support for `no_std` environments and various serialization frameworks if needed, though for this project its primary use will be for standard UUID operations.12 - -The Crockford Base32 encoding and decoding logic itself will be implemented directly within the `pycrockford_rs` library. While crates like `crockford-uuid` 13 or `uuid32` 14 exist, their documentation or specific API features might not perfectly align with the requirements for tight `msgspec` integration and the desired error handling granularity. For instance, the `crockford-uuid` crate's detailed API for parsing and validation was not readily available from the provided information.13 A custom implementation, potentially drawing inspiration from established Python libraries like `base32-crockford` 15 or `base32-lib` 17 for algorithm correctness, allows for precise control over performance optimizations, error types, and direct mapping to the 128-bit UUID byte array. This control is paramount for achieving the efficiency goals. - -The implementation will strictly adhere to the Crockford Base32 specification 8, including the character set, handling of 'I', 'L', 'O' during decoding, and ignoring hyphens. Checksum validation and generation can be considered as an optional feature, potentially deferred if not critical for the initial `msgspec` use case, which typically prioritizes compact representation. +For generating and manipulating standard UUIDs (e.g., v4, v7), the widely +adopted `uuid` Rust crate 9 will be utilized. This crate provides robust +functionality for UUID parsing, generation, and handling of different versions. +It is well-maintained and feature-rich, including support for `no_std` +environments and various serialization frameworks if needed, though for this +project its primary use will be for standard UUID operations.12 + +The Crockford Base32 encoding and decoding logic itself will be implemented +directly within the `pycrockford_rs` library. While crates like +`crockford-uuid` 13 or `uuid32` 14 exist, their documentation or specific API +features might not perfectly align with the requirements for tight `msgspec` +integration and the desired error handling granularity. For instance, the +`crockford-uuid` crate's detailed API for parsing and validation was not +readily available from the provided information.13 A custom implementation, +potentially drawing inspiration from established Python libraries like +`base32-crockford` 15 or `base32-lib` 17 for algorithm correctness, allows for +precise control over performance optimizations, error types, and direct mapping +to the 128-bit UUID byte array. This control is paramount for achieving the +efficiency goals. + +The implementation will strictly adhere to the Crockford Base32 specification +8, including the character set, handling of 'I', 'L', 'O' during decoding, and +ignoring hyphens. Checksum validation and generation can be considered as an +optional feature, potentially deferred if not critical for the initial +`msgspec` use case, which typically prioritizes compact representation. ### B. Core Rust API Design -The Rust API exposed by `pycrockford_rs` (before PyO3 wrapping) will be minimal and focused: +The Rust API exposed by `pycrockford_rs` (before PyO3 wrapping) will be minimal +and focused: 1. `decode_crockford_to_bytes(crockford_str: &str) -> Result<[u8; 16], CrockfordError>` - - **Input:** A string slice (`&str`) representing the Crockford Base32 encoded UUID. - - **Output:** A `Result` which, on success, contains a 16-byte array (`[u8; 16]`) representing the decoded UUID. On failure, it returns a `CrockfordError`. - - **Logic:** This function will parse the input string, validate characters against the Crockford set (handling 'I', 'L', 'O' as per spec), ignore hyphens, convert the 5-bit symbols back to bytes, and ensure the output is exactly 128 bits (16 bytes). + - **Input:** A string slice (`&str`) representing the Crockford Base32 + encoded UUID. + - **Output:** A `Result` which, on success, contains a 16-byte array + (`[u8; 16]`) representing the decoded UUID. On failure, it returns a + `CrockfordError`. + - **Logic:** This function will parse the input string, validate characters + against the Crockford set (handling 'I', 'L', 'O' as per spec), ignore + hyphens, convert the 5-bit symbols back to bytes, and ensure the output is + exactly 128 bits (16 bytes). 2. `encode_bytes_to_crockford(uuid_bytes: &[u8; 16]) -> String` - **Input:** A 16-byte array slice (`&[u8; 16]`) representing the UUID. - - **Output:** A `String` containing the Crockford Base32 encoded representation. - - **Logic:** This function will take the 16 bytes, process them in 5-bit chunks, and map each chunk to the corresponding Crockford Base32 uppercase character. The standard output will be compact, without hyphens or checksums, to ensure efficiency for serialization. + - **Output:** A `String` containing the Crockford Base32 encoded + representation. + - **Logic:** This function will take the 16 bytes, process them in 5-bit + chunks, and map each chunk to the corresponding Crockford Base32 uppercase + character. The standard output will be compact, without hyphens or + checksums, to ensure efficiency for serialization. 3. `CrockfordError` **Enum** - - A custom error enum will be defined to provide specific information about failures during decoding. This allows for granular error reporting back to the Python layer. + - A custom error enum will be defined to provide specific information about + failures during decoding. This allows for granular error reporting back to + the Python layer. - Possible variants: - - `InvalidCharacter(char)`: Encountered a character not valid in Crockford Base32. + - `InvalidCharacter(char)`: Encountered a character not valid in + Crockford Base32. - `InvalidLength(usize)`: The decoded data does not result in exactly 128 bits. - `InvalidChecksum` (if checksums are implemented and validation fails). -This focused Rust API ensures that the performance-critical operations are handled efficiently in Rust, providing a solid foundation for the Python bindings. +This focused Rust API ensures that the performance-critical operations are +handled efficiently in Rust, providing a solid foundation for the Python +bindings. ## IV. PyO3 Bridge: Exposing Rust to Python -To make the Rust-implemented Crockford UUID functionality available in Python, PyO3 will be used to create native Python bindings.2 PyO3 facilitates seamless interoperability between Rust and Python, allowing Rust code to be called from Python with minimal overhead and providing tools to map data types between the two languages. +To make the Rust-implemented Crockford UUID functionality available in Python, +PyO3 will be used to create native Python bindings.2 PyO3 facilitates seamless +interoperability between Rust and Python, allowing Rust code to be called from +Python with minimal overhead and providing tools to map data types between the +two languages. ### A. PyO3 Module Structure and Function Exposure -The Rust code will be compiled into a Python extension module, conventionally named with a leading underscore, for example, `_pycrockford_rs_bindings`. This module will house the Python-callable functions and classes. +The Rust code will be compiled into a Python extension module, conventionally +named with a leading underscore, for example, `_pycrockford_rs_bindings`. This +module will house the Python-callable functions and classes. 1. Module Definition: - The Rust lib.rs file will use the #\[pymodule\] macro to define the structure of the Python module. This macro takes the module name as an argument and a function that populates the module with functions and classes.18 + The Rust lib.rs file will use the #\[pymodule\] macro to define the + structure of the Python module. This macro takes the module name as an + argument and a function that populates the module with functions and + classes.18 - Rust - - ``` + ```rust // In src/lib.rs use pyo3::prelude::*; @@ -126,34 +223,57 @@ The Rust code will be compiled into a Python extension module, conventionally na 2. Exposing Functions: - The core Rust functions decode_crockford_to_bytes and encode_bytes_to_crockford will be wrapped using the #\[pyfunction\] macro to make them callable from Python. These wrappers will handle the conversion of Python types to Rust types and vice-versa. + The core Rust functions decode_crockford_to_bytes and + encode_bytes_to_crockford will be wrapped using the #\[pyfunction\] macro to + make them callable from Python. These wrappers will handle the conversion of + Python types to Rust types and vice-versa. - - #\[pyfunction\] fn decode_crockford_py(s: &str) -> PyResult<PyObject>: + - #\[pyfunction\] fn decode_crockford_py(s: &str) -> + PyResult<PyObject>: - This function will take a Python string, call the internal Rust decode_crockford_to_bytes function. On success, it converts the resulting \[u8; 16\] into Python bytes. On error, it converts the CrockfordError into a Python exception (see Section IV.B). The PyObject return type allows returning PyBytes which represents Python bytes. + This function will take a Python string, call the internal Rust + decode_crockford_to_bytes function. On success, it converts the + resulting \[u8; 16\] into Python bytes. On error, it converts the + CrockfordError into a Python exception (see Section IV.B). The PyObject + return type allows returning PyBytes which represents Python bytes. - - #\[pyfunction\] fn encode_crockford_py(b: &\[u8\]) -> PyResult<String>: + - #\[pyfunction\] fn encode_crockford_py(b: &\[u8\]) -> + PyResult<String>: - This function will take Python bytes (received as &\[u8\]), ensure it's 16 bytes long (or handle error), convert to &\[u8; 16\] if necessary, call the internal Rust encode_bytes_to_crockford function, and return the resulting Crockford string. + This function will take Python bytes (received as &\[u8\]), ensure it's + 16 bytes long (or handle error), convert to &\[u8; 16\] if necessary, + call the internal Rust encode_bytes_to_crockford function, and return + the resulting Crockford string. 3. Data Type Conversions: - PyO3 handles many common data type conversions automatically or with straightforward helpers.2 For this project, the key conversions are: + PyO3 handles many common data type conversions automatically or with + straightforward helpers.2 For this project, the key conversions are: - Python `str` to Rust `&str` (for Crockford strings passed to the decoder). - - Rust `String` to Python `str` (for Crockford strings returned by the encoder). + - Rust `String` to Python `str` (for Crockford strings returned by the + encoder). - Python `bytes` to Rust `&[u8]` (for UUID bytes passed to the encoder). - - Rust `[u8; 16]` (or `Vec`) to Python `bytes` (for UUID bytes returned by the decoder). This can be achieved using `PyBytes::new` in Rust. + - Rust `[u8; 16]` (or `Vec`) to Python `bytes` (for UUID bytes returned + by the decoder). This can be achieved using `PyBytes::new` in Rust. 4. Python-Native CrockfordUUID Type (using #\[pyclass\]): - For optimal ergonomics and integration with Python's type system and msgspec, a dedicated Python class CrockfordUUID will be defined. This class will be implemented in Rust using PyO3's #\[pyclass\] macro.2 This approach is superior to merely exposing raw decoding and encoding functions because it allows users to work with CrockfordUUID objects directly, which can encapsulate behavior and state, and integrate more naturally into Python codebases. - - A `CrockfordUUID` object can be instantiated from a Crockford string, raw bytes, or even a standard Python `uuid.UUID` object. It can then provide methods to access its string representation, byte representation, or convert back to a `uuid.UUID` object. This object-oriented approach enhances clarity and usability significantly. - - Rust - - ``` + For optimal ergonomics and integration with Python's type system and + msgspec, a dedicated Python class CrockfordUUID will be defined. This class + will be implemented in Rust using PyO3's #\[pyclass\] macro.2 This approach + is superior to merely exposing raw decoding and encoding functions because + it allows users to work with CrockfordUUID objects directly, which can + encapsulate behavior and state, and integrate more naturally into Python + codebases. + + A `CrockfordUUID` object can be instantiated from a Crockford string, raw + bytes, or even a standard Python `uuid.UUID` object. It can then provide + methods to access its string representation, byte representation, or convert + back to a `uuid.UUID` object. This object-oriented approach enhances clarity + and usability significantly. + + ```rust // In src/lib.rs, part of the PyO3 module use pyo3::types::{PyBytes, PyString}; use pyo3::exceptions::PyValueError; @@ -257,17 +377,22 @@ The Rust code will be compiled into a Python extension module, conventionally na ``` - This `#[pyclass]` approach provides a rich, Python-native feel for the `CrockfordUUID` type, making it far more usable than just standalone conversion functions. It allows for type hinting, attribute access (e.g., `.bytes`, `.uuid`), and standard Python methods (`__str__`, `__repr__`, `__eq__`, `__hash__`). + This `#[pyclass]` approach provides a rich, Python-native feel for the + `CrockfordUUID` type, making it far more usable than just standalone + conversion functions. It allows for type hinting, attribute access (e.g., + `.bytes`, `.uuid`), and standard Python methods (`__str__`, `__repr__`, + `__eq__`, `__hash__`). ### B. Error Handling: Rust to Python -Rust errors (instances of CrockfordError) need to be translated into Python exceptions. PyO3 allows for defining custom Python exception types in Rust or mapping Rust errors to existing Python exceptions.3 - -A custom Python exception, CrockfordUUIDError, will be created, inheriting from a standard Python exception like ValueError. +Rust errors (instances of CrockfordError) need to be translated into Python +exceptions. PyO3 allows for defining custom Python exception types in Rust or +mapping Rust errors to existing Python exceptions.3 -Rust +A custom Python exception, CrockfordUUIDError, will be created, inheriting from +a standard Python exception like ValueError. -``` +```rust // In src/lib.rs use pyo3::create_exception; @@ -287,35 +412,49 @@ impl From for PyErr { } ``` -This ensures that errors originating from the Rust core are propagated to Python in a Pythonic way, allowing standard `try...except CrockfordUUIDError:` blocks. +This ensures that errors originating from the Rust core are propagated to +Python in a Pythonic way, allowing standard `try...except CrockfordUUIDError:` +blocks. -**Table 1: Rust-Python Data Mapping via PyO3** +Table 1: Rust-Python Data Mapping via PyO3 - - -

Rust Type

PyO3 Conversion Mechanism

Python Type

Notes

&str (input) / String (output)

Automatic / PyString

str

For Crockford encoded strings.

&[u8] (input) / [u8; 16] or Vec<u8> (Rust internal)

PyBytes::as_bytes() / PyBytes::new(py, &data)

bytes

For 16-byte UUID binary data.

my_rust_module::CrockfordError (Rust enum)

Custom From<CrockfordError> for PyErr implementation

pycrockford_msgspec.CrockfordUUIDError

Custom Python exception derived from ValueError.

struct CrockfordUUID (Rust struct with #[pyclass])

#[pyclass] macro

pycrockford_msgspec.CrockfordUUID (Python class)

Python wrapper class providing methods and properties.

uuid::Uuid (from uuid crate, Rust internal)

Handled internally within Rust methods of CrockfordUUID

(exposed as uuid.UUID via CrockfordUUID.uuid property)

Used for generating v4/v7 UUIDs before Crockford encoding.

+| Rust Type | PyO3 Conversion Mechanism | Python Type | Notes | +| ----------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------- | +| &str (input) / String (output) | Automatic / PyString | str | For Crockford encoded strings. | +| &[u8] (input) / [u8; 16] or `Vec` (Rust internal) | PyBytes::as_bytes() / PyBytes::new(py, &data) | bytes | For 16-byte UUID binary data. | +| my_rust_module::CrockfordError (Rust enum) | Custom `From` for PyErr implementation | pycrockford_msgspec.CrockfordUUIDError | Custom Python exception derived from ValueError. | +| struct CrockfordUUID (Rust struct with #[pyclass]) | #[pyclass] macro | pycrockford_msgspec.CrockfordUUID (Python class) | Python wrapper class providing methods and properties. | +| uuid::Uuid (from uuid crate, Rust internal) | Handled internally within Rust methods of CrockfordUUID | (exposed as uuid.UUID via CrockfordUUID.uuid property) | Used for generating v4/v7 UUIDs before Crockford encoding. | -This table clarifies the transformations occurring at the Rust-Python boundary, which is essential for both implementing the PyO3 layer and for users to understand the data flow. The design prioritizes direct and efficient mappings for performance-critical paths. +This table clarifies the transformations occurring at the Rust-Python boundary, +which is essential for both implementing the PyO3 layer and for users to +understand the data flow. The design prioritizes direct and efficient mappings +for performance-critical paths. ## V. Seamless `msgspec` Integration -The primary goal is to use these Rust-accelerated Crockford UUIDs within `msgspec`.1 `msgspec` is a high-performance serialization and validation library that supports custom type extensions through encoder and decoder hooks.1 +The primary goal is to use these Rust-accelerated Crockford UUIDs within +`msgspec`.1 `msgspec` is a high-performance serialization and validation +library that supports custom type extensions through encoder and decoder hooks.1 ### A. Implementing `msgspec` Hooks -To enable `msgspec` to handle the `CrockfordUUID` type, custom encoder and decoder hooks must be provided. +To enable `msgspec` to handle the `CrockfordUUID` type, custom encoder and +decoder hooks must be provided. 1. CrockfordUUID Registration/Hook Provision: - msgspec allows enc_hook and dec_hook arguments to be passed to its Encoder and Decoder objects, or directly to the encode/decode methods.16 This means users will need to explicitly use these hooks when working with types containing CrockfordUUID. The library will provide these hook functions. + msgspec allows enc_hook and dec_hook arguments to be passed to its Encoder + and Decoder objects, or directly to the encode/decode methods.16 This means + users will need to explicitly use these hooks when working with types + containing CrockfordUUID. The library will provide these hook functions. 2. Decoder Hook (dec_hook) Implementation: - The decoder hook is responsible for converting a basic JSON/MessagePack type (expected to be a string for Crockford UUIDs) into a CrockfordUUID instance. + The decoder hook is responsible for converting a basic JSON/MessagePack type + (expected to be a string for Crockford UUIDs) into a CrockfordUUID instance. - Python - - ``` + ```python # In pycrockford_msgspec/hooks.py from typing import Type, Any from.types import CrockfordUUID # Assuming CrockfordUUID is defined/exposed here @@ -343,15 +482,22 @@ To enable `msgspec` to handle the `CrockfordUUID` type, custom encoder and decod ``` - This hook checks if the target type is `CrockfordUUID` and if the input from the serialized data (`obj`) is a string. It then uses the `CrockfordUUID` constructor (which internally calls the PyO3-wrapped Rust function) to perform the decoding. Errors from the Rust layer (propagated as `CrockfordUUIDError`) are caught and re-raised as `msgspec.ValidationError`. This re-raising is important because `msgspec` has its own validation framework and error reporting mechanisms, which can provide contextual information like the path to the failing field (e.g., `$.id`).1 Consistent error types improve the user experience. + This hook checks if the target type is `CrockfordUUID` and if the input from + the serialized data (`obj`) is a string. It then uses the `CrockfordUUID` + constructor (which internally calls the PyO3-wrapped Rust function) to + perform the decoding. Errors from the Rust layer (propagated as + `CrockfordUUIDError`) are caught and re-raised as `msgspec.ValidationError`. + This re-raising is important because `msgspec` has its own validation + framework and error reporting mechanisms, which can provide contextual + information like the path to the failing field (e.g., `$.id`).1 Consistent + error types improve the user experience. 3. Encoder Hook (enc_hook) Implementation: - The encoder hook converts a CrockfordUUID instance into a string representation suitable for serialization. + The encoder hook converts a CrockfordUUID instance into a string + representation suitable for serialization. - Python - - ``` + ```python # In pycrockford_msgspec/hooks.py def cuuid_encoder(obj: Any) -> str: """msgspec encoder hook for CrockfordUUID.""" @@ -361,15 +507,17 @@ To enable `msgspec` to handle the `CrockfordUUID` type, custom encoder and decod ``` - This hook checks if the object is an instance of `CrockfordUUID` and then simply calls `str(obj)`. The `__str__` method of the `CrockfordUUID` class is designed to return the compact Crockford string by calling the PyO3-wrapped Rust encoding function. + This hook checks if the object is an instance of `CrockfordUUID` and then + simply calls `str(obj)`. The `__str__` method of the `CrockfordUUID` class + is designed to return the compact Crockford string by calling the + PyO3-wrapped Rust encoding function. 4. Usage in msgspec.Struct: - Users will define their msgspec.Struct types using CrockfordUUID and then provide the hooks to the encoder/decoder. - - Python + Users will define their msgspec.Struct types using CrockfordUUID and then + provide the hooks to the encoder/decoder. - ``` + ```python # Example user code import msgspec from pycrockford_msgspec import CrockfordUUID, cuuid_decoder, cuuid_encoder @@ -398,23 +546,45 @@ To enable `msgspec` to handle the `CrockfordUUID` type, custom encoder and decod ``` - The necessity of passing hooks explicitly to `msgspec.Encoder` and `msgspec.Decoder` instances is a characteristic of `msgspec`'s API design.16 To simplify this for users, the `pycrockford_msgspec` package could potentially offer pre-configured encoder/decoder instances or helper functions that return these instances. + The necessity of passing hooks explicitly to `msgspec.Encoder` and + `msgspec.Decoder` instances is a characteristic of `msgspec`'s API design.16 + To simplify this for users, the `pycrockford_msgspec` package could + potentially offer pre-configured encoder/decoder instances or helper + functions that return these instances. ### B. Performance Considerations for Hooks -The Python hooks (`cuuid_decoder`, `cuuid_encoder`) are designed to be very lightweight. The actual parsing, validation, and formatting (the "heavy lifting") are delegated to the compiled Rust code via PyO3. This architecture ensures that the overhead of the Python part of the hook is minimal. Data transfer between Python and Rust (strings and bytes) is generally efficient with PyO3. This approach aims to preserve as much of the Rust performance benefit as possible within the `msgspec` framework. - -Regarding `msgspec`'s `strict` versus `lax` mode for decoding 16: The `cuuid_decoder` hook firmly expects its input `obj` to be a string. If a user employs `msgspec` in `lax` mode (e.g., `strict=False`), `msgspec` itself might attempt to coerce non-string JSON values to strings before they reach any custom `dec_hook`, if the type annotation were simply `str`. However, for a custom type like `CrockfordUUID`, the `dec_hook` is invoked with the raw JSON value associated with the field. The `cuuid_decoder` explicitly checks `isinstance(obj, str)` and raises a `msgspec.ValidationError` if it's not, ensuring that it only attempts to decode valid string inputs. The Crockford decoding specification itself is inherently strict regarding its character set and format.8 +The Python hooks (`cuuid_decoder`, `cuuid_encoder`) are designed to be very +lightweight. The actual parsing, validation, and formatting (the "heavy +lifting") are delegated to the compiled Rust code via PyO3. This architecture +ensures that the overhead of the Python part of the hook is minimal. Data +transfer between Python and Rust (strings and bytes) is generally efficient +with PyO3. This approach aims to preserve as much of the Rust performance +benefit as possible within the `msgspec` framework. + +Regarding `msgspec`'s `strict` versus `lax` mode for decoding 16: The +`cuuid_decoder` hook firmly expects its input `obj` to be a string. If a user +employs `msgspec` in `lax` mode (e.g., `strict=False`), `msgspec` itself might +attempt to coerce non-string JSON values to strings before they reach any custom +`dec_hook`, if the type annotation were simply `str`. However, for a custom +type like `CrockfordUUID`, the `dec_hook` is invoked with the raw JSON value +associated with the field. The `cuuid_decoder` explicitly checks +`isinstance(obj, str)` and raises a `msgspec.ValidationError` if it's not, +ensuring that it only attempts to decode valid string inputs. The Crockford +decoding specification itself is inherently strict regarding its character set +and format.8 ## VI. Python Package Design: `pycrockford_msgspec` -The final product will be a pip-installable Python package named `pycrockford_msgspec`. This package will provide the `CrockfordUUID` type and the necessary hooks for `msgspec` integration. +The final product will be a pip-installable Python package named +`pycrockford_msgspec`. This package will provide the `CrockfordUUID` type and +the necessary hooks for `msgspec` integration. ### A. Proposed Module Structure A clean and intuitive module structure enhances usability: -``` +```text pycrockford_msgspec/ ├── __init__.py # Exports public API (CrockfordUUID, hooks, exception) ├── types.py # Python-side extensions or definitions for CrockfordUUID (if any beyond PyO3 pyclass) @@ -423,42 +593,82 @@ pycrockford_msgspec/ └── _pycrockford_rs_bindings.so # Compiled Rust extension module (or.pyd on Windows) ``` -The compiled Rust extension module (`_pycrockford_rs_bindings.so` or `.pyd`) is typically placed in the package root by `maturin` during the build process. The `__init__.py` file will be responsible for exporting the main components, allowing users to import them directly, e.g., `from pycrockford_msgspec import CrockfordUUID`. +The compiled Rust extension module (`_pycrockford_rs_bindings.so` or `.pyd`) is +typically placed in the package root by `maturin` during the build process. The +`__init__.py` file will be responsible for exporting the main components, +allowing users to import them directly, e.g., +`from pycrockford_msgspec import CrockfordUUID`. ### B. Python API for `CrockfordUUID` -The `CrockfordUUID` Python class, primarily defined in Rust using `#[pyclass]`, will offer a rich and Pythonic API. The goal is to make working with Crockford UUIDs as natural as working with built-in Python types. +The `CrockfordUUID` Python class, primarily defined in Rust using `#[pyclass]`, +will offer a rich and Pythonic API. The goal is to make working with Crockford +UUIDs as natural as working with built-in Python types. 1. **Instantiation:** - `CrockfordUUID(value: str)`: Parses a Crockford-encoded string. - `CrockfordUUID(value: bytes)`: Initializes from 16 raw UUID bytes. - - `CrockfordUUID(value: uuid.UUID)`: Initializes from a standard Python `uuid.UUID` object. - - `CrockfordUUID.generate_v4() -> CrockfordUUID`: Class method to create a new v4 UUID and return as a `CrockfordUUID` instance. This leverages the `uuid` Rust crate's v4 generation.9 - - `CrockfordUUID.generate_v7() -> CrockfordUUID`: Class method to create a new v7 UUID and return as a `CrockfordUUID` instance, using the `uuid` Rust crate's v7 capabilities.11 + - `CrockfordUUID(value: uuid.UUID)`: Initializes from a standard Python + `uuid.UUID` object. + - `CrockfordUUID.generate_v4() -> CrockfordUUID`: Class method to create a + new v4 UUID and return as a `CrockfordUUID` instance. This leverages the + `uuid` Rust crate's v4 generation.9 + - `CrockfordUUID.generate_v7() -> CrockfordUUID`: Class method to create a + new v7 UUID and return as a `CrockfordUUID` instance, using the `uuid` + Rust crate's v7 capabilities.11 2. **Properties and Methods:** - - `cuuid.bytes -> bytes`: Property returning the 16-byte representation of the UUID. - - `cuuid.uuid -> uuid.UUID`: Property returning an equivalent standard Python `uuid.UUID` object. - - `str(cuuid) -> str`: Returns the canonical Crockford string representation (compact, uppercase, no hyphens by default, suitable for `msgspec`). - - `cuuid.to_crockford_string(hyphens: bool = False, checksum: bool = False) -> str`: Method to get the Crockford string with optional hyphenation for display 8 or checksum (if implemented). - - `cuuid.version -> int | None`: Property to get the UUID version (e.g., 4 or 7) if determinable from the bytes. This might require inspecting the UUID bytes according to RFC 4122/9562. + - `cuuid.bytes -> bytes`: Property returning the 16-byte representation of + the UUID. + - `cuuid.uuid -> uuid.UUID`: Property returning an equivalent standard Python + `uuid.UUID` object. + - `str(cuuid) -> str`: Returns the canonical Crockford string representation + (compact, uppercase, no hyphens by default, suitable for `msgspec`). + - `cuuid.to_crockford_string(hyphens: bool = False, checksum: bool = + False) -> str`: + Method to get the Crockford string with optional hyphenation for display + 8 or checksum (if implemented). + - `cuuid.version -> int | None`: Property to get the UUID version (e.g., 4 + or 7) if determinable from the bytes. This might require inspecting the + UUID bytes according to RFC 4122/9562. 3. **Comparison and Hashing:** - - `__eq__(self, other) -> bool`, `__ne__(self, other) -> bool`: Equality comparison based on the underlying 128-bit value. - - `__hash__(self) -> int`: Hash implementation, allowing `CrockfordUUID` instances to be used in sets and as dictionary keys. This is important if used in `msgspec.Struct` with `frozen=True`.16 + - `__eq__(self, other) -> bool`, `__ne__(self, other) -> bool`: Equality + comparison based on the underlying 128-bit value. + - `__hash__(self) -> int`: Hash implementation, allowing `CrockfordUUID` + instances to be used in sets and as dictionary keys. This is important if + used in `msgspec.Struct` with `frozen=True`.16 -This comprehensive API ensures that the `CrockfordUUID` type is not just a thin wrapper but a genuinely useful abstraction. The Rust core handles the performance-critical encoding/decoding, while the Python layer (defined via `#[pyclass]` and potentially augmented in `types.py` if needed) provides the rich, developer-friendly interface. This division of labor plays to the respective strengths of Rust (performance, safety 6) and Python (expressiveness, ease of use 6). +This comprehensive API ensures that the `CrockfordUUID` type is not just a thin +wrapper but a genuinely useful abstraction. The Rust core handles the +performance-critical encoding/decoding, while the Python layer (defined via +`#[pyclass]` and potentially augmented in `types.py` if needed) provides the +rich, developer-friendly interface. This division of labor plays to the +respective strengths of Rust (performance, safety 6) and Python +(expressiveness, ease of use 6). **Table 2: Proposed Python API for** `CrockfordUUID` **Type** - - -

Member Type

Signature/Name

Return Type

Description

Constructor

__init__(value: Union)

CrockfordUUID

Initializes from a Crockford string, 16 raw bytes, or a standard uuid.UUID object.

Class Method

generate_v4()

CrockfordUUID

Creates a new, random (version 4) CrockfordUUID.

Class Method

generate_v7()

CrockfordUUID

Creates a new, time-based (version 7) CrockfordUUID.

Property

bytes

bytes

Returns the underlying 16-byte representation of the UUID.

Property

uuid

uuid.UUID

Returns an equivalent standard Python uuid.UUID object.

Dunder Method

__str__()

str

Returns the canonical, compact Crockford Base32 string (uppercase, no hyphens).

Method

to_crockford_string(hyphens: bool = False, checksum: bool = False)

str

Returns the Crockford string with optional hyphenation and checksum (checksum TBD).

Property

version

`int \

None`

Dunder Method

__eq__(other: Any)

bool

Compares for equality with another CrockfordUUID based on their byte values.

Dunder Method

__ne__(other: Any)

bool

Compares for inequality.

Dunder Method

__hash__()

int

Computes a hash based on the UUID's byte value, for use in hashable collections.

Dunder Method

__repr__()

str

Returns a developer-friendly string representation, e.g., CrockfordUUID('...').

- -This API specification serves as a blueprint for developing a user-friendly and functional `CrockfordUUID` class. +| Member Type | Signature/Name | Return Type | Description | +| ------------- | ------------------------------------------------------------------ | ------------- | ----------------------------------------------------------------------------------- | +| Constructor | **init**(value: Union) | CrockfordUUID | Initializes from a Crockford string, 16 raw bytes, or a standard uuid.UUID object. | +| Class Method | generate_v4() | CrockfordUUID | Creates a new, random (version 4) CrockfordUUID. | +| Class Method | generate_v7() | CrockfordUUID | Creates a new, time-based (version 7) CrockfordUUID. | +| Property | bytes | bytes | Returns the underlying 16-byte representation of the UUID. | +| Property | uuid | uuid.UUID | Returns an equivalent standard Python uuid.UUID object. | +| Dunder Method | **str**() | str | Returns the canonical, compact Crockford Base32 string (uppercase, no hyphens). | +| Method | to_crockford_string(hyphens: bool = False, checksum: bool = False) | str | Returns the Crockford string with optional hyphenation and checksum (checksum TBD). | +| Property | version | `int \ | None` | +| Dunder Method | **eq**(other: Any) | bool | Compares for equality with another CrockfordUUID based on their byte values. | +| Dunder Method | **ne**(other: Any) | bool | Compares for inequality. | +| Dunder Method | **hash**() | int | Computes a hash based on the UUID's byte value, for use in hashable collections. | +| Dunder Method | **repr**() | str | Returns a developer-friendly string representation, e.g., CrockfordUUID('…'). | + +This API specification serves as a blueprint for developing a user-friendly and +functional `CrockfordUUID` class. ### C. Example Usage Scenarios @@ -466,9 +676,7 @@ The documentation will include various examples: - Creating `CrockfordUUID` instances: - Python - - ``` + ```python from pycrockford_msgspec import CrockfordUUID import uuid @@ -492,13 +700,16 @@ The documentation will include various examples: print(c_uuid_v4.uuid) ``` -- Using `CrockfordUUID` in a `msgspec.Struct` and performing serialization/deserialization with `msgspec.json` and `msgspec.msgpack`, demonstrating the use of `cuuid_encoder` and `cuuid_decoder` as shown in Section V.A.4. -The `__init__.py` file will ensure a clean import experience by exporting only the public API components: +- Using `CrockfordUUID` in a `msgspec.Struct` and performing + serialization/deserialization with `msgspec.json` and `msgspec.msgpack`, + demonstrating the use of `cuuid_encoder` and `cuuid_decoder` as shown in + Section V.A.4. -Python +The `__init__.py` file will ensure a clean import experience by exporting only +the public API components: -``` +```python # pycrockford_msgspec/__init__.py from.types import CrockfordUUID from.hooks import cuuid_encoder, cuuid_decoder @@ -508,88 +719,160 @@ from._pycrockford_rs_bindings import __version__ # If version is exposed from Ru __all__ = ``` -This makes the library easy to learn and use, as users typically only need to import from the top-level package. +This makes the library easy to learn and use, as users typically only need to +import from the top-level package. ## VII. Build, Packaging, and Distribution -The project will employ modern Python packaging tools and practices to ensure it is easy to build, distribute, and install. `maturin` will be used for building the Rust-based Python extension, and `uv` will be used for environment management and as a package installer/resolver. +The project will employ modern Python packaging tools and practices to ensure +it is easy to build, distribute, and install. `maturin` will be used for +building the Rust-based Python extension, and `uv` will be used for environment +management and as a package installer/resolver. ### A. Project Setup with `maturin` -`Maturin` is a tool for building and publishing Rust-based Python packages, integrating seamlessly with PyO3.2 +`Maturin` is a tool for building and publishing Rust-based Python packages, +integrating seamlessly with PyO3.2 -1. **Project Initialization:** The project can be started using `maturin new --bindings pyo3 pycrockford_msgspec` 2 or by manually creating the necessary configuration files. +1. **Project Initialization:** The project can be started using + `maturin new --bindings pyo3 pycrockford_msgspec` 2 or by manually creating + the necessary configuration files. 2. `Cargo.toml` **Configuration:** - - The `[lib]` section must specify `crate-type = ["cdylib"]` to produce a dynamic shared library that Python can import.18 - - `pyo3` will be listed as a dependency in `[dependencies.pyo3]`. Crucially, this will include features like `extension-module` (to tell PyO3 it's building an extension module, not linking `libpython` 18) and `abi3-py3X` (e.g., `abi3-py38`).18 The `abi3` feature enables the creation of "stable ABI" wheels, which are compatible with multiple Python versions (e.g., Python 3.8 and newer) without needing recompilation for each minor Python release. This significantly simplifies distribution and reduces the build matrix for PyPI. - - Dependencies on the `uuid` crate 9 (for UUID generation) will also be included. -3. `pyproject.toml` **Configuration:** This file is standard for Python packaging and defines build system requirements (PEP 517).18 - - `[build-system]`: Specifies `requires = ["maturin>=1.X,<2.Y"]` and `build-backend = "maturin"`. - - `[project]`: Contains standard Python package metadata such as `name = "pycrockford-msgspec"`, `version`, `description`, `authors`, `license`, `classifiers`, `requires-python`, and dependencies (like `msgspec`). + - The `[lib]` section must specify `crate-type = ["cdylib"]` to produce a + dynamic shared library that Python can import.18 + - `pyo3` will be listed as a dependency in `[dependencies.pyo3]`. Crucially, + this will include features like `extension-module` (to tell PyO3 it's + building an extension module, not linking `libpython` 18) and `abi3-py3X` + (e.g., `abi3-py38`).18 The `abi3` feature enables the creation of "stable + ABI" wheels, which are compatible with multiple Python versions (e.g., + Python 3.8 and newer) without needing recompilation for each minor Python + release. This significantly simplifies distribution and reduces the build + matrix for PyPI. + - Dependencies on the `uuid` crate 9 (for UUID generation) will also be + included. +3. `pyproject.toml` **Configuration:** This file is standard for Python + packaging and defines build system requirements (PEP 517).18 + - `[build-system]`: Specifies `requires = ["maturin>=1.X,<2.Y"]` and + `build-backend = "maturin"`. + - `[project]`: Contains standard Python package metadata such as + `name = "pycrockford-msgspec"`, `version`, `description`, `authors`, + `license`, `classifiers`, `requires-python`, and dependencies (like + `msgspec`). ### B. Using `uv` for Development and Packaging -`uv` is a very fast Python package installer and resolver, written in Rust, designed as a drop-in replacement for `pip` and `pip-tools` workflows.4 +`uv` is a very fast Python package installer and resolver, written in Rust, +designed as a drop-in replacement for `pip` and `pip-tools` workflows.4 -1. **Environment Management:** `uv venv` will be used to create and manage isolated virtual environments for development and testing. +1. **Environment Management:** `uv venv` will be used to create and manage + isolated virtual environments for development and testing. 2. **Dependency Management:** - - Development dependencies (e.g., `pytest`, `pytest-benchmark`, `ruff`) will be managed using `uv pip install `. - - For reproducible environments, `uv pip compile requirements.in -o requirements.txt` can lock dependencies, and `uv pip sync requirements.txt` can install them.5 -3. **Building Wheels:** `maturin build --release` will be used to compile the Rust extension and produce optimized Python wheels (`.whl` files).21 These wheels will be placed in the `target/wheels/` directory. While `uv` is primarily an installer, it respects the `build-backend` specified in `pyproject.toml`, so if installing from source, `uv` would invoke `maturin`. -4. **Pip-Installable Package:** The wheels generated by `maturin` are standard and can be installed using `uv pip install .whl` or `pip install .whl`. - -The combination of `maturin` for the Rust-to-Python build process and `uv` for overall Python environment and package management provides a modern, fast, and efficient toolchain, aligning with the user's request. + - Development dependencies (e.g., `pytest`, `pytest-benchmark`, `ruff`) will + be managed using `uv pip install `. + - For reproducible environments, + `uv pip compile requirements.in -o requirements.txt` can lock + dependencies, and `uv pip sync requirements.txt` can install them.5 +3. **Building Wheels:** `maturin build --release` will be used to compile the + Rust extension and produce optimized Python wheels (`.whl` files).21 These + wheels will be placed in the `target/wheels/` directory. While `uv` is + primarily an installer, it respects the `build-backend` specified in + `pyproject.toml`, so if installing from source, `uv` would invoke `maturin`. +4. **Pip-Installable Package:** The wheels generated by `maturin` are standard + and can be installed using `uv pip install .whl` or + `pip install .whl`. + +The combination of `maturin` for the Rust-to-Python build process and `uv` for +overall Python environment and package management provides a modern, fast, and +efficient toolchain, aligning with the user's request. ### C. Distribution -1. **Uploading to PyPI:** Once wheels are built for various target platforms (Linux, macOS, Windows) and Python versions (covered by `abi3`), they can be uploaded to the Python Package Index (PyPI). This can be done using tools like `twine upload target/wheels/*` or `maturin publish`.18 For Linux, `manylinux` standards should be adhered to, often by building in a `manylinux` Docker container or using tools like `zig` with `maturin` to ensure broad compatibility.21 GitHub Actions, potentially using `maturin-action` 21, are ideal for automating these cross-platform builds and uploads. -2. **Source Distribution (**`sdist`**):** `maturin sdist` can generate a source distribution. This allows users to build the package from source if a pre-compiled wheel is not available for their specific platform or Python version, provided they have a Rust toolchain installed. - -The emphasis on `abi3` wheels is critical for a library intended for general distribution on PyPI, as it greatly simplifies maintenance and improves the user experience by providing widely compatible binary wheels. +1. **Uploading to PyPI:** Once wheels are built for various target platforms + (Linux, macOS, Windows) and Python versions (covered by `abi3`), they can be + uploaded to the Python Package Index (PyPI). This can be done using tools + like `twine upload target/wheels/*` or `maturin publish`.18 For Linux, + `manylinux` standards should be adhered to, often by building in a + `manylinux` Docker container or using tools like `zig` with `maturin` to + ensure broad compatibility.21 GitHub Actions, potentially using + `maturin-action` 21, are ideal for automating these cross-platform builds + and uploads. +2. **Source Distribution (**`sdist`**):** `maturin sdist` can generate a source + distribution. This allows users to build the package from source if a + pre-compiled wheel is not available for their specific platform or Python + version, provided they have a Rust toolchain installed. + +The emphasis on `abi3` wheels is critical for a library intended for general +distribution on PyPI, as it greatly simplifies maintenance and improves the +user experience by providing widely compatible binary wheels. ## VIII. Testing and Validation Strategy -A comprehensive testing strategy is essential to ensure the correctness, reliability, and performance of the `pycrockford_msgspec` package. `pytest` will be the primary framework for Python-level tests, as requested. +A comprehensive testing strategy is essential to ensure the correctness, +reliability, and performance of the `pycrockford_msgspec` package. `pytest` +will be the primary framework for Python-level tests, as requested. ### A. Test Framework: `pytest` -`pytest` will be used for its powerful features, plugin ecosystem, and ease of use for organizing and running Python tests. Tests will be located in a dedicated `tests/` directory. +`pytest` will be used for its powerful features, plugin ecosystem, and ease of +use for organizing and running Python tests. Tests will be located in a +dedicated `tests/` directory. ### B. Unit Tests (Rust) -The core Rust logic for Crockford encoding and decoding will be thoroughly unit-tested using Rust's built-in testing framework (`#[test]` functions within the Rust modules, run via `cargo test`). +The core Rust logic for Crockford encoding and decoding will be thoroughly +unit-tested using Rust's built-in testing framework (`#[test]` functions within +the Rust modules, run via `cargo test`). - **Coverage:** - - Valid inputs: A variety of known UUIDs and their corresponding Crockford strings. Test cases should include strings with and without hyphens. - - Invalid inputs: Strings with characters outside the Crockford set, strings of incorrect length after decoding, strings that are malformed. - - Edge cases: UUIDs representing all zeros, all ones (0xFF... bytes), and other boundary conditions. - - Character mapping: Specific tests for 'I', 'i', 'L', 'l' decoding to '1', and 'O', 'o' decoding to '0'.8 + - Valid inputs: A variety of known UUIDs and their corresponding Crockford + strings. Test cases should include strings with and without hyphens. + - Invalid inputs: Strings with characters outside the Crockford set, strings + of incorrect length after decoding, strings that are malformed. + - Edge cases: UUIDs representing all zeros, all ones (0xFF… bytes), and + other boundary conditions. + - Character mapping: Specific tests for 'I', 'i', 'L', 'l' decoding to '1', + and 'O', 'o' decoding to '0'.8 - Checksum validation (if this feature is implemented). ### C. Integration Tests (Python/PyO3) -These tests, written in Python using `pytest`, will validate the PyO3 bridge and the Python-facing API of the compiled Rust extension. +These tests, written in Python using `pytest`, will validate the PyO3 bridge +and the Python-facing API of the compiled Rust extension. - **PyO3 Bindings:** - - Verify that Python can call the exposed Rust functions (`decode_crockford_py`, `encode_crockford_py`) correctly. + - Verify that Python can call the exposed Rust functions + (`decode_crockford_py`, `encode_crockford_py`) correctly. - Test with a range of valid and invalid inputs passed from Python to Rust. - - Ensure correct data type conversions between Python and Rust (e.g., Python `str` to Rust `&str`, Rust `[u8; 16]` to Python `bytes`). - - Verify that Rust errors (e.g., `CrockfordError`) are correctly translated into the custom Python exception (`CrockfordUUIDError`) and can be caught appropriately in Python. + - Ensure correct data type conversions between Python and Rust (e.g., Python + `str` to Rust `&str`, Rust `[u8; 16]` to Python `bytes`). + - Verify that Rust errors (e.g., `CrockfordError`) are correctly translated + into the custom Python exception (`CrockfordUUIDError`) and can be caught + appropriately in Python. - `CrockfordUUID` **Python Class:** - Test all constructors: from string, from bytes, from `uuid.UUID`. - Test properties: `.bytes`, `.uuid`. - - Test methods: `str()`, `repr()`, `to_crockford_string()` (with and without options), `generate_v4()`, `generate_v7()`. + - Test methods: `str()`, `repr()`, `to_crockford_string()` (with and without + options), `generate_v4()`, `generate_v7()`. - Verify comparison methods (`__eq__`, `__ne__`) and hashing (`__hash__`). ### D. `msgspec` Integration Tests (Python) -These `pytest` tests will ensure that `CrockfordUUID` integrates seamlessly with `msgspec` for serialization and deserialization. - -- Define various `msgspec.Struct` classes that include fields of type `CrockfordUUID`. -- Test serialization to JSON and MessagePack using `msgspec.json.Encoder` and `msgspec.msgpack.Encoder`, ensuring the `cuuid_encoder` hook is correctly invoked and produces the expected string output. -- Test deserialization from JSON and MessagePack using `msgspec.json.Decoder` and `msgspec.msgpack.Decoder`, ensuring the `cuuid_decoder` hook is correctly invoked and reconstructs the `CrockfordUUID` object. -- Verify that `msgspec.ValidationError` is raised (and contains appropriate error messages) when attempting to decode invalid Crockford strings through `msgspec`. -- Test scenarios with multiple `CrockfordUUID` fields, optional fields, and fields within nested structures. +These `pytest` tests will ensure that `CrockfordUUID` integrates seamlessly with +`msgspec` for serialization and deserialization. + +- Define various `msgspec.Struct` classes that include fields of type + `CrockfordUUID`. +- Test serialization to JSON and MessagePack using `msgspec.json.Encoder` and + `msgspec.msgpack.Encoder`, ensuring the `cuuid_encoder` hook is correctly + invoked and produces the expected string output. +- Test deserialization from JSON and MessagePack using `msgspec.json.Decoder` + and `msgspec.msgpack.Decoder`, ensuring the `cuuid_decoder` hook is correctly + invoked and reconstructs the `CrockfordUUID` object. +- Verify that `msgspec.ValidationError` is raised (and contains appropriate + error messages) when attempting to decode invalid Crockford strings through + `msgspec`. +- Test scenarios with multiple `CrockfordUUID` fields, optional fields, and + fields within nested structures. ### E. Performance Benchmarking (Optional but Recommended) @@ -597,38 +880,71 @@ To quantify the "efficiency" goal, performance benchmarks should be conducted. - The `pytest-benchmark` plugin can be used for this. - Key operations to benchmark: - - Decoding a Crockford string to a `CrockfordUUID` object via `msgspec.json.decode` (which will use the Rust-backed `cuuid_decoder`). - - Encoding a `CrockfordUUID` object to a JSON string via `msgspec.json.encode` (using the Rust-backed `cuuid_encoder`). -- If feasible, these benchmarks could be compared against a pure Python implementation of Crockford decoding/encoding (e.g., using a library like `base32-crockford` 15 or `base32-lib` 17 adapted into `msgspec` hooks) to concretely demonstrate the performance gains from the Rust extension. Rust generally offers significant speed advantages over Python for CPU-bound tasks like string parsing and manipulation.6 - -This multi-layered testing approach—Rust unit tests, PyO3 binding tests, Python class tests, and `msgspec` integration tests—is crucial. Each layer addresses different potential points of failure, ensuring robustness from the core Rust logic up to the final user interaction with `msgspec`. The Crockford specification itself, with its specific rules for character mapping and hyphen ignorance 8, necessitates thorough test coverage of these edge cases to ensure full compliance and correctness, not just speed. + - Decoding a Crockford string to a `CrockfordUUID` object via + `msgspec.json.decode` (which will use the Rust-backed `cuuid_decoder`). + - Encoding a `CrockfordUUID` object to a JSON string via + `msgspec.json.encode` (using the Rust-backed `cuuid_encoder`). +- If feasible, these benchmarks could be compared against a pure Python + implementation of Crockford decoding/encoding (e.g., using a library like + `base32-crockford` 15 or `base32-lib` 17 adapted into `msgspec` hooks) to + concretely demonstrate the performance gains from the Rust extension. Rust + generally offers significant speed advantages over Python for CPU-bound tasks + like string parsing and manipulation.6 + +This multi-layered testing approach—Rust unit tests, PyO3 binding tests, Python +class tests, and `msgspec` integration tests—is crucial. Each layer addresses +different potential points of failure, ensuring robustness from the core Rust +logic up to the final user interaction with `msgspec`. The Crockford +specification itself, with its specific rules for character mapping and hyphen +ignorance 8, necessitates thorough test coverage of these edge cases to ensure +full compliance and correctness, not just speed. ## IX. Documentation Plan -Clear, comprehensive documentation is vital for the adoption and maintainability of the `pycrockford_msgspec` package. The documentation should cater to both end-users of the library and potential contributors. +Clear, comprehensive documentation is vital for the adoption and +maintainability of the `pycrockford_msgspec` package. The documentation should +cater to both end-users of the library and potential contributors. ### A. API Documentation 1. **Python API:** - Generated using Sphinx, a standard tool for Python project documentation. - - Extensions like `sphinx.ext.autodoc` will be used to pull documentation from docstrings within the Python code. - - `sphinx.ext.napoleon` will enable support for Google or NumPy style docstrings, promoting readability. - - The documentation will cover the `pycrockford_msgspec` module, detailing the `CrockfordUUID` class (constructors, methods, properties), the `cuuid_encoder` and `cuuid_decoder` hook functions, and the `CrockfordUUIDError` exception. + - Extensions like `sphinx.ext.autodoc` will be used to pull documentation + from docstrings within the Python code. + - `sphinx.ext.napoleon` will enable support for Google or NumPy style + docstrings, promoting readability. + - The documentation will cover the `pycrockford_msgspec` module, detailing + the `CrockfordUUID` class (constructors, methods, properties), the + `cuuid_encoder` and `cuuid_decoder` hook functions, and the + `CrockfordUUIDError` exception. 2. **Rust API (Internal):** - - `cargo doc --no-deps` can generate HTML documentation for the internal Rust crate (`pycrockford_rs`). This is primarily for developers working on the Rust core or the PyO3 bindings. + - `cargo doc --no-deps` can generate HTML documentation for the internal + Rust crate (`pycrockford_rs`). This is primarily for developers working on + the Rust core or the PyO3 bindings. ### B. Usage Guide -This will be the primary resource for users learning how to use the library. It can be part of the Sphinx documentation and/or included in the project's `README.md`. - -- **Installation:** Clear instructions on how to install the package using `uv pip install pycrockford_msgspec` or `pip install pycrockford_msgspec`. -- **Quick Start:** A concise, compelling example demonstrating the definition of a `msgspec.Struct` with a `CrockfordUUID` field, followed by serialization and deserialization using `msgspec.json`. This should highlight the ease of use. -- `CrockfordUUID` **Class:** Detailed explanation of how to instantiate `CrockfordUUID` objects (from strings, bytes, `uuid.UUID`), access its properties (`.bytes`, `.uuid`), and use its methods (`.generate_v4()`, `.to_crockford_string()`). -- `msgspec` **Integration:** Explicit instructions and examples on how to correctly use the `cuuid_encoder` and `cuuid_decoder` hooks with `msgspec.json.Encoder`, `msgspec.json.Decoder`, `msgspec.msgpack.Encoder`, and `msgspec.msgpack.Decoder`.16 This section is critical, as proper hook usage is key to integrating custom types with `msgspec`. - - Python - - ``` +This will be the primary resource for users learning how to use the library. It +can be part of the Sphinx documentation and/or included in the project's +`README.md`. + +- **Installation:** Clear instructions on how to install the package using + `uv pip install pycrockford_msgspec` or `pip install pycrockford_msgspec`. +- **Quick Start:** A concise, compelling example demonstrating the definition + of a `msgspec.Struct` with a `CrockfordUUID` field, followed by serialization + and deserialization using `msgspec.json`. This should highlight the ease of + use. +- `CrockfordUUID` **Class:** Detailed explanation of how to instantiate + `CrockfordUUID` objects (from strings, bytes, `uuid.UUID`), access its + properties (`.bytes`, `.uuid`), and use its methods (`.generate_v4()`, + `.to_crockford_string()`). +- `msgspec` **Integration:** Explicit instructions and examples on how to + correctly use the `cuuid_encoder` and `cuuid_decoder` hooks with + `msgspec.json.Encoder`, `msgspec.json.Decoder`, `msgspec.msgpack.Encoder`, and + `msgspec.msgpack.Decoder`.16 This section is critical, as proper hook usage + is key to integrating custom types with `msgspec`. + + ```python # Example for documentation: import msgspec from pycrockford_msgspec import CrockfordUUID, cuuid_encoder, cuuid_decoder @@ -645,61 +961,103 @@ This will be the primary resource for users learning how to use the library. It #... rest of the example... ``` -- **Error Handling:** How to catch and interpret `CrockfordUUIDError` and `msgspec.ValidationError` when working with the library. + +- **Error Handling:** How to catch and interpret `CrockfordUUIDError` and + `msgspec.ValidationError` when working with the library. ### C. Contribution Guidelines (`CONTRIBUTING.md`) For those wishing to contribute to the project: -- Instructions for setting up the development environment: Rust toolchain, Python (specific versions), `uv`, and `maturin`. +- Instructions for setting up the development environment: Rust toolchain, + Python (specific versions), `uv`, and `maturin`. - How to build the package locally (e.g., `maturin develop`). -- How to run the different test suites (`cargo test` for Rust tests, `pytest` for Python tests). -- Information on code style guidelines (e.g., using `ruff` for Python linting/formatting, `rustfmt` for Rust). +- How to run the different test suites (`cargo test` for Rust tests, `pytest` + for Python tests). +- Information on code style guidelines (e.g., using `ruff` for Python + linting/formatting, `rustfmt` for Rust). - Process for submitting pull requests. - How to build the documentation locally. ### D. Changelog (`CHANGELOG.md`) -A log detailing changes, new features, bug fixes, and any breaking changes for each released version of the package. This helps users understand the evolution of the library and upgrade with confidence. +A log detailing changes, new features, bug fixes, and any breaking changes for +each released version of the package. This helps users understand the evolution +of the library and upgrade with confidence. -Providing clear examples, especially for the `msgspec` hook integration 16, is paramount. This is often the part where users might face difficulties if not well-documented. The documentation strategy aims to make the library accessible and easy to use for its target audience while also facilitating community contributions, which is beneficial for the long-term health of an open-source project.19 +Providing clear examples, especially for the `msgspec` hook integration 16, is +paramount. This is often the part where users might face difficulties if not +well-documented. The documentation strategy aims to make the library accessible +and easy to use for its target audience while also facilitating community +contributions, which is beneficial for the long-term health of an open-source +project.19 ## X. Conclusion and Roadmap ### A. Summary of Proposal -This report has detailed a comprehensive proposal for creating `pycrockford_msgspec`, a Python package enabling the efficient and ergonomic use of Crockford Base32 encoded UUIDs with the `msgspec` library. The core design leverages Rust for high-performance encoding/decoding operations, exposed to Python via PyO3 bindings. A Python-native `CrockfordUUID` class will offer a rich API, integrating smoothly with `msgspec` through custom encoder and decoder hooks. The project emphasizes modern tooling with `uv` and `maturin`, robust testing with `pytest`, and thorough documentation. +This report has detailed a comprehensive proposal for creating +`pycrockford_msgspec`, a Python package enabling the efficient and ergonomic +use of Crockford Base32 encoded UUIDs with the `msgspec` library. The core +design leverages Rust for high-performance encoding/decoding operations, +exposed to Python via PyO3 bindings. A Python-native `CrockfordUUID` class will +offer a rich API, integrating smoothly with `msgspec` through custom encoder +and decoder hooks. The project emphasizes modern tooling with `uv` and +`maturin`, robust testing with `pytest`, and thorough documentation. ### B. Key Deliverables The successful execution of this proposal will result in: 1. A pip-installable Python package named `pycrockford_msgspec`. -2. A `CrockfordUUID` Python type with a clean, intuitive API, supporting instantiation from strings, bytes, and standard `uuid.UUID` objects, as well as generation of v4 and v7 UUIDs. -3. Rust-accelerated performance for Crockford UUID encoding and decoding operations. -4. Seamless integration with `msgspec` for JSON and MessagePack serialization/deserialization via `enc_hook` and `dec_hook`. +2. A `CrockfordUUID` Python type with a clean, intuitive API, supporting + instantiation from strings, bytes, and standard `uuid.UUID` objects, as well + as generation of v4 and v7 UUIDs. +3. Rust-accelerated performance for Crockford UUID encoding and decoding + operations. +4. Seamless integration with `msgspec` for JSON and MessagePack + serialization/deserialization via `enc_hook` and `dec_hook`. 5. A comprehensive test suite ensuring correctness and reliability. 6. Clear user and contributor documentation. ### C. Potential Future Enhancements -While the initial version will focus on core functionality, several enhancements could be considered for future releases, guided by user feedback and evolving needs: - -1. **Crockford Checksum Support:** Full implementation of Crockford's optional checksum generation and validation 8, potentially configurable via `CrockfordUUID.to_crockford_string(checksum=True)` and during parsing. -2. **Configurable Hyphenation in** `msgspec` **Output:** While the default for `msgspec` will be compact output, an option might be explored to allow hyphenated output if a specific `msgspec` use case benefits from it (though this is less common for machine-to-machine formats). -3. **Advanced Performance Optimizations:** If profiling reveals specific bottlenecks in the Rust or PyO3 layers, further micro-optimizations could be investigated. -4. **Support for Other UUID Versions:** While UUIDv4 and v7 are prioritized 11, explicit generation support for other UUID versions (e.g., v1, v6) could be added to the `CrockfordUUID` class if there is demand. -5. **Convenience** `msgspec` **Encoders/Decoders:** Provide pre-configured `msgspec.Encoder` and `msgspec.Decoder` instances that already have the Crockford UUID hooks set up, simplifying usage for common cases: - - Python - - ``` +While the initial version will focus on core functionality, several +enhancements could be considered for future releases, guided by user feedback +and evolving needs: + +1. **Crockford Checksum Support:** Full implementation of Crockford's optional + checksum generation and validation 8, potentially configurable via + `CrockfordUUID.to_crockford_string(checksum=True)` and during parsing. +2. **Configurable Hyphenation in** `msgspec` **Output:** While the default for + `msgspec` will be compact output, an option might be explored to allow + hyphenated output if a specific `msgspec` use case benefits from it (though + this is less common for machine-to-machine formats). +3. **Advanced Performance Optimizations:** If profiling reveals specific + bottlenecks in the Rust or PyO3 layers, further micro-optimizations could be + investigated. +4. **Support for Other UUID Versions:** While UUIDv4 and v7 are prioritized 11, + explicit generation support for other UUID versions (e.g., v1, v6) could be + added to the `CrockfordUUID` class if there is demand. +5. **Convenience** `msgspec` **Encoders/Decoders:** Provide pre-configured + `msgspec.Encoder` and `msgspec.Decoder` instances that already have the + Crockford UUID hooks set up, simplifying usage for common cases: + + ```python # Hypothetical future convenience # from pycrockford_msgspec import CrockfordJSONEncoder, CrockfordJSONDecoder # encoder = CrockfordJSONEncoder() # decoder = CrockfordJSONDecoder(type=MyMessage) ``` -6. **Broader Protocol Support:** Evaluate and potentially add explicit examples or tests for using `CrockfordUUID` with other `msgspec`-supported protocols like YAML or TOML, if applicable (noting TOML's lack of null might be irrelevant here, but general protocol compatibility is good). -The development approach should be iterative, focusing on delivering a robust and performant core in the initial release. Subsequent enhancements can be prioritized based on their value to the user community and the overall goals of the library. Community feedback gathered through platforms like GitHub issues will be invaluable in shaping the future roadmap. +6. **Broader Protocol Support:** Evaluate and potentially add explicit examples + or tests for using `CrockfordUUID` with other `msgspec`-supported protocols + like YAML or TOML, if applicable (noting TOML's lack of null might be + irrelevant here, but general protocol compatibility is good). + +The development approach should be iterative, focusing on delivering a robust +and performant core in the initial release. Subsequent enhancements can be +prioritized based on their value to the user community and the overall goals of +the library. Community feedback gathered through platforms like GitHub issues +will be invaluable in shaping the future roadmap. diff --git a/msgspec_crockford/__init__.py b/msgspec_crockford/__init__.py index fc7f287..0f9db29 100644 --- a/msgspec_crockford/__init__.py +++ b/msgspec_crockford/__init__.py @@ -1,4 +1,6 @@ # pyright: reportMissingImports=false, reportAttributeAccessIssue=false +"""Crockford Base32 UUID support for the msgspec serialization library.""" + from __future__ import annotations from _pycrockford_rs_bindings import decode_crockford, encode_crockford @@ -9,9 +11,9 @@ __all__ = [ "CrockfordUUID", - "decode_crockford", - "encode_crockford", "CrockfordUUIDError", "cuuid_decoder", "cuuid_encoder", + "decode_crockford", + "encode_crockford", ] diff --git a/msgspec_crockford/exceptions.py b/msgspec_crockford/exceptions.py index 4b8497a..e3924f3 100644 --- a/msgspec_crockford/exceptions.py +++ b/msgspec_crockford/exceptions.py @@ -1,2 +1,17 @@ +"""Exception types raised by the Crockford UUID integration.""" + +from __future__ import annotations + + class CrockfordUUIDError(ValueError): """Error raised for invalid Crockford UUID operations.""" + + @classmethod + def invalid_length(cls, expected: int, actual: int) -> CrockfordUUIDError: + """Build an error for a byte payload of the wrong length.""" + return cls(f"expected {expected} bytes, got {actual}") + + @classmethod + def unsupported_value(cls) -> CrockfordUUIDError: + """Build an error for a value of an unsupported type.""" + return cls("expected Crockford string, 16 bytes, or uuid.UUID") diff --git a/msgspec_crockford/hooks.py b/msgspec_crockford/hooks.py index 184f3d4..0b265bd 100644 --- a/msgspec_crockford/hooks.py +++ b/msgspec_crockford/hooks.py @@ -1,21 +1,30 @@ # pyright: reportMissingImports=false, reportAttributeAccessIssue=false +"""msgspec encode and decode hooks for `CrockfordUUID` values.""" + from __future__ import annotations -from typing import Any, get_args, get_origin, Union +import typing as typ from types import NotImplementedType, UnionType import msgspec -from .types import CrockfordUUID from .exceptions import CrockfordUUIDError +from .types import CrockfordUUID + + +def _invalid_payload_error(obj: object) -> msgspec.ValidationError: + """Build a validation error for a non-string CrockfordUUID payload.""" + return msgspec.ValidationError( + f"Expected str for CrockfordUUID, got {type(obj).__name__}" + ) -def cuuid_decoder(type_hint: Any, obj: Any) -> CrockfordUUID | NotImplementedType: +def cuuid_decoder(type_hint: object, obj: object) -> CrockfordUUID | NotImplementedType: """Decode CrockfordUUID strings for msgspec.""" - origin = get_origin(type_hint) - args = get_args(type_hint) + origin = typ.get_origin(type_hint) + args = typ.get_args(type_hint) is_crockford_type = type_hint is CrockfordUUID or ( - origin in (Union, UnionType) and CrockfordUUID in args + origin in {typ.Union, UnionType} and CrockfordUUID in args ) if is_crockford_type: @@ -26,12 +35,10 @@ def cuuid_decoder(type_hint: Any, obj: Any) -> CrockfordUUID | NotImplementedTyp return CrockfordUUID(obj) except CrockfordUUIDError as exc: raise msgspec.ValidationError(str(exc)) from exc - raise msgspec.ValidationError( - f"Expected str for CrockfordUUID, got {type(obj).__name__}" - ) + raise _invalid_payload_error(obj) return NotImplemented -def cuuid_encoder(obj: Any) -> str | NotImplementedType: +def cuuid_encoder(obj: object) -> str | NotImplementedType: """Encode CrockfordUUID instances for msgspec.""" return str(obj) if isinstance(obj, CrockfordUUID) else NotImplemented diff --git a/msgspec_crockford/types.py b/msgspec_crockford/types.py index 1a8568d..891c94c 100644 --- a/msgspec_crockford/types.py +++ b/msgspec_crockford/types.py @@ -1,25 +1,29 @@ # pyright: reportMissingImports=false, reportAttributeAccessIssue=false, reportArgumentType=false +"""Crockford Base32 UUID type backed by the Rust bindings.""" + from __future__ import annotations -from typing import ClassVar -import uuid +import typing as typ +import uuid as py_uuid -from .exceptions import CrockfordUUIDError from _pycrockford_rs_bindings import decode_crockford, encode_crockford +from .exceptions import CrockfordUUIDError + -class CrockfordUUID(uuid.UUID): +class CrockfordUUID(py_uuid.UUID): """UUID stored as Crockford Base32 when stringified.""" - _expected_length: ClassVar[int] = 16 + _expected_length: typ.ClassVar[int] = 16 - def __new__(cls, value: str | bytes | uuid.UUID) -> "CrockfordUUID": - if isinstance(value, uuid.UUID): + def __new__(cls, value: str | bytes | py_uuid.UUID) -> CrockfordUUID: + """Create an instance from a Crockford string, bytes, or UUID.""" + if isinstance(value, py_uuid.UUID): bytes_value = value.bytes elif isinstance(value, (bytes, bytearray)): if len(value) != cls._expected_length: - raise CrockfordUUIDError( - f"expected {cls._expected_length} bytes, got {len(value)}" + raise CrockfordUUIDError.invalid_length( + cls._expected_length, len(value) ) bytes_value = bytes(value) elif isinstance(value, str): @@ -28,33 +32,35 @@ def __new__(cls, value: str | bytes | uuid.UUID) -> "CrockfordUUID": except ValueError as exc: raise CrockfordUUIDError(str(exc)) from exc else: - raise CrockfordUUIDError( - "expected Crockford string, 16 bytes, or uuid.UUID" - ) + raise CrockfordUUIDError.unsupported_value() obj = object.__new__(cls) - uuid.UUID.__init__(obj, bytes=bytes_value) + py_uuid.UUID.__init__(obj, bytes=bytes_value) return obj - def __init__(self, value: str | bytes | uuid.UUID) -> None: - pass + def __init__(self, value: str | bytes | py_uuid.UUID) -> None: + """Accept the constructor value; state is populated in `__new__`.""" @classmethod - def generate_v4(cls) -> "CrockfordUUID": - return cls(uuid.uuid4()) + def generate_v4(cls) -> CrockfordUUID: + """Generate a random (version 4) Crockford UUID.""" + return cls(py_uuid.uuid4()) @classmethod - def generate_v7(cls) -> "CrockfordUUID": - if not hasattr(uuid, "uuid7"): + def generate_v7(cls) -> CrockfordUUID: + """Generate a time-ordered (version 7) Crockford UUID.""" + if not hasattr(py_uuid, "uuid7"): raise NotImplementedError("uuid.uuid7 is not available") - return cls(uuid.uuid7()) # pyright: ignore[reportAttributeAccessIssue] + return cls(py_uuid.uuid7()) # pyright: ignore[reportAttributeAccessIssue] @property - def uuid(self) -> uuid.UUID: - """Return a `uuid.UUID` instance representing this CrockfordUUID.""" - return uuid.UUID(bytes=self.bytes) + def uuid(self) -> py_uuid.UUID: + """A plain `uuid.UUID` view of this identifier.""" + return py_uuid.UUID(bytes=self.bytes) def __str__(self) -> str: + """Render the identifier as Crockford Base32.""" return encode_crockford(self.bytes) def __repr__(self) -> str: + """Render a constructor-style representation.""" return f"CrockfordUUID('{self}')" diff --git a/msgspec_crockford/unittests/test_hooks.py b/msgspec_crockford/unittests/test_hooks.py index 8bdefe6..8e00233 100644 --- a/msgspec_crockford/unittests/test_hooks.py +++ b/msgspec_crockford/unittests/test_hooks.py @@ -1,18 +1,16 @@ # pyright: reportMissingImports=false, reportAttributeAccessIssue=false, reportGeneralTypeIssues=false, reportArgumentType=false +"""Unit tests for the msgspec encode and decode hooks.""" + from __future__ import annotations import msgspec import pytest -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) - from msgspec_crockford import CrockfordUUID, cuuid_decoder, cuuid_encoder def test_encode_decode_round_trip() -> None: + """Encoding then decoding returns an equal identifier.""" original = CrockfordUUID.generate_v4() encoded = cuuid_encoder(original) decoded = cuuid_decoder(CrockfordUUID, encoded) @@ -20,21 +18,25 @@ def test_encode_decode_round_trip() -> None: def test_invalid_decode_raises() -> None: + """A malformed Crockford string raises a validation error.""" with pytest.raises(msgspec.ValidationError): cuuid_decoder(CrockfordUUID, "not-a-cuuid") def test_invalid_obj_type_raises() -> None: + """A non-string payload raises a validation error.""" with pytest.raises(msgspec.ValidationError): cuuid_decoder(CrockfordUUID, 123) def test_hooks_return_not_implemented() -> None: + """The hooks defer to msgspec for unrelated types.""" assert cuuid_decoder(str, "abc") is NotImplemented assert cuuid_encoder("abc") is NotImplemented def test_decoder_optional_handling() -> None: + """Optional hints defer `None` and decode string payloads.""" hint = CrockfordUUID | None uuid_obj = CrockfordUUID.generate_v4() assert cuuid_decoder(hint, None) is NotImplemented diff --git a/msgspec_crockford/unittests/test_integration.py b/msgspec_crockford/unittests/test_integration.py index 2e3938b..6ebe45f 100644 --- a/msgspec_crockford/unittests/test_integration.py +++ b/msgspec_crockford/unittests/test_integration.py @@ -1,23 +1,23 @@ # pyright: reportMissingImports=false, reportAttributeAccessIssue=false +"""Integration tests exercising msgspec encode and decode round trips.""" + from __future__ import annotations import msgspec import pytest -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) - from msgspec_crockford import CrockfordUUID, cuuid_decoder class Record(msgspec.Struct): + """Sample struct with a Crockford UUID field.""" + id: CrockfordUUID name: str def test_msgspec_roundtrip() -> None: + """A struct with a Crockford UUID field survives a JSON round trip.""" record = Record(CrockfordUUID.generate_v4(), "example") encoder = msgspec.json.Encoder() data = encoder.encode({"id": str(record.id), "name": record.name}) @@ -26,6 +26,7 @@ def test_msgspec_roundtrip() -> None: def test_msgspec_invalid() -> None: + """A malformed identifier fails msgspec validation.""" dec = msgspec.json.Decoder(type=Record, dec_hook=cuuid_decoder) with pytest.raises(msgspec.ValidationError): dec.decode(b'{"id":"invalid","name":"x"}') diff --git a/msgspec_crockford/unittests/test_types.py b/msgspec_crockford/unittests/test_types.py index 5ce6f75..e85786f 100644 --- a/msgspec_crockford/unittests/test_types.py +++ b/msgspec_crockford/unittests/test_types.py @@ -1,44 +1,48 @@ # pyright: reportMissingImports=false, reportGeneralTypeIssues=false, reportArgumentType=false +"""Unit tests for the `CrockfordUUID` type.""" + from __future__ import annotations import uuid -import pytest -import sys -from pathlib import Path - -sys.path.insert(0, str(Path(__file__).resolve().parents[2])) +import pytest -from msgspec_crockford import CrockfordUUID +from msgspec_crockford import CrockfordUUID, CrockfordUUIDError def test_str_round_trip() -> None: + """A stringified identifier reconstructs an equal instance.""" cuuid = CrockfordUUID.generate_v4() assert CrockfordUUID(str(cuuid)) == cuuid def test_uuid_property_returns_uuid() -> None: + """The `uuid` property exposes an equivalent plain `uuid.UUID`.""" cuuid = CrockfordUUID.generate_v4() assert isinstance(cuuid.uuid, uuid.UUID) assert cuuid.uuid.bytes == cuuid.bytes def test_invalid_bytes_length() -> None: - with pytest.raises(ValueError): + """A byte payload that is not 16 bytes long is rejected.""" + with pytest.raises(CrockfordUUIDError, match="expected 16 bytes"): CrockfordUUID(b"short") def test_invalid_string_input() -> None: - with pytest.raises(ValueError): + """A malformed Crockford string is rejected.""" + with pytest.raises(CrockfordUUIDError): CrockfordUUID("invalid") def test_invalid_type_input() -> None: - with pytest.raises(ValueError): - CrockfordUUID(123) # type: ignore[arg-type] + """A value of an unsupported type is rejected.""" + with pytest.raises(CrockfordUUIDError, match="expected Crockford string"): + CrockfordUUID(123) # type: ignore[arg-type] # ty: ignore[invalid-argument-type] def test_generate_v7_unavailable(monkeypatch: pytest.MonkeyPatch) -> None: + """`generate_v7` raises when `uuid.uuid7` is unavailable.""" monkeypatch.delattr(uuid, "uuid7", raising=False) with pytest.raises(NotImplementedError): CrockfordUUID.generate_v7() diff --git a/pycrockford_rs/Cargo.lock b/pycrockford_rs/Cargo.lock index 47fc95f..a743a94 100644 --- a/pycrockford_rs/Cargo.lock +++ b/pycrockford_rs/Cargo.lock @@ -1,12 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +version = 4 [[package]] name = "bitflags" @@ -46,15 +40,9 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "indoc" -version = "2.0.6" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "js-sys" @@ -72,60 +60,18 @@ version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" -[[package]] -name = "lock_api" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" -dependencies = [ - "autocfg", - "scopeguard", -] - [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "parking_lot" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets", -] - [[package]] name = "portable-atomic" version = "1.11.1" @@ -146,44 +92,38 @@ name = "pycrockford_rs" version = "0.1.0" dependencies = [ "data-encoding", - "once_cell", "pyo3", "uuid", ] [[package]] name = "pyo3" -version = "0.20.3" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233" +checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c" dependencies = [ - "cfg-if", - "indoc", "libc", - "memoffset", - "parking_lot", + "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] name = "pyo3-build-config" -version = "0.20.3" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deaa5745de3f5231ce10517a1f5dd97d53e5a2fd77aa6b5842292085831d48d7" +checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078" dependencies = [ - "once_cell", "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.20.3" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b42531d03e08d4ef1f6e85a2ed422eb678b8cd62b762e53891c05faf0d4afa" +checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b" dependencies = [ "libc", "pyo3-build-config", @@ -191,9 +131,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.20.3" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7305c720fa01b8055ec95e484a6eca7a83c841267f0dd5280f0c8b8551d2c158" +checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -203,13 +143,12 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.20.3" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c7e9b68bb9c3149c5b0cade5d07f953d6d125eb4337723c4ccdb665f1f96185" +checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362" dependencies = [ "heck", "proc-macro2", - "pyo3-build-config", "quote", "syn", ] @@ -229,33 +168,12 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" -[[package]] -name = "redox_syscall" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" -dependencies = [ - "bitflags", -] - [[package]] name = "rustversion" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - [[package]] name = "syn" version = "2.0.102" @@ -269,9 +187,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.16" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "unicode-ident" @@ -279,12 +197,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - [[package]] name = "uuid" version = "1.17.0" @@ -363,70 +275,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/pycrockford_rs/Cargo.toml b/pycrockford_rs/Cargo.toml index 01bdde2..4ec0461 100644 --- a/pycrockford_rs/Cargo.toml +++ b/pycrockford_rs/Cargo.toml @@ -8,7 +8,6 @@ name = "_pycrockford_rs_bindings" crate-type = ["cdylib", "rlib"] [dependencies] -once_cell = "1" data-encoding = "2" -pyo3 = { version = "0.20", features = ["extension-module", "abi3-py38"] } +pyo3 = { version = "0.29", features = ["extension-module", "abi3-py310"] } uuid = { version = "1", features = ["v4", "v7"] } diff --git a/pycrockford_rs/src/lib.rs b/pycrockford_rs/src/lib.rs index fdd8b48..fcda043 100644 --- a/pycrockford_rs/src/lib.rs +++ b/pycrockford_rs/src/lib.rs @@ -1,15 +1,15 @@ -#![allow(non_local_definitions)] +//! Rust bindings exposing Crockford Base32 UUID helpers to Python. use data_encoding::{Encoding, Specification}; -use once_cell::sync::Lazy; use pyo3::basic::CompareOp; use pyo3::exceptions::PyValueError; -use pyo3::once_cell::GILOnceCell; use pyo3::prelude::*; -use pyo3::pycell::PyRef; +use pyo3::sync::PyOnceLock; use pyo3::types::{PyAny, PyBytes, PyDict, PyModule, PyString, PyType}; +use pyo3::IntoPyObjectExt; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; +use std::sync::LazyLock; use uuid::Uuid; #[derive(Debug)] @@ -29,7 +29,7 @@ impl std::fmt::Display for CrockfordError { impl std::error::Error for CrockfordError {} -static CROCKFORD: Lazy = Lazy::new(|| { +static CROCKFORD: LazyLock = LazyLock::new(|| { let mut spec = Specification::new(); spec.symbols.push_str("0123456789ABCDEFGHJKMNPQRSTVWXYZ"); // Accept lowercase input for all allowed characters. @@ -43,11 +43,11 @@ static CROCKFORD: Lazy = Lazy::new(|| { }); // Cache the 'uuid' module to avoid repeated imports at runtime. -fn uuid_module(py: Python<'_>) -> PyResult<&PyModule> { - static UUID_MODULE: GILOnceCell> = GILOnceCell::new(); +fn uuid_module(py: Python<'_>) -> PyResult> { + static UUID_MODULE: PyOnceLock> = PyOnceLock::new(); let module = - UUID_MODULE.get_or_try_init(py, || PyModule::import(py, "uuid").map(|m| m.into()))?; - Ok(module.as_ref(py)) + UUID_MODULE.get_or_try_init(py, || PyModule::import(py, "uuid").map(Bound::unbind))?; + Ok(module.bind(py).clone()) } pub fn encode_bytes_to_crockford(bytes: &[u8; 16]) -> String { @@ -68,20 +68,16 @@ pub fn decode_crockford_to_bytes(s: &str) -> Result<[u8; 16], CrockfordError> { #[pyfunction] fn encode_crockford(b: &[u8]) -> PyResult { - if b.len() != 16 { - return Err(PyValueError::new_err(format!( - "expected 16 bytes, got {}", - b.len() - ))); - } - let arr: &[u8; 16] = b.try_into().unwrap(); + let arr: &[u8; 16] = b + .try_into() + .map_err(|_| PyValueError::new_err(format!("expected 16 bytes, got {}", b.len())))?; Ok(encode_bytes_to_crockford(arr)) } #[pyfunction] fn decode_crockford(py: Python<'_>, s: &str) -> PyResult> { let bytes = decode_crockford_to_bytes(s).map_err(|e| PyValueError::new_err(e.to_string()))?; - Ok(PyBytes::new(py, &bytes).into()) + Ok(PyBytes::new(py, &bytes).unbind()) } #[pyclass] @@ -89,58 +85,68 @@ struct CrockfordUUID { bytes: [u8; 16], } +impl CrockfordUUID { + /// Build from a Crockford Base32 string. + fn from_py_string(s: &Bound<'_, PyString>) -> PyResult { + let bytes = decode_crockford_to_bytes(s.to_str()?) + .map_err(|e| PyValueError::new_err(e.to_string()))?; + Ok(Self { bytes }) + } + + /// Build from a raw 16-byte value. + fn from_py_bytes(b: &Bound<'_, PyBytes>) -> PyResult { + let slice = b.as_bytes(); + let arr: [u8; 16] = slice.try_into().map_err(|_| { + PyValueError::new_err(format!("expected 16 bytes, got {}", slice.len())) + })?; + Ok(Self { bytes: arr }) + } + + /// Build from a `uuid.UUID` instance; rejects any other type. + fn from_py_uuid(value: &Bound<'_, PyAny>) -> PyResult { + let uuid_mod = uuid_module(value.py())?; + if !value.is_instance(&uuid_mod.getattr("UUID")?)? { + return Err(PyValueError::new_err( + "expected Crockford string, 16 bytes, or uuid.UUID", + )); + } + let bytes_vec: Vec = value.getattr("bytes")?.extract()?; + let arr: [u8; 16] = bytes_vec.as_slice().try_into().map_err(|_| { + PyValueError::new_err(format!("expected 16 bytes, got {}", bytes_vec.len())) + })?; + Ok(Self { bytes: arr }) + } +} + #[pymethods] -#[allow(non_local_definitions)] impl CrockfordUUID { #[new] - fn new(value: &PyAny) -> PyResult { - if let Ok(s) = value.downcast::() { - let bytes = decode_crockford_to_bytes(s.to_str()?) - .map_err(|e| PyValueError::new_err(e.to_string()))?; - Ok(Self { bytes }) - } else if let Ok(b) = value.downcast::() { - let slice = b.as_bytes(); - if slice.len() != 16 { - return Err(PyValueError::new_err(format!( - "expected 16 bytes, got {}", - slice.len() - ))); - } - let mut arr = [0u8; 16]; - arr.copy_from_slice(slice); - Ok(Self { bytes: arr }) + fn new(value: &Bound<'_, PyAny>) -> PyResult { + if let Ok(s) = value.cast::() { + Self::from_py_string(s) + } else if let Ok(b) = value.cast::() { + Self::from_py_bytes(b) } else { - let uuid_mod = uuid_module(value.py())?; - if value.is_instance(uuid_mod.getattr("UUID")?)? { - let py_bytes: &PyBytes = value.getattr("bytes")?.extract()?; - let slice = py_bytes.as_bytes(); - let mut arr = [0u8; 16]; - arr.copy_from_slice(slice); - Ok(Self { bytes: arr }) - } else { - Err(PyValueError::new_err( - "expected Crockford string, 16 bytes, or uuid.UUID", - )) - } + Self::from_py_uuid(value) } } #[getter] - fn bytes<'py>(&self, py: Python<'py>) -> &'py PyBytes { + fn bytes<'py>(&self, py: Python<'py>) -> Bound<'py, PyBytes> { PyBytes::new(py, &self.bytes) } #[getter] - fn uuid(&self, py: Python) -> PyResult { + fn uuid(&self, py: Python<'_>) -> PyResult> { let uuid_mod = uuid_module(py)?; let uuid_cls = uuid_mod.getattr("UUID")?; let kwargs = PyDict::new(py); kwargs.set_item("bytes", PyBytes::new(py, &self.bytes))?; - uuid_cls.call((), Some(kwargs)).map(Into::into) + Ok(uuid_cls.call((), Some(&kwargs))?.unbind()) } - fn __bytes__(&self, py: Python<'_>) -> PyObject { - PyBytes::new(py, &self.bytes).into() + fn __bytes__(&self, py: Python<'_>) -> Py { + PyBytes::new(py, &self.bytes).unbind() } fn __str__(&self) -> String { @@ -164,16 +170,21 @@ impl CrockfordUUID { value } - fn __richcmp__(&self, other: PyRef, op: CompareOp, py: Python<'_>) -> PyObject { + fn __richcmp__( + &self, + other: PyRef<'_, Self>, + op: CompareOp, + py: Python<'_>, + ) -> PyResult> { match op { - CompareOp::Eq => (self.bytes == other.bytes).into_py(py), - CompareOp::Ne => (self.bytes != other.bytes).into_py(py), - _ => py.NotImplemented(), + CompareOp::Eq => (self.bytes == other.bytes).into_py_any(py), + CompareOp::Ne => (self.bytes != other.bytes).into_py_any(py), + _ => Ok(py.NotImplemented()), } } #[classmethod] - fn generate_v4(_cls: &PyType) -> Self { + fn generate_v4(_cls: &Bound<'_, PyType>) -> Self { let uuid = Uuid::new_v4(); Self { bytes: *uuid.as_bytes(), @@ -181,7 +192,7 @@ impl CrockfordUUID { } #[classmethod] - fn generate_v7(_cls: &PyType) -> Self { + fn generate_v7(_cls: &Bound<'_, PyType>) -> Self { let uuid = Uuid::now_v7(); Self { bytes: *uuid.as_bytes(), @@ -190,7 +201,7 @@ impl CrockfordUUID { } #[pymodule] -fn _pycrockford_rs_bindings(_py: Python, m: &PyModule) -> PyResult<()> { +fn _pycrockford_rs_bindings(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(decode_crockford, m)?)?; m.add_function(wrap_pyfunction!(encode_crockford, m)?)?; m.add_class::()?; diff --git a/pyproject.toml b/pyproject.toml index 898a961..cc15fa3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,16 +3,313 @@ name = "pycrockford_msgspec" version = "0.1.0" description = "Crockford Base32 UUID integration for msgspec" readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.10" authors = [{name = "Example"}] dependencies = ["msgspec>=0.19"] [project.optional-dependencies] dev = ["pytest", "ruff", "pyright", "maturin"] +[dependency-groups] +dev = [ + "pytest", + "interrogate", + "pip-audit", + "ruff", + "pyright", + "ty", + "pytest-timeout", + "pytest-xdist", +] + +[tool.ruff] +line-length = 88 +preview = true +target-version = "py310" + +[tool.ruff.lint] +select = [ + "F", # Pyflakes rules + "W", # PyCodeStyle warnings + "E", # PyCodeStyle errors + "I", # Sort imports properly + "UP", # Warn if certain things can changed due to newer Python versions + "C4", # Catch incorrect use of comprehensions, dict, list, etc + "FA", # Enforce from __future__ import annotations + "ISC", # Good use of string concatenation + "ICN", # Use common import conventions + "RET", # Good return practices + "SIM", # Common simplification rules + "TID", # Some good import practices + "TC", # Enforce importing certain types in a TYPE_CHECKING block + "PTH", # Use pathlib instead of os.path + "TD", # Be diligent with TODO comments + "A", # detect shadowed builtins + "BLE", # disallow catch-all exceptions + "S", # disallow things like "exec"; also restricts "assert" but I just NOQA it when I really need it + "DTZ", # require strict timezone manipulation with datetime + "FBT", # detect boolean traps + "N", # enforce naming conventions, e.g. ClassName vs function_name + "FURB", + "B", + "RUF", + "LOG", + "Q", + "PT", + "RSE", + "PERF", + "TRY", + "D", + "ANN", + "C90", + "PLR", + "PLE4703", + "PLR0202", + "PLR0203", + "PLR0914", + "PLR0916", + "PLR1702", + "PLR6104", + "PLR6201", + "PLR6301", + "PLW0108", + "RUF036", + "RUF053", + "RUF055", + "RUF064", + "RUF066", +] +extend-ignore = [ + "D203", # Conflicts with D211; prefer enforcing no blank line before class docstring. + "D213", # Conflicts with D212; keep summary on the first line. +] + +[tool.ruff.lint.per-file-ignores] +"**/__init__.py" = ["RUF067"] +"**/test_*.py" = ["S101", "PLR0913", "PLR0917", "PLR2004", "PLR6301"] + +[tool.ruff.lint.flake8-import-conventions] +# Declare the banned `from` imports. +banned-from = [ + "typing", + "datetime", + "collections.abc", + "dataclasses", + "enum", + "unittest.mock", + "msgspec", +] + +[tool.ruff.lint.flake8-import-conventions.aliases] +# Declare the default aliases. +altair = "alt" +"matplotlib.pyplot" = "plt" +numpy = "np" +pandas = "pd" +seaborn = "sns" +scipy = "sp" +"collections.abc" = "cabc" +datetime = "dt" +"unittest.mock" = "mock" +"msgspec.json" = "msjson" +typing = "typ" + +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"typing.AbstractSet".msg = "Use collections.abc.Set instead of deprecated typing generics." +"typing.AsyncContextManager".msg = "Use contextlib.AbstractAsyncContextManager instead of deprecated typing generics." +"typing.AsyncGenerator".msg = "Use collections.abc.AsyncGenerator instead of deprecated typing generics." +"typing.AsyncIterable".msg = "Use collections.abc.AsyncIterable instead of deprecated typing generics." +"typing.AsyncIterator".msg = "Use collections.abc.AsyncIterator instead of deprecated typing generics." +"typing.Awaitable".msg = "Use collections.abc.Awaitable instead of deprecated typing generics." +"typing.Callable".msg = "Use collections.abc.Callable instead of deprecated typing generics." +"typing.ChainMap".msg = "Use collections.ChainMap instead of deprecated typing generics." +"typing.Collection".msg = "Use collections.abc.Collection instead of deprecated typing generics." +"typing.Container".msg = "Use collections.abc.Container instead of deprecated typing generics." +"typing.ContextManager".msg = "Use contextlib.AbstractContextManager instead of deprecated typing generics." +"typing.Coroutine".msg = "Use collections.abc.Coroutine instead of deprecated typing generics." +"typing.Counter".msg = "Use collections.Counter instead of deprecated typing generics." +"typing.DefaultDict".msg = "Use collections.defaultdict instead of deprecated typing generics." +"typing.Deque".msg = "Use collections.deque instead of deprecated typing generics." +"typing.Dict".msg = "Use dict instead of deprecated typing generics." +"typing.FrozenSet".msg = "Use frozenset instead of deprecated typing generics." +"typing.Generator".msg = "Use collections.abc.Generator instead of deprecated typing generics." +"typing.ItemsView".msg = "Use collections.abc.ItemsView instead of deprecated typing generics." +"typing.Iterable".msg = "Use collections.abc.Iterable instead of deprecated typing generics." +"typing.Iterator".msg = "Use collections.abc.Iterator instead of deprecated typing generics." +"typing.KeysView".msg = "Use collections.abc.KeysView instead of deprecated typing generics." +"typing.List".msg = "Use list instead of deprecated typing generics." +"typing.Mapping".msg = "Use collections.abc.Mapping instead of deprecated typing generics." +"typing.Match".msg = "Use re.Match instead of deprecated typing generics." +"typing.MutableMapping".msg = "Use collections.abc.MutableMapping instead of deprecated typing generics." +"typing.MutableSequence".msg = "Use collections.abc.MutableSequence instead of deprecated typing generics." +"typing.MutableSet".msg = "Use collections.abc.MutableSet instead of deprecated typing generics." +"typing.OrderedDict".msg = "Use collections.OrderedDict instead of deprecated typing generics." +"typing.Pattern".msg = "Use re.Pattern instead of deprecated typing generics." +"typing.Reversible".msg = "Use collections.abc.Reversible instead of deprecated typing generics." +"typing.Sequence".msg = "Use collections.abc.Sequence instead of deprecated typing generics." +"typing.Set".msg = "Use set instead of deprecated typing generics." +"typing.Tuple".msg = "Use tuple instead of deprecated typing generics." +"typing.Type".msg = "Use type instead of deprecated typing generics." +"typing.ValuesView".msg = "Use collections.abc.ValuesView instead of deprecated typing generics." + +[tool.ruff.lint.pydocstyle] +convention = "numpy" + +[tool.ruff.lint.mccabe] +max-complexity = 8 + +[tool.ruff.lint.pylint] +max-args = 4 +max-bool-expr = 2 +max-locals = 10 + +[tool.pylint.main] +recursive = true +max-module-lines = 400 + +[tool.pylint.design] +max-args = 4 +max-locals = 20 +max-statements = 70 +max-positional-arguments = 4 + +[tool.pylint."messages control"] +disable = [ + "all", + # Managed PyPy can lag the requested CPython syntax; keep the PyPy-backed + # pass useful on files it can parse. + "syntax-error", +] +enable = [ + # Logging format safety. + "logging-format-interpolation", + "logging-format-truncated", + "logging-fstring-interpolation", + "logging-not-lazy", + "logging-too-few-args", + "logging-too-many-args", + "logging-unsupported-format", + + # Pattern matching correctness and readability. + "bare-name-capture-pattern", + "invalid-match-args-definition", + "match-class-bind-self", + "match-class-positional-attributes", + "multiple-class-sub-patterns", + "too-many-positional-sub-patterns", + + # Control-flow simplification. + "chained-comparison", + "condition-evals-to-constant", + "consider-merging-isinstance", + "consider-swap-variables", + "consider-using-in", + "consider-using-max-builtin", + "consider-using-min-builtin", + "consider-using-sys-exit", + "consider-using-ternary", + "inconsistent-return-statements", + "no-else-break", + "no-else-continue", + "no-else-raise", + "no-else-return", + "redefined-argument-from-local", + "simplifiable-condition", + "simplifiable-if-expression", + "simplifiable-if-statement", + "simplify-boolean-expression", + "stop-iteration-return", + "super-with-arguments", + "trailing-comma-tuple", + "unnecessary-negation", + "useless-return", + + # Collection and iterator idioms. + "consider-iterating-dictionary", + "consider-using-dict-comprehension", + "consider-using-dict-items", + "consider-using-enumerate", + "consider-using-f-string", + "consider-using-generator", + "consider-using-get", + "consider-using-join", + "consider-using-set-comprehension", + "unnecessary-comprehension", + "unnecessary-dict-index-lookup", + "unnecessary-list-index-lookup", + "use-a-generator", + "use-dict-literal", + "use-implicit-booleaness-not-comparison", + "use-implicit-booleaness-not-comparison-to-string", + "use-implicit-booleaness-not-len", + "use-list-literal", + "use-maxsplit-arg", + "use-sequence-for-iteration", + "use-yield-from", + + # Runtime APIs, resources, and compatibility hazards. + "bad-open-mode", + "bad-thread-instantiation", + "boolean-datetime", + "consider-using-with", + "deprecated-argument", + "deprecated-attribute", + "deprecated-class", + "deprecated-decorator", + "deprecated-method", + "forgotten-debug-statement", + "invalid-envvar-default", + "invalid-envvar-value", + "method-cache-max-size-none", + "redundant-unittest-assert", + "shallow-copy-environ", + "singledispatch-method", + "singledispatchmethod-function", + "subprocess-popen-preexec-fn", + "subprocess-run-check", + "unnecessary-dunder-call", + "unnecessary-ellipsis", + "unspecified-encoding", + + # Source layout and text hygiene. + "missing-final-newline", + "mixed-line-endings", + "superfluous-parens", + "trailing-newlines", + "trailing-whitespace", + "unexpected-line-ending-format", + + # Mutation during iteration. + "modified-iterating-dict", + "modified-iterating-list", + "modified-iterating-set", + + # Structural complexity limits. + "too-many-arguments", + "too-many-boolean-expressions", + "too-many-branches", + "too-many-lines", + "too-many-locals", + "too-many-nested-blocks", + "too-many-positional-arguments", + "too-many-public-methods", + "too-many-statements", +] + +[tool.pytest.ini_options] +# Tests automatically killed after seconds elapsed +timeout = 30 +testpaths = ["msgspec_crockford/unittests", "pycrockford_rs/tests"] + +[tool.uv] +package = true + + [build-system] requires = ["maturin>=1.8,<2.0"] build-backend = "maturin" -[tool.uv] -package = true +[tool.maturin] +manifest-path = "pycrockford_rs/Cargo.toml" +module-name = "_pycrockford_rs_bindings" +python-packages = ["msgspec_crockford"] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..55d768e --- /dev/null +++ b/uv.lock @@ -0,0 +1,854 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[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 = "boolean-py" +version = "5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/cf/85379f13b76f3a69bca86b60237978af17d6aa0bc5998978c3b8cf05abb2/boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95", size = 37047, upload-time = "2025-04-03T10:39:49.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" }, +] + +[[package]] +name = "cachecontrol" +version = "0.14.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msgpack" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/f6/c972b32d80760fb79d6b9eeb0b3010a46b89c0b23cf6329417ff7886cd22/cachecontrol-0.14.4.tar.gz", hash = "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1", size = 16150, upload-time = "2025-11-14T04:32:13.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl", hash = "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b", size = 22247, upload-time = "2025-11-14T04:32:11.733Z" }, +] + +[package.optional-dependencies] +filecache = [ + { name = "filelock" }, +] + +[[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 = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/81/8e983840c6e5b93b33c2ba81aa3d52c2e42f0e9a690ce7607a2e61da4a5c/charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a", size = 322240, upload-time = "2026-07-07T14:32:36.236Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/b4319dc3229d8272fba305e206fc0a148e2de8d4087917ce62ae6382f359/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616", size = 216475, upload-time = "2026-07-07T14:32:38.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/6c99c1b3e6b8bf730e1bc809b9a2608f224145069114c479a2e9e1494346/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209", size = 238670, upload-time = "2026-07-07T14:32:39.658Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f4/ffbb83546e1f198ecc70ecd372b65cf2b50f9068b380abd67640f17a8e18/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99", size = 233476, upload-time = "2026-07-07T14:32:41.155Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5f/b98b8da398637b551e427e7be922bdec19177dc54d6811dcdaa503f23aac/charset_normalizer-3.4.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8", size = 223817, upload-time = "2026-07-07T14:32:42.592Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/a276bb2e66243072a3fd06fdcab9cbb61a305b02143d70d2bda21d888fa8/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b", size = 207974, upload-time = "2026-07-07T14:32:44.258Z" }, + { url = "https://files.pythonhosted.org/packages/5e/be/7ee4453d7e88dfbc4104ccd34900b9f2c7c17dac22881865fe0e82424a25/charset_normalizer-3.4.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2", size = 221655, upload-time = "2026-07-07T14:32:45.64Z" }, + { url = "https://files.pythonhosted.org/packages/1d/85/181c652953eb5276d198f375b1dd641047392050098100a3a02d6534f657/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9", size = 219229, upload-time = "2026-07-07T14:32:47.376Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e7/aaf6da33fc9f4691cda8f7efbc9f69179d3d39ec8a4799baf273ee1d8db0/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15", size = 209704, upload-time = "2026-07-07T14:32:48.855Z" }, + { url = "https://files.pythonhosted.org/packages/63/01/f2fb3bd3a73be48b173ee0c6aa8d2497af97d5663a8c4c4b491de4c62f7a/charset_normalizer-3.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d", size = 226243, upload-time = "2026-07-07T14:32:50.239Z" }, + { url = "https://files.pythonhosted.org/packages/c4/02/c57a22739fe05246b0b5783b3bfb6afaac4eebb46f3ececdfb2f048f780e/charset_normalizer-3.4.9-cp310-cp310-win32.whl", hash = "sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381", size = 150935, upload-time = "2026-07-07T14:32:51.676Z" }, + { url = "https://files.pythonhosted.org/packages/37/8d/ca39a7559a4797505530d084fd3a49a2c959efbbbff146302fb7be4e3b35/charset_normalizer-3.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee", size = 162314, upload-time = "2026-07-07T14:32:53.193Z" }, + { url = "https://files.pythonhosted.org/packages/01/da/a44bd7a13d426e69e4894557106cd58669097bfad4a8681123b618fbfc5d/charset_normalizer-3.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419", size = 153075, upload-time = "2026-07-07T14:32:54.554Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, + { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, + { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, + { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, + { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, + { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, + { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, + { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, + { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, + { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, + { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, + { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, + { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[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 = "cyclonedx-python-lib" +version = "11.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "license-expression" }, + { name = "packageurl-python" }, + { name = "py-serializable" }, + { name = "sortedcontainers" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/c9/5d0ccdd19bc7d8ab803b90695c1706aa2ea8529685d18e682dc2524d2630/cyclonedx_python_lib-11.11.0.tar.gz", hash = "sha256:4b3194db72b613717f2912447e67ab618c75ff7dcac6c4af3c0e9e1ac617c102", size = 1442983, upload-time = "2026-06-17T11:57:49.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/f3/56ccb2884aaa3db5622368e5191a3384b15f35392aa93df8b2f508c660d2/cyclonedx_python_lib-11.11.0-py3-none-any.whl", hash = "sha256:3049fc83e06a059b5c5907a527625a8ed5073caab10607ed4c9e5503b590fd44", size = 528689, upload-time = "2026-06-17T11:57:47.358Z" }, +] + +[[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 = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +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" } +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" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/94/00f2059e4835eace3ae8fde680b932c496f8ec7bdc99168dfa53fb2e6b79/filelock-3.29.7.tar.gz", hash = "sha256:5b481979797ae69e72f0b389d89a80bdd585c260c5b3f1fb9c0a5ba9bb3f195d", size = 71521, upload-time = "2026-07-08T05:46:58.716Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/02/be4a57b60c7149b55b9e3b3c13f609cd8eb5307c751f22bd8fb8d262e75b/filelock-3.29.7-py3-none-any.whl", hash = "sha256:987db6f789a3a2a59f55081801b2b3697cb97e2a736b5f1a9e99b559285fbc51", size = 46036, upload-time = "2026-07-08T05:46:57.53Z" }, +] + +[[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 = "interrogate" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "click" }, + { name = "colorama" }, + { name = "py" }, + { name = "tabulate" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/22/74f7fcc96280eea46cf2bcbfa1354ac31de0e60a4be6f7966f12cef20893/interrogate-1.7.0.tar.gz", hash = "sha256:a320d6ec644dfd887cc58247a345054fc4d9f981100c45184470068f4b3719b0", size = 159636, upload-time = "2024-04-07T22:30:46.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/c9/6869a1dcf4aaf309b9543ec070be3ec3adebee7c9bec9af8c230494134b9/interrogate-1.7.0-py3-none-any.whl", hash = "sha256:b13ff4dd8403369670e2efe684066de9fcb868ad9d7f2b4095d8112142dc9d12", size = 46982, upload-time = "2024-04-07T22:30:44.277Z" }, +] + +[[package]] +name = "license-expression" +version = "30.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boolean-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/71/d89bb0e71b1415453980fd32315f2a037aad9f7f70f695c7cec7035feb13/license_expression-30.4.4.tar.gz", hash = "sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd", size = 186402, upload-time = "2025-07-22T11:13:32.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/40/791891d4c0c4dab4c5e187c17261cedc26285fd41541577f900470a45a4d/license_expression-30.4.4-py3-none-any.whl", hash = "sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4", size = 120615, upload-time = "2025-07-22T11:13:31.217Z" }, +] + +[[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 = "maturin" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/b3/addd877f871fb1860d46d3a4f206ecb10b946c85846805e6367631926fd3/maturin-1.14.1.tar.gz", hash = "sha256:9d6577a62cd08e0ceba7a0db06fb098e0c9b1b3429bad747a4f3a18215a1b3df", size = 369637, upload-time = "2026-06-19T05:19:49.774Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/f0/97c5a5bd9c71653a066c0976a484eaaae50b9369557838a4176b7b0bdaa5/maturin-1.14.1-py3-none-linux_armv6l.whl", hash = "sha256:522292398945442cdafa9daeb2271b2340fbde57027b818f923f88eab04174f8", size = 10207496, upload-time = "2026-06-19T05:19:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/fe/83/294bca639b0e052f1e2f65199b3db258780c7d4e31408b934c9c974a1379/maturin-1.14.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ffe5ad71f21d1e6603c4dd75f7fee34adf5ed5ebcebb692886549888ebb329ed", size = 19680113, upload-time = "2026-06-19T05:19:13.43Z" }, + { url = "https://files.pythonhosted.org/packages/43/b6/79c881410a3b1c187f7eb3d407aecae646c6a4433d630d72200359015e83/maturin-1.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f3306078070c1508fd715b9116070cbcaff5959024272a9f1e6f5cb29768b86c", size = 10169205, upload-time = "2026-06-19T05:19:16.615Z" }, + { url = "https://files.pythonhosted.org/packages/93/9d/44b6f26dcb7f7a04c5501ac2dbb6ca1490150682baa525ca5860504f9eab/maturin-1.14.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:cd457cd88961156e26379e1155bd287cc0ec1c8b2f1582b0660fb31b87c8842d", size = 10188098, upload-time = "2026-06-19T05:19:19.736Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bd/9c0d5d6983905ce2c9edaa073a7e89355a9cf7f396988e05d32f1c37785d/maturin-1.14.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:dfc54ae32e6fcb18302193ab9a30b0b25eefffba994ae13238974805533ef75e", size = 10627576, upload-time = "2026-06-19T05:19:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/e5/33/b096412bd6a7cb399652b260666f901adf88a687181a6dbd6a3f89f0a94e/maturin-1.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a131d912b5267e640bc96d70f4914e10590aed64082ec9abacba7cea52004224", size = 10085181, upload-time = "2026-06-19T05:19:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/56/8d/08c3bf469c38a23c9e6c877e338193001eb604d010fedc08341974e38528/maturin-1.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:be18fc568fb76884c0205456336892a75105ec398e6b667cd777c6268bd06d69", size = 10026363, upload-time = "2026-06-19T05:19:28.904Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/c4d1a92839f8745ab4aab988a7db884a79d6d710bd3b286fcf9316dece1a/maturin-1.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:994a0c8ba3ad8a92b3a9ee1b02645d200d610216b15cff5102b0fe65e8e08666", size = 13321347, upload-time = "2026-06-19T05:19:32.411Z" }, + { url = "https://files.pythonhosted.org/packages/b3/fa/170f04624d03fd07d2a8b1b67de83a127af93aef9eaa425839553347297b/maturin-1.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be80866363e605d137991b491a741a84cde9ae350183c4c85f49690ca9aaaa65", size = 10877609, upload-time = "2026-06-19T05:19:35.448Z" }, + { url = "https://files.pythonhosted.org/packages/61/ad/1ae2e1d0ded282bf2c55ac13f0811d87deb425e200ae64a15785675dede9/maturin-1.14.1-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:5282dffd4b539d2be245f4e5b1a5ab6bc1033b58f4a4872f5833f9d43c954aa4", size = 10417316, upload-time = "2026-06-19T05:19:38.28Z" }, + { url = "https://files.pythonhosted.org/packages/fb/27/bf677183920718da49cd7982d6a3ffc440aad8919329f571d189f81b7bdf/maturin-1.14.1-py3-none-win32.whl", hash = "sha256:1a04de0a20188f95c721b5702eed18140bdcccb28c386797093eca3f62f4d4e0", size = 8931293, upload-time = "2026-06-19T05:19:41.183Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/585adeb9167b08d3cdff0032a938b0e72655c92003df4f52c3f696a1bcc2/maturin-1.14.1-py3-none-win_amd64.whl", hash = "sha256:3c9f94640ecc4895e94abaf834a0684430032c865b2748a36c12461fd9252fdd", size = 10314067, upload-time = "2026-06-19T05:19:44.389Z" }, + { url = "https://files.pythonhosted.org/packages/51/d4/dac8c0720ae246be1700afb6fbdbbea20fe35b13f6570b2f70faa005df77/maturin-1.14.1-py3-none-win_arm64.whl", hash = "sha256:15cea8fcb3ba47dd636f50092bb34baea8b04ac777392f23e6bf8a9a61efb894", size = 9718943, upload-time = "2026-06-19T05:19:47.49Z" }, +] + +[[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 = "msgpack" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/16/f70100614b69feb3ade7285f08c9c52d6cda0a5c03f3f5e2facd63acb211/msgpack-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c7b398c56ff125feae96c2737abfec5595f1fa0aa186df60c56040b8accb95c", size = 82926, upload-time = "2026-06-18T16:12:31.531Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3c/08ecd5cdfe4e2de43aec79062028ad0f7b2d9b1fea5430068c198ba570da/msgpack-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1548006a91aa93c5da81f3bdcebc1a0d10cea2d25969754fbe848da622b2b895", size = 82730, upload-time = "2026-06-18T16:12:32.894Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/a70c9cb1a04ecc134005149367dcfe35d167284e8f65035a1e4156ad17b5/msgpack-1.2.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1dabedcd0f23559f3596428c6589c1cd8c6eaed3a0d720795b07b0225d769203", size = 400729, upload-time = "2026-06-18T16:12:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7f/5ce020168cf0439041526e95aa068c722c016aee21624e331aeabeee2e8e/msgpack-1.2.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83efa1c898e0fc5380fc0cabbf75164c52e3b5cbb45973710d75821928380c73", size = 407625, upload-time = "2026-06-18T16:12:35.239Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/fb7668ce0386819303047057aef6fc1da73b584291d9cff82b821744e2ef/msgpack-1.2.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01e2dd6c9b19d333a00282330cc8a73d38d8dabc306dc5b42cd668c3ac82e833", size = 377891, upload-time = "2026-06-18T16:12:36.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/dc/9ebe654a73c3aed2e40aa6b52e3c2a02b5f53ef0085fa235a45d5b367f87/msgpack-1.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:350cb813d0af6e65d2f7ef0d729f7ff5be5a8bce03665892f43e5883d4ecc1b8", size = 391987, upload-time = "2026-06-18T16:12:37.839Z" }, + { url = "https://files.pythonhosted.org/packages/42/eb/b67cf64218a2fa25e1c671fe1d3dbb06cbeb973e71bc4b822da079862d0b/msgpack-1.2.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ee1d9ed27d0497b848923746cf762ed2e7db24f4be7eec8e5cbe8c766aa707b7", size = 374603, upload-time = "2026-06-18T16:12:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2e/9ee200cde32fd1a0101b4006202fde554c1860adfb9bf7bff31ea4c08df8/msgpack-1.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:633727297ed063441fd1cda2288865487f33ad14eeb8831afb5f0c396a62cfce", size = 405121, upload-time = "2026-06-18T16:12:40.524Z" }, + { url = "https://files.pythonhosted.org/packages/43/b6/f10117be7ca7a51e8feed699a907b8e663a8cd66e115ae6b4fb30cc7945c/msgpack-1.2.1-cp310-cp310-win32.whl", hash = "sha256:298872ecf9e61950f1c6af4ca969b859ee91783bb920ef6e6172697d0c8aad74", size = 64088, upload-time = "2026-06-18T16:12:41.762Z" }, + { url = "https://files.pythonhosted.org/packages/ba/93/89976c696fb0224662239d952c47b4d1661b34d79a332ef5584facaa8579/msgpack-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2ff164c1b0bcb740b073b99e945234d0212852fa378e44a208c425379140dbeb", size = 70113, upload-time = "2026-06-18T16:12:42.78Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6b/e9b1cdc042c4458801d2545ed782a95f3d6ba8e270cce8745b8603c7f748/msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22", size = 82812, upload-time = "2026-06-18T16:12:45.022Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3a/dd518a1bf78ed1e9ad8afe57307c079a00eafe4b3068932a27ca1ea56b4f/msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5", size = 82739, upload-time = "2026-06-18T16:12:46.025Z" }, + { url = "https://files.pythonhosted.org/packages/70/e0/7ba9e1542bf0771a27b8b37c1316e3f95ae9d748fd765284655c476ad4ef/msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06", size = 414233, upload-time = "2026-06-18T16:12:47.029Z" }, + { url = "https://files.pythonhosted.org/packages/03/8d/671d81534ea0e2b0e8a121be100020da09eb78861fe3aa8f3ef7dcd3bed1/msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4", size = 423843, upload-time = "2026-06-18T16:12:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b6/e5c737515ed1f166664b87601b532f58cbb73d8aa6a90b99f7c2c5037e8e/msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8", size = 390772, upload-time = "2026-06-18T16:12:49.624Z" }, + { url = "https://files.pythonhosted.org/packages/a8/46/62ed8c2e87d7021eab19921594d961ef3aa3794eec76c716dc30f3bfd433/msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b", size = 409559, upload-time = "2026-06-18T16:12:50.936Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/59aa3887b860bbf43532835e192b1c388a17590d6068ae4f8b2bc74c906e/msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e", size = 387838, upload-time = "2026-06-18T16:12:52.161Z" }, + { url = "https://files.pythonhosted.org/packages/09/11/f8563e471093420cf6478cb3271a0175d8402b82d879783d4035d2d03360/msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f", size = 421732, upload-time = "2026-06-18T16:12:53.556Z" }, + { url = "https://files.pythonhosted.org/packages/57/cf/e673683c4c6c90c1022b24c65af4b03eda72b182a1176ef6449069d66acc/msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d", size = 64091, upload-time = "2026-06-18T16:12:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/ca212739d179f9083bff2c7c08c24101c3555a334fadc2b876b18768a3ae/msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8", size = 70462, upload-time = "2026-06-18T16:12:55.898Z" }, + { url = "https://files.pythonhosted.org/packages/6d/be/6798347b425e26f35db82e69dd83c09716c856a3714e7bffc4c0860fd830/msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66", size = 65059, upload-time = "2026-06-18T16:12:57.053Z" }, + { url = "https://files.pythonhosted.org/packages/bc/dd/9e8cbd8f5582ca4b590336f2b91ee5662f6a6ca562b565abaf696a0f81ff/msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35", size = 83531, upload-time = "2026-06-18T16:12:58.249Z" }, + { url = "https://files.pythonhosted.org/packages/50/2e/ebdb85a8da151397a2790363676b7ed7c125924fe618e4c6d8befb0cc62c/msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c", size = 82657, upload-time = "2026-06-18T16:12:59.396Z" }, + { url = "https://files.pythonhosted.org/packages/26/aa/753ad8b007b464e1d8aa0c8e650b9c5f4f725e658fc5ac8a7635c55b7f6e/msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0", size = 410634, upload-time = "2026-06-18T16:13:00.383Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a", size = 419989, upload-time = "2026-06-18T16:13:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cc/85039b7b0eb168aaad7383a23c97e291a11f08351cb45a606ce865e4e3f1/msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6", size = 377544, upload-time = "2026-06-18T16:13:03.637Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bf/35963899493b32030c85fc513b723ae66144ac70c11ebc52e889e16e3d99/msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a", size = 400842, upload-time = "2026-06-18T16:13:05.012Z" }, + { url = "https://files.pythonhosted.org/packages/a6/df/8e2ac970c8f99264cd9997d1c73df5466bc19da3301d7dc5500862a9b089/msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1", size = 374108, upload-time = "2026-06-18T16:13:06.232Z" }, + { url = "https://files.pythonhosted.org/packages/17/dd/fa8bd265110dfa51c20cb529f9e6d240a16fafe7e645004c6af2d01353ba/msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64", size = 414939, upload-time = "2026-06-18T16:13:07.478Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b9/8377a5ad8953fc0437c70cc98d9ae29f27fe5ac5109fbec0812085865735/msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac", size = 64504, upload-time = "2026-06-18T16:13:08.822Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/ce1e377df7e62461fefd9eb23bfb93a4a523f40a517b377b8f844d836828/msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24", size = 71421, upload-time = "2026-06-18T16:13:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/8f/32/ebfe84c9929f08f188d56c7a2fd913406a9ddad76a634697c1c43b8112e6/msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07", size = 64775, upload-time = "2026-06-18T16:13:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/dcddcab6f6c20ecb387ca5e980371cdb3f87ff69aeca388be97eebc4c074/msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064", size = 83151, upload-time = "2026-06-18T16:13:12.173Z" }, + { url = "https://files.pythonhosted.org/packages/64/71/fbcfa83a1d6a9c6091942d1cfd070962244664b87427a9a49a6897b1b219/msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056", size = 82351, upload-time = "2026-06-18T16:13:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/e3/10/ddf7b06db879e8792d13934ddda09ff20bd2a583fd84c9b59aae9b0e650b/msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc", size = 407518, upload-time = "2026-06-18T16:13:14.233Z" }, + { url = "https://files.pythonhosted.org/packages/79/d3/36a46a8ed992b781acbc05928bd5bee3c810cb0c3563bf81a7b0c04a1a76/msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d", size = 416405, upload-time = "2026-06-18T16:13:15.435Z" }, + { url = "https://files.pythonhosted.org/packages/f9/84/e8e9598b557c0ba6ddae901a73780a4c75ac667dddf59414b1e56a42fb34/msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155", size = 376257, upload-time = "2026-06-18T16:13:17.022Z" }, + { url = "https://files.pythonhosted.org/packages/40/16/738fe6d875ad7e2a9429c165322a4ec088f4f273cdfae63d96a89c467961/msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402", size = 397469, upload-time = "2026-06-18T16:13:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/ca/be/6d5952df75a7f24f35833af764c3a6860780364cb3a0030beb8099e1b2b4/msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c", size = 372802, upload-time = "2026-06-18T16:13:19.685Z" }, + { url = "https://files.pythonhosted.org/packages/e1/39/e2ef7dbf0473bcb8dc7c50bf782a892d67414877b63e47fc88eb189ef5e6/msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6", size = 411273, upload-time = "2026-06-18T16:13:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c5/133f4512a56e983a93445c836c9d94d88f3bc2e0980ff4b9e577bd8416ce/msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707", size = 64471, upload-time = "2026-06-18T16:13:22.293Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/577e10b055096a7dd40732358cabaf7180a20c79ed1dcdbb618e4b9deac7/msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9", size = 71274, upload-time = "2026-06-18T16:13:23.455Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ee/0c0048e7cfbef23c6a94791b8959ab28155232e7956de8a305b5ff588f05/msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a", size = 64795, upload-time = "2026-06-18T16:13:24.687Z" }, + { url = "https://files.pythonhosted.org/packages/77/58/cce442852c6b9e1639c7c8ac8fd9143121cb32dab0f308df4d1426a8eb9c/msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d", size = 83610, upload-time = "2026-06-18T16:13:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/60/5c/15b4c7a0182f75ffa90751958ba36a9c01cafee367d49a3edc10ed140b01/msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7", size = 83138, upload-time = "2026-06-18T16:13:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/99e58722feaffc5f2fbcc0c8c0d1451ab9f84097f7af87291b46af2390f4/msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889", size = 406090, upload-time = "2026-06-18T16:13:28.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/03/8c63e8cf52958534ef688625965ab04c269a6cadd8caef16758b380a821a/msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720", size = 412106, upload-time = "2026-06-18T16:13:29.427Z" }, + { url = "https://files.pythonhosted.org/packages/63/d2/155d9e71b40e41fd934bc0c48b9b2770f22263e1ac20aad8e29fdca7be3f/msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190", size = 374851, upload-time = "2026-06-18T16:13:30.631Z" }, + { url = "https://files.pythonhosted.org/packages/98/48/deaf2326262a8d5ea3295ce9649912ecd3f551ba7ec8e33c665d2ba583f3/msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d", size = 396168, upload-time = "2026-06-18T16:13:31.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/2a/b4410f906c2ec0008f1608d3ab5143afc3ad3f4e6da0fed3ea2231d0bef4/msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24", size = 371959, upload-time = "2026-06-18T16:13:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/59/86/1edc67270099a528fa2093ea60fe191233cd238e4bd30cfacf7db79fc959/msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7", size = 408457, upload-time = "2026-06-18T16:13:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/82/90/8b630fef07d8c5ab457b71ff2c217910c83d333c7a68472c186e87cc504a/msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb", size = 65942, upload-time = "2026-06-18T16:13:36.056Z" }, + { url = "https://files.pythonhosted.org/packages/16/f1/467b81e98b24dd3885d7b1857728797b4ffc76a7a7483af4fb321a07de3c/msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b", size = 72627, upload-time = "2026-06-18T16:13:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1d/5d8c4c89985feb6acefb82a09e501c60392261856d2408d20bfe4f0360b1/msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7", size = 66908, upload-time = "2026-06-18T16:13:38.23Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ad2afb678b4de94496cd432b581759b756a92c1192d8c767edd6b132efdc/msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273", size = 86000, upload-time = "2026-06-18T16:13:39.44Z" }, + { url = "https://files.pythonhosted.org/packages/54/74/0b797484013128837f3b1cbb6cea019277c4de4e377dc512b4d9a0f92940/msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1", size = 86544, upload-time = "2026-06-18T16:13:40.447Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b4/b774d7eb95561739907fec675582f83203cf41c597a418c2589b4bfb8e9d/msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc", size = 427661, upload-time = "2026-06-18T16:13:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f9/3243191dc9937e00756c8bc1b0272fed8f23758e43df2a3b46f533e5090f/msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde", size = 426375, upload-time = "2026-06-18T16:13:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/23/c7/1693111db9944ba4ad4b67a1e788400d78a0b6af7a6523dc7e4e58f8274b/msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4", size = 380495, upload-time = "2026-06-18T16:13:44.306Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2b/92f86956a0c13e8662f7e2ad630c4eb4db07497b967589bd5245e018b2c1/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d", size = 410897, upload-time = "2026-06-18T16:13:45.629Z" }, + { url = "https://files.pythonhosted.org/packages/da/ea/1479f72d200313a76fc2f823a79d1e07ed052ab7b8a0280640aa7b95de42/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355", size = 378519, upload-time = "2026-06-18T16:13:46.998Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4d/fa006060ffa1011d32bfae826fe766fe73e02982183601633b7121058ab3/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c", size = 419815, upload-time = "2026-06-18T16:13:48.205Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/aab6c946570496b78e67804721f3d5e2d62a93081b9b37df77764ef56347/msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1", size = 70914, upload-time = "2026-06-18T16:13:49.385Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/e608956488a2af014cfe6e3d665e090b8ee42aa14b07f8f95b8880d66b09/msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2", size = 77999, upload-time = "2026-06-18T16:13:50.467Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8a/27e2e57055176e366a46b85d02d68e7a5bcfbdd8474c9706375d965f24d3/msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107", size = 71160, upload-time = "2026-06-18T16:13:51.498Z" }, +] + +[[package]] +name = "msgspec" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/60/f79b9b013a16fa3a58350c9295ddc6789f2e335f36ea61ed10a21b215364/msgspec-0.21.1.tar.gz", hash = "sha256:2313508e394b0d208f8f56892ca9b2799e2561329de9763b19619595a6c0f72c", size = 319193, upload-time = "2026-04-12T21:44:50.394Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/38/d591d9f66d43d897ecbd249f2833665823d19c8b043f16619bc8343e23df/msgspec-0.21.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72d9cd03241b8b2edb2e12dcc66c500fa480d8cbd71a8bac105809d468882064", size = 195172, upload-time = "2026-04-12T21:43:45.062Z" }, + { url = "https://files.pythonhosted.org/packages/69/1a/6899188b5982ec1324e0c629b7801eed2db987f6634fab58abd9fc82d317/msgspec-0.21.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed2ab278200e743a1d2610a4e0c8fc74f6cecb8548544cdec43f927bd9265238", size = 188316, upload-time = "2026-04-12T21:43:46.641Z" }, + { url = "https://files.pythonhosted.org/packages/9e/95/7e591b4fa11fdbbf9891164473c23420a8c781ef553295abe416bf335f42/msgspec-0.21.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd677e3001fdfed9186de72eab434da2976303cd5eb9550921d3d0c3e3e168ce", size = 216565, upload-time = "2026-04-12T21:43:48.081Z" }, + { url = "https://files.pythonhosted.org/packages/19/86/714feeaf3b84cf2027235681725593840153dedd2868578f9f2715e296bb/msgspec-0.21.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f667b90b37fad734a91671abd68e0d7f4d066862771b87e91c53996dcb7a9027", size = 222689, upload-time = "2026-04-12T21:43:49.385Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b9/4384243e814f2579e5205e17d170b9c1a30121afd1393298d904817a7fa7/msgspec-0.21.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:49880fd20fdbcfe1b793f07dd83f12572bab679c9800352c8b2240289aa46a06", size = 222343, upload-time = "2026-04-12T21:43:50.612Z" }, + { url = "https://files.pythonhosted.org/packages/04/01/4b227d9c4057346271043632bad41979cf8c3dca372e41bb1f7d546395b2/msgspec-0.21.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae0162e22849a5e91eaad907766525107523b0daea3df267a9fcb5ba4e0936ae", size = 225607, upload-time = "2026-04-12T21:43:52.129Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/27021d1c3e5da837743092a7b7a5e8818397e1f4c05ee8b068bd7d1fd78a/msgspec-0.21.1-cp310-cp310-win_amd64.whl", hash = "sha256:f041a2279f31e3a53319005e4d60ba77c085cfcbe394cdc7ce803c2d01fe9449", size = 188392, upload-time = "2026-04-12T21:43:53.384Z" }, + { url = "https://files.pythonhosted.org/packages/80/2b/daf7a8d6d7cf00e0dcd0439178b284ade701234abdcadf3385601da04fbd/msgspec-0.21.1-cp310-cp310-win_arm64.whl", hash = "sha256:1bf17cbd7b28a5dffc7e764c654eed8ccde5e0f1de7970628608304640d4ce4e", size = 174191, upload-time = "2026-04-12T21:43:54.6Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7f/bbc4e74cd33d316b75541149e4d35b163b63bce066530ae185a2ec3b5bfc/msgspec-0.21.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b504b6e7f7a22a24b27232b73034421692147865162daaec9f3bf62439007c87", size = 193131, upload-time = "2026-04-12T21:43:56.094Z" }, + { url = "https://files.pythonhosted.org/packages/c1/60/504886af1aaf854112663b842d5eea9a15d9588f9bf7d0d2df736424b84d/msgspec-0.21.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4692b7c1609155708c4418f88e92f63c13fdf08aa095c84bae82bad75b53389b", size = 186597, upload-time = "2026-04-12T21:43:57.242Z" }, + { url = "https://files.pythonhosted.org/packages/fa/54/d24ddeaa65b5278c9e67f48ce3c17a9831e8f3722f3c8322ee120aca22ef/msgspec-0.21.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3124010b3815451494c85ff345e693cb9fe5889cfcbbef39ed8622e0e72319c", size = 215158, upload-time = "2026-04-12T21:43:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/9f/75/bb79c8b89a93ae23cd33c0d802373f16feaf9633f05d8af77091350dda0a/msgspec-0.21.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6badc03b9725352219cca017bfe71c61f2fbd0fb5982b410ac17c97c213deb30", size = 219856, upload-time = "2026-04-12T21:44:00.015Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9c/c5ca26b46f0ebbd3a6683695ef89396712cb9e4199fd1f0bc1dd968216b1/msgspec-0.21.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5d2d4116ebe3035a78d9ec76e99a9d64e5fa6d44fe61a9c5de7fd1acf54bcc69", size = 220314, upload-time = "2026-04-12T21:44:01.548Z" }, + { url = "https://files.pythonhosted.org/packages/c8/31/645a351c4285dce40ed6755c3dcc0aa648e26dacb20a98018fe2cce5e87b/msgspec-0.21.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0d1009f6715f5bff3b54d4ff5c7428ad96197e0534e1645b8e9b955890c84664", size = 223215, upload-time = "2026-04-12T21:44:02.884Z" }, + { url = "https://files.pythonhosted.org/packages/09/af/8bf15736a6dd3cb4f90c5467f6dc39197d2daaf10754490cdc0aa17b7312/msgspec-0.21.1-cp311-cp311-win_amd64.whl", hash = "sha256:c6faffe5bb644ec884052679af4dfd776d4b5ca90e4a7ec7e7e319e4e6b93a6e", size = 188554, upload-time = "2026-04-12T21:44:04.151Z" }, + { url = "https://files.pythonhosted.org/packages/ef/29/cc7db3a165b62d16e64a83f82eccb79655055cb5bc1f60459a6f9d7c82f2/msgspec-0.21.1-cp311-cp311-win_arm64.whl", hash = "sha256:ee9e3f11fa94603f7d673bf795cfa31b549c4a2c723bc39b45beb1e7f5a3fb99", size = 174517, upload-time = "2026-04-12T21:44:05.66Z" }, + { url = "https://files.pythonhosted.org/packages/6e/cf/317224852c00248c620a9bcf4b26e2e4ab8afd752f18d2a6ef73ebd423b6/msgspec-0.21.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4248cf0b6129b7d230eacd493c17cc2d4f3989f3bb7f633a928a85b7dcfa251", size = 196188, upload-time = "2026-04-12T21:44:07.181Z" }, + { url = "https://files.pythonhosted.org/packages/6d/81/074612945c0666078f7366f40000013de9f6ba687491d450df699bceebc9/msgspec-0.21.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5102c7e9b3acff82178449b85006d96310e690291bb1ea0142f1b24bcb8aabcb", size = 188473, upload-time = "2026-04-12T21:44:08.736Z" }, + { url = "https://files.pythonhosted.org/packages/8a/37/655101799590bcc5fddb2bd3fe0e6194e816c2d1da7c361725f5eb89a910/msgspec-0.21.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:846758412e9518252b2ac9bffd6f0e54d9ff614f5f9488df7749f81ff5c80920", size = 218871, upload-time = "2026-04-12T21:44:09.917Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d1/d4cd9fe89c7d400d7a18f86ccc94daa3f0927f53558846fcb60791dce5d6/msgspec-0.21.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21995e74b5c598c2e004110ad66ec7f1b8c20bf2bcf3b2de8fd9a3094422d3ff", size = 225025, upload-time = "2026-04-12T21:44:11.191Z" }, + { url = "https://files.pythonhosted.org/packages/24/bf/e20549e602b9edccadeeff98760345a416f9cce846a657e8b18e3396b212/msgspec-0.21.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6129f0cca52992e898fd5344187f7c8127b63d810b2fd73e36fca73b4c6475ee", size = 222672, upload-time = "2026-04-12T21:44:12.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/68/04d7a8f0f786545cf9b8c280c57aa6befb5977af6e884b8b54191cbe44b3/msgspec-0.21.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ef3ec2296248d1f8b9231acb051b6d471dfde8f21819e86c9adaaa9f42918521", size = 227303, upload-time = "2026-04-12T21:44:13.709Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4d/619866af2840875be408047bf9e70ceafbae6ab50660de7134ed1b25eb86/msgspec-0.21.1-cp312-cp312-win_amd64.whl", hash = "sha256:d4ab834a054c6f0cbeef6df9e7e1b33d5f1bc7b86dea1d2fd7cad003873e783d", size = 190017, upload-time = "2026-04-12T21:44:14.977Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2e/a8f9eca8fd00e097d7a9e99ba8a4685db994494448e3d4f0b7f6e9a3c0f7/msgspec-0.21.1-cp312-cp312-win_arm64.whl", hash = "sha256:628aaa35c74950a8c59da330d7e98917e1c7188f983745782027748ee4ca573e", size = 175345, upload-time = "2026-04-12T21:44:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/7e/74/f11ede02839b19ff459f88e3145df5d711626ca84da4e23520cebf819367/msgspec-0.21.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:764173717a01743f007e9f74520ed281f24672c604514f7d76c1c3a10e8edb66", size = 196176, upload-time = "2026-04-12T21:44:17.613Z" }, + { url = "https://files.pythonhosted.org/packages/bb/40/4476c1bd341418a046c4955aff632ec769315d1e3cb94e6acf86d461f9ed/msgspec-0.21.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:344c7cd0eaed1fb81d7959f99100ef71ec9b536881a376f11b9a6c4803365697", size = 188524, upload-time = "2026-04-12T21:44:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d9/9e9d7d7e5061b47540d03d640fab9b3965ba7ae49c1b2154861c8f007518/msgspec-0.21.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48943e278b3854c2f89f955ddc6f9f430d3f0784b16e47d10604ee0463cd21f5", size = 218880, upload-time = "2026-04-12T21:44:20.028Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/2bb344f34abb4b57e60c7c9c761994e0417b9718ec1460bf00c296f2a7ea/msgspec-0.21.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9aa659ebb0101b1cbc31461212b87e341d961f0ab0772aaf068a99e001ec4aa", size = 225050, upload-time = "2026-04-12T21:44:21.577Z" }, + { url = "https://files.pythonhosted.org/packages/1a/84/7c1e412f76092277bf760cef12b7979d03314d259ab5b5cafde5d0c1722d/msgspec-0.21.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7b27d1a8ead2b6f5b0c4f2d07b8be1ccfcc041c8a0e704781edebe3ae13c484", size = 222713, upload-time = "2026-04-12T21:44:22.83Z" }, + { url = "https://files.pythonhosted.org/packages/4e/27/0bba04b2b4ef05f3d068429410bc71d2cea925f1596a8f41152cccd5edb8/msgspec-0.21.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38fe93e86b61328fe544cb7fd871fad5a27c8734bfda90f65e5dbe288ae50f61", size = 227259, upload-time = "2026-04-12T21:44:24.11Z" }, + { url = "https://files.pythonhosted.org/packages/b0/2d/09574b0eea02fed2c2c1383dbaae2c7f79dc16dcd6487a886000afb5d7c4/msgspec-0.21.1-cp313-cp313-win_amd64.whl", hash = "sha256:8bc666331c35fcce05a7cd2d6221adbe0f6058f8e750711413d22793c080ac6a", size = 189857, upload-time = "2026-04-12T21:44:25.359Z" }, + { url = "https://files.pythonhosted.org/packages/46/34/105b1576ad182879914f0c821f17ee1d13abb165cb060448f96fe2aff078/msgspec-0.21.1-cp313-cp313-win_arm64.whl", hash = "sha256:42bb1241e0750c1a4346f2aa84db26c5ffd99a4eb3a954927d9f149ff2f42898", size = 175403, upload-time = "2026-04-12T21:44:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ad/86954e987d1d6a5c579e2c2e7832b65e0fff194179fdac4f581536086024/msgspec-0.21.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fab48eb45fdbfbdb2c0edfec00ffc53b6b6085beefc6b50b61e01659f9f8757f", size = 196261, upload-time = "2026-04-12T21:44:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a1/c5e46c3e42b866199365e35d11dddfd1fbd8bba4fdb3c52f965b1607ce94/msgspec-0.21.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3cb779ea0c35bc807ff941d415875c1f69ca0be91a2e907ab99a171811d86a9a", size = 188729, upload-time = "2026-04-12T21:44:28.99Z" }, + { url = "https://files.pythonhosted.org/packages/85/7d/1e29a319d678d6cb962ae5bdf32a6858ebdf38f73bc654c0e9c742a0c2c8/msgspec-0.21.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68604db36b3b4dd9bf160e436e12798a4738848144cea1aca1cb984011eb160f", size = 219866, upload-time = "2026-04-12T21:44:31.104Z" }, + { url = "https://files.pythonhosted.org/packages/25/1f/cca084ca2572810fff12ea9dbdcbe39eac048f40daf4a9077b49fcbe8cee/msgspec-0.21.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d6b9dc50948eaf65df54d2fd0ff66e6d8c32f116037209ee861810eb9b676cb", size = 224993, upload-time = "2026-04-12T21:44:32.649Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/d2120fc9d419a89a3a7c13e5b7078798c4b392a96a02a6e2b3ce43a8766c/msgspec-0.21.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:52c5e21930942302394429c5a582ce7e6b62c7f983b3760834c2ce107e0dd6df", size = 223535, upload-time = "2026-04-12T21:44:33.839Z" }, + { url = "https://files.pythonhosted.org/packages/75/17/42418b66a3ad972a89bab73dd78b79cc6282bb488a25e73c853cee7443b9/msgspec-0.21.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:abbb39d65681fa24ed394e01af3d59d869068324f900c61d06062b7fb9980f2f", size = 227222, upload-time = "2026-04-12T21:44:35.093Z" }, + { url = "https://files.pythonhosted.org/packages/c4/33/265c894268cca88ff67b144ca2b4c522fc8b9a6f1966a3640c70516e78e1/msgspec-0.21.1-cp314-cp314-win_amd64.whl", hash = "sha256:5666b1b560b97b6ec2eb3fca8a502298ebac56e13bbca1f88523538ce83d01ea", size = 193810, upload-time = "2026-04-12T21:44:36.612Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8f/a6d35f25bf1fc63c492fdd88fdce01ba0875ead48c2b91f90f33653b4131/msgspec-0.21.1-cp314-cp314-win_arm64.whl", hash = "sha256:d8b8578e4c83b14ceea4cef0d0b747e31d9330fe4b03b2b2ad4063866a178f93", size = 179125, upload-time = "2026-04-12T21:44:38.198Z" }, + { url = "https://files.pythonhosted.org/packages/c6/39/74839641e64b99d87da55af0fc472854d42b46e2183b9e2a67fe1bb2a512/msgspec-0.21.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:15f523d51c00ebad412213bfe9f06f0a50ec2b93e0c19e824a2d267cabb48ea2", size = 200171, upload-time = "2026-04-12T21:44:39.414Z" }, + { url = "https://files.pythonhosted.org/packages/70/9b/ce0cca6d2d87fcd4b6ff97600790494e64f26a2c55d61507cd2755c16193/msgspec-0.21.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e47390360583ba3d5c6cb44cf0a9f61b0a06a899d3c2c00627cedebb2e2884b", size = 192879, upload-time = "2026-04-12T21:44:40.882Z" }, + { url = "https://files.pythonhosted.org/packages/a7/08/673a7bb05e5702dc787ddd3011195b509f9867927970da59052211929987/msgspec-0.21.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f60800e6299b798142dc40b0644da77ceac5ea0568be58228417eae14135c847", size = 226281, upload-time = "2026-04-12T21:44:42.181Z" }, + { url = "https://files.pythonhosted.org/packages/7d/45/86508cf57283e9070b3c447e3ab25b792a7a0855a3ea4e0c6d111ac34c97/msgspec-0.21.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5f8e9dfcd98419cf7568808470c4317a3fb30bef0e3715b568730a2b272a20d7", size = 229863, upload-time = "2026-04-12T21:44:43.442Z" }, + { url = "https://files.pythonhosted.org/packages/2c/62/e7c9367cd08d590559faacd711edbae36840342843e669440363f33c7d36/msgspec-0.21.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92d89dfad13bd1ea640dc3e37e724ed380da1030b272bdf5ecafb983c3ad7c75", size = 230445, upload-time = "2026-04-12T21:44:44.806Z" }, + { url = "https://files.pythonhosted.org/packages/42/b4/c0f54632103846b658a10930025f4de41c8724b5e4805a5f3b395586cb7e/msgspec-0.21.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0d03867786e5d7ba25d666df4b11320c27170f4aeafcb8e3a8b0a50a4fb742ca", size = 231822, upload-time = "2026-04-12T21:44:46.343Z" }, + { url = "https://files.pythonhosted.org/packages/ea/1d/0d85cc79d0ccf5508e9c846cc66552a6a16bf92abd1dbd8362617f7b35cd/msgspec-0.21.1-cp314-cp314t-win_amd64.whl", hash = "sha256:740fbf1c9d59992ca3537d6fbe9ebbf9eaf726a65fbf31448e0ecbc710697a63", size = 206650, upload-time = "2026-04-12T21:44:47.601Z" }, + { url = "https://files.pythonhosted.org/packages/90/91/56c5d560f20e6c20e9e4f55bd0e458f7f162aa689ee350346c04c48eac0b/msgspec-0.21.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0d2cc73df6058d811a126ac3a8ad63a4dfa210c82f9cf5a004802eaf4712de90", size = 183149, upload-time = "2026-04-12T21:44:48.833Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "packageurl-python" +version = "0.17.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/d6/3b5a4e3cfaef7a53869a26ceb034d1ff5e5c27c814ce77260a96d50ab7bb/packageurl_python-0.17.6.tar.gz", hash = "sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25", size = 50618, upload-time = "2025-11-24T15:20:17.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/2f/c7277b7615a93f51b5fbc1eacfc1b75e8103370e786fd8ce2abf6e5c04ab/packageurl_python-0.17.6-py3-none-any.whl", hash = "sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9", size = 36776, upload-time = "2025-11-24T15:20:16.962Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +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 = "pip" +version = "26.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/91/47e7d486260f618783899587af63ccf7980fb60245c3e63dd4571c6b57ad/pip-26.1.2.tar.gz", hash = "sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605", size = 1840799, upload-time = "2026-05-31T17:33:58.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/95/6b5cb3461ea5673ba0995989746db58eb18b91b54dbf331e72f569540946/pip-26.1.2-py3-none-any.whl", hash = "sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab", size = 1813144, upload-time = "2026-05-31T17:33:56.772Z" }, +] + +[[package]] +name = "pip-api" +version = "0.0.34" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pip" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/f1/ee85f8c7e82bccf90a3c7aad22863cc6e20057860a1361083cd2adacb92e/pip_api-0.0.34.tar.gz", hash = "sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625", size = 123017, upload-time = "2024-07-09T20:32:30.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/f7/ebf5003e1065fd00b4cbef53bf0a65c3d3e1b599b676d5383ccb7a8b88ba/pip_api-0.0.34-py3-none-any.whl", hash = "sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb", size = 120369, upload-time = "2024-07-09T20:32:29.099Z" }, +] + +[[package]] +name = "pip-audit" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachecontrol", extra = ["filecache"] }, + { name = "cyclonedx-python-lib" }, + { name = "packaging" }, + { name = "pip-api" }, + { name = "pip-requirements-parser" }, + { name = "platformdirs" }, + { name = "requests" }, + { name = "rich" }, + { name = "tomli" }, + { name = "tomli-w" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/a4/f21d5f0a0edabcbce31560b73c7c5a6f72ae87af4236fd1069c8f59a353d/pip_audit-2.10.1.tar.gz", hash = "sha256:1eb4565d19ebe5d48996f4b770b4d2b32887e12cb12cfa637f1a064011b55ffc", size = 54275, upload-time = "2026-06-10T22:17:01.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/a7/b0c504148114047bd1bc9d97447453c6850ca176bb2f3c0038835994e8b7/pip_audit-2.10.1-py3-none-any.whl", hash = "sha256:99ef3f600a317c1945f1e89e227ef26e1c2d618429b8bd3fa6f4f7c440c4611a", size = 62023, upload-time = "2026-06-10T22:17:00.309Z" }, +] + +[[package]] +name = "pip-requirements-parser" +version = "32.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/2a/63b574101850e7f7b306ddbdb02cb294380d37948140eecd468fae392b54/pip-requirements-parser-32.0.1.tar.gz", hash = "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3", size = 209359, upload-time = "2022-12-21T15:25:22.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl", hash = "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", size = 35648, upload-time = "2022-12-21T15:25:21.046Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +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 = "py" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/ff/fec109ceb715d2a6b4c4a85a61af3b40c723a961e8828319fbcb15b868dc/py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", size = 207796, upload-time = "2021-11-04T17:17:01.377Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", size = 98708, upload-time = "2021-11-04T17:17:00.152Z" }, +] + +[[package]] +name = "py-serializable" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "defusedxml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/21/d250cfca8ff30c2e5a7447bc13861541126ce9bd4426cd5d0c9f08b5547d/py_serializable-2.1.0.tar.gz", hash = "sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103", size = 52368, upload-time = "2025-07-21T09:56:48.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304", size = 23045, upload-time = "2025-07-21T09:56:46.848Z" }, +] + +[[package]] +name = "pycrockford-msgspec" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "msgspec" }, +] + +[package.optional-dependencies] +dev = [ + { name = "maturin" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.dev-dependencies] +dev = [ + { name = "interrogate" }, + { name = "pip-audit" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-timeout" }, + { name = "pytest-xdist" }, + { name = "ruff" }, + { name = "ty" }, +] + +[package.metadata] +requires-dist = [ + { name = "maturin", marker = "extra == 'dev'" }, + { name = "msgspec", specifier = ">=0.19" }, + { name = "pyright", marker = "extra == 'dev'" }, + { name = "pytest", marker = "extra == 'dev'" }, + { name = "ruff", marker = "extra == 'dev'" }, +] +provides-extras = ["dev"] + +[package.metadata.requires-dev] +dev = [ + { name = "interrogate" }, + { name = "pip-audit" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-timeout" }, + { name = "pytest-xdist" }, + { name = "ruff" }, + { name = "ty" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.411" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +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/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[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 = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, + { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, + { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, + { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, + { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "tabulate" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, +] + +[[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" }, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + +[[package]] +name = "ty" +version = "0.0.56" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/55/07/fb29aea5235b0aa8ecfc4d1cc6ddf9fba8b863d67d96c6d345694d644c43/ty-0.0.56.tar.gz", hash = "sha256:84d114dc3796361c0fc72945016eabd74d46b9ee64f198cb0e485719704681e5", size = 6050123, upload-time = "2026-07-01T16:44:56.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/48/bce79e7ca5c1cc529d3e0d37ddd1121aea4b68a4f749974ad1cc77161871/ty-0.0.56-py3-none-linux_armv6l.whl", hash = "sha256:186d4a53e15747c947e1ec3d7eec8e345d8e40a1ca10e634c585db52497e87dd", size = 11643066, upload-time = "2026-07-01T16:44:18.374Z" }, + { url = "https://files.pythonhosted.org/packages/80/d1/22555d8a1d719661f10050f3865d877bbf497da908961c75fe22217dd18a/ty-0.0.56-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aae1a980fd9535da0469b7ba2b2e1b54a907743a5e0f442dd57eee9f5bfd034c", size = 11407487, upload-time = "2026-07-01T16:44:20.956Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2d/b3b7a74ce8bc59ef48843ad80179bb0d9598bbd6cfc0d11d519bdf6b1352/ty-0.0.56-py3-none-macosx_11_0_arm64.whl", hash = "sha256:afd3058c0a6c5f241e814734f133008c93ee805f61c9cf4ce7412b8822b5d9ad", size = 10962270, upload-time = "2026-07-01T16:44:22.959Z" }, + { url = "https://files.pythonhosted.org/packages/64/ac/6c2fd7de0304a8a7218a756af74f7e62a5e8540fdb175e0a869e51042345/ty-0.0.56-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:058b52f7a823ac13aae3cae30809dd6b5145794b64d8478f9ef38c75d79b4483", size = 11471406, upload-time = "2026-07-01T16:44:25.327Z" }, + { url = "https://files.pythonhosted.org/packages/50/b6/11d861156861c03c7726b74558f9a0e0092661aff83a4fda1279df28c425/ty-0.0.56-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c66e00c1522add1f2bbdd2e45828c953b35c306b7bef03ec9169c75a63699a0", size = 11445612, upload-time = "2026-07-01T16:44:27.531Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ba/09df108582090f3c0770ec4bc8675affed60248f6793a78d909be16211d9/ty-0.0.56-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40903d71c669a30691b5a5d5728056c7877a1bd6be4f233a38883a8b28cf34d7", size = 12093889, upload-time = "2026-07-01T16:44:29.548Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f7/dbb4b4ccb69cd64c209ae55b1ab788ace8222c2bc1f6845be9e7cbedbf25/ty-0.0.56-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63fe3947fe0c46c69a7d950e6832ee70a9ec17321fefbff3d2e3c20baf9e5bd0", size = 12666337, upload-time = "2026-07-01T16:44:31.586Z" }, + { url = "https://files.pythonhosted.org/packages/86/e9/73f903fe4a3d9ea02f26f57c1eb07e3b1029ec92b0e8c2364718893440e3/ty-0.0.56-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71a0c1a72f9854532e710e119b6871ffe4542c8a65146f1f65dcd78fecd885b4", size = 12280247, upload-time = "2026-07-01T16:44:33.637Z" }, + { url = "https://files.pythonhosted.org/packages/d6/90/cebd222495832f1a00dcd321ba25f3cab804221a4991b992c2178bec68ee/ty-0.0.56-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70d1665596494e24d8ebd198438872b5a56ec3cae5f2bcf6c673be797acc4e3c", size = 11991107, upload-time = "2026-07-01T16:44:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/b7/07/8f7337a07250f42d975cdb6decf47fc5b421e6c7da5e3e7be1e85f63a7e5/ty-0.0.56-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:778f99e51558afc1dbbe48ee38ab6aae7b31390ed8c1a1ef1499b295e9f1e82f", size = 12298970, upload-time = "2026-07-01T16:44:38.243Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b9/a52cd59034a48f5f18c6b155cc2cc36861d874b6d0af204b12c898024c3d/ty-0.0.56-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:867bc5708e0066bb4ff6c7db524bd5deea2676c62bfe71d3303138b3be850af0", size = 11425683, upload-time = "2026-07-01T16:44:40.473Z" }, + { url = "https://files.pythonhosted.org/packages/1d/2e/48e42d33357d52eefb695c0c3fcfc96879b73668a7447d1d1e0ad774fedc/ty-0.0.56-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a6012f4189c928edb330a37deb9930f982380bd4aa7c4b8e0428eec9651c7551", size = 11469258, upload-time = "2026-07-01T16:44:42.513Z" }, + { url = "https://files.pythonhosted.org/packages/d5/01/ad1b4138be1e3fa97863af3925aa2134f17a593240c35dc38c3429fb5ad1/ty-0.0.56-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8ee83de1a7ff4cc32837ec06134ce391d441bc5b35ecd8d3cfe053f120f3e4c1", size = 11758736, upload-time = "2026-07-01T16:44:44.567Z" }, + { url = "https://files.pythonhosted.org/packages/09/34/9d81967ff240eaa57e9249728ef7b7790747cf6d3c9a98ec86b2cfdcc8ee/ty-0.0.56-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:62619b3b0e2c6248ef30d3f0e2f2217ae9893040585be07f32324242f197cd6f", size = 12100242, upload-time = "2026-07-01T16:44:46.584Z" }, + { url = "https://files.pythonhosted.org/packages/c3/36/f51d4666d2de6cf33c1f3a1fcc4bb6b70b197dd6ceaa491eef71d78fe8e8/ty-0.0.56-py3-none-win32.whl", hash = "sha256:b30687bb5cd9729d34c889a289edf32770388d9bb05243e534e723fb45e0381b", size = 11093759, upload-time = "2026-07-01T16:44:49.171Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b4/8fb5d4acfa4afb152245b20fa263069a7547bd1f8e4bfca4eda280c897d7/ty-0.0.56-py3-none-win_amd64.whl", hash = "sha256:ad4c8c47b6f4e3f9ed3fc0b1a5d650088d229e17dd8f63c1826d6bbe94cc3235", size = 12100327, upload-time = "2026-07-01T16:44:51.26Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fc/6a183e71edde90d0c35c2303f23f7a45b6891d1a2c45daf7b8f869831e19/ty-0.0.56-py3-none-win_arm64.whl", hash = "sha256:57538f273d444a5f1293fa7860e967178afe3917611fc5eff16b64e1204fe0d6", size = 11538780, upload-time = "2026-07-01T16:44:53.8Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[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" }, +]