diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 170dbbd..8ca839c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,11 @@ on: jobs: nix-tests: + name: Testing on Python ${{matrix.python-version}} runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10","3.11","3.12","3.13"] steps: - uses: actions/checkout@v4 - uses: cachix/install-nix-action@v27 @@ -17,30 +21,31 @@ jobs: nix_path: nixpkgs=channel:nixos-25.05 extra_nix_config: | experimental-features = nix-command flakes - - name: Run tests via Nix - run: nix develop --command just test + - name: Build Rust module and run tests via Nix + run: nix develop --command bash -lc 'just venv ${{matrix.python-version}} dev test' - rust-tests: - name: Rust module test on ${{ matrix.os }} (Python ${{ matrix.python-version }}) - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["10", "11", "12", "13"] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: 3.${{ matrix.python-version }} - - uses: astral-sh/setup-uv@v4 - - uses: messense/maturin-action@v1 - with: - command: build - args: --interpreter python3.${{ matrix.python-version }} -m crates/codetracer-python-recorder/Cargo.toml --release - - name: Install and test built wheel with uv (pytest) - shell: bash - run: | - v=${{matrix.python-version}} - file=(crates/codetracer-python-recorder/target/wheels/*.whl) - file="${file[0]}" - uv run -p python3.$v --with "${file}" --with pytest -- python -m pytest crates/codetracer-python-recorder/test -q + # rust-tests: + # name: Rust module test on ${{ matrix.os }} (Python ${{ matrix.python-version }}) + # runs-on: ${{ matrix.os }} + # strategy: + # matrix: + # os: [ubuntu-latest, macos-latest, windows-latest] + # python-version: ["10", "11", "12", "13"] + # steps: + # - uses: actions/checkout@v4 + # - uses: actions/setup-python@v5 + # with: + # python-version: 3.${{ matrix.python-version }} + # - uses: astral-sh/setup-uv@v4 + # - uses: messense/maturin-action@v1 + # with: + # command: build + # args: --interpreter python3.${{ matrix.python-version }} -m crates/codetracer-python-recorder/Cargo.toml --release + # - name: Install and test built wheel with uv (pytest) + # shell: bash + # run: | + # v=${{matrix.python-version}} + # file=(crates/codetracer-python-recorder/target/wheels/*.whl) + # file="${file[0]}" + # uv run -p python3.$v --with "${file}" --with pytest -- \ + # python -m pytest crates/codetracer-python-recorder/test tests/test_codetracer_api.py -q diff --git a/Justfile b/Justfile index 464ae9a..ce4a73b 100644 --- a/Justfile +++ b/Justfile @@ -1,48 +1,58 @@ +default: + @just --list + # Development helpers for the monorepo +# Python version used for development +PYTHON_DEFAULT_VERSION := "3.13" + # Python versions used for multi-version testing/building with uv PY_VERSIONS := "3.10 3.11 3.12 3.13" PY_SHORT_VERSIONS := "10 11 12 13" + # Print toolchain versions to verify the dev environment env: + uv --version python3 --version cargo --version rustc --version maturin --version -# Create a local virtualenv for Python tooling -venv: - test -d .venv || python3 -m venv .venv +clean: + rm -rf .venv **/__pycache__ **/*.pyc **/*.pyo **/.pytest_cache + rm -rf codetracer-python-recorder/target codetracer-python-recorder/**/*.so + -# Build and develop-install the Rust-backed Python module -build-rust: - test -d .venv || python3 -m venv .venv - VIRTUAL_ENV=.venv maturin develop -m crates/codetracer-python-recorder/Cargo.toml +# Create a clean local virtualenv for Python tooling (without editable packages installed) +venv version=PYTHON_DEFAULT_VERSION: + uv sync -p {{version}} -# Smoke test the Rust module after build -smoke-rust: - .venv/bin/python -m pip install -U pip pytest - .venv/bin/python -m pytest crates/codetracer-python-recorder/test -q +# Build the module in dev mode +dev: + uv run --directory codetracer-python-recorder maturin develop --uv -# Run the Python test suite for the pure-Python recorder +# Run unit tests of dev build test: - python3 -m unittest discover -v + uv run --group dev --group test pytest -# Run the test suite across multiple Python versions using uv -test-uv-all: - uv python install {{PY_VERSIONS}} - for v in {{PY_VERSIONS}}; do uv run -p "$v" -m unittest discover -v; done +# Run tests only on the pure recorder +test-pure: + uv run --group dev --group test pytest codetracer-pure-python-recorder + +# Build the module in release mode +build: + just venv \ + uv run --directory codetracer-python-recorder maturin build --release # Build wheels for all target Python versions with maturin -build-rust-uv-all: - for v in {{PY_VERSIONS}}; do \ - maturin build --interpreter "python$v" -m crates/codetracer-python-recorder/Cargo.toml --release; \ - done +build-all: + just venv + uv run --directory codetracer-python-recorder maturin build --release --interpreter {{PY_VERSIONS}} # Smoke the built Rust wheels across versions using uv -smoke-rust-uv-all: +test-all: for v in {{PY_SHORT_VERSIONS}}; do \ - file=(crates/codetracer-python-recorder/target/wheels/codetracer_python_recorder-*-cp3$v-cp3$v-*.whl); \ + file=(codetracer-python-recorder/target/wheels/codetracer_python_recorder-*-cp3$v-cp3$v-*.whl); \ file="${file[0]}"; \ - uv run -p "python3.$v" --with "${file}" --with pytest -- python -m pytest crates/codetracer-python-recorder/test -q; \ + uv run -p "python3.$v" --with "${file}" --with pytest -- pytest -q; \ done diff --git a/codetracer-pure-python-recorder/pyproject.toml b/codetracer-pure-python-recorder/pyproject.toml new file mode 100644 index 0000000..558ea65 --- /dev/null +++ b/codetracer-pure-python-recorder/pyproject.toml @@ -0,0 +1,28 @@ +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + +[project] +name = "codetracer-pure-python-recorder" +version = "0.1.0" +description = "Pure-Python prototype recorder producing CodeTracer traces" +authors = [{name = "Metacraft Labs Ltd"}] +license = {text = "MIT"} +readme = "README.md" +requires-python = ">=3.8" +classifiers = [ + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", +] + +[tool.setuptools] +py-modules = ["trace"] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] + +[project.scripts] +codetracer-record = "codetracer_pure_python_recorder.cli:main" +codetracer-record-pure = "codetracer_pure_python_recorder.cli:main" diff --git a/src/codetracer_pure_python_recorder/__init__.py b/codetracer-pure-python-recorder/src/codetracer_pure_python_recorder/__init__.py similarity index 100% rename from src/codetracer_pure_python_recorder/__init__.py rename to codetracer-pure-python-recorder/src/codetracer_pure_python_recorder/__init__.py diff --git a/src/codetracer_pure_python_recorder/cli.py b/codetracer-pure-python-recorder/src/codetracer_pure_python_recorder/cli.py similarity index 100% rename from src/codetracer_pure_python_recorder/cli.py rename to codetracer-pure-python-recorder/src/codetracer_pure_python_recorder/cli.py diff --git a/src/trace.py b/codetracer-pure-python-recorder/src/trace.py similarity index 100% rename from src/trace.py rename to codetracer-pure-python-recorder/src/trace.py diff --git a/tests/__init__.py b/codetracer-pure-python-recorder/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to codetracer-pure-python-recorder/tests/__init__.py diff --git a/tests/fixtures/array_sum.json b/codetracer-pure-python-recorder/tests/fixtures/array_sum.json similarity index 100% rename from tests/fixtures/array_sum.json rename to codetracer-pure-python-recorder/tests/fixtures/array_sum.json diff --git a/tests/fixtures/calc.json b/codetracer-pure-python-recorder/tests/fixtures/calc.json similarity index 100% rename from tests/fixtures/calc.json rename to codetracer-pure-python-recorder/tests/fixtures/calc.json diff --git a/tests/programs/array_sum.py b/codetracer-pure-python-recorder/tests/programs/array_sum.py similarity index 100% rename from tests/programs/array_sum.py rename to codetracer-pure-python-recorder/tests/programs/array_sum.py diff --git a/tests/programs/calc.py b/codetracer-pure-python-recorder/tests/programs/calc.py similarity index 100% rename from tests/programs/calc.py rename to codetracer-pure-python-recorder/tests/programs/calc.py diff --git a/tests/test_trace.py b/codetracer-pure-python-recorder/tests/test_trace.py similarity index 100% rename from tests/test_trace.py rename to codetracer-pure-python-recorder/tests/test_trace.py diff --git a/crates/codetracer-python-recorder/Cargo.lock b/codetracer-python-recorder/Cargo.lock similarity index 99% rename from crates/codetracer-python-recorder/Cargo.lock rename to codetracer-python-recorder/Cargo.lock index ff1e5ba..d688a23 100644 --- a/crates/codetracer-python-recorder/Cargo.lock +++ b/codetracer-python-recorder/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "autocfg" diff --git a/crates/codetracer-python-recorder/Cargo.toml b/codetracer-python-recorder/Cargo.toml similarity index 100% rename from crates/codetracer-python-recorder/Cargo.toml rename to codetracer-python-recorder/Cargo.toml diff --git a/codetracer-python-recorder/codetracer_python_recorder/.gitignore b/codetracer-python-recorder/codetracer_python_recorder/.gitignore new file mode 100644 index 0000000..f1fe8d1 --- /dev/null +++ b/codetracer-python-recorder/codetracer_python_recorder/.gitignore @@ -0,0 +1 @@ +*.so \ No newline at end of file diff --git a/codetracer-python-recorder/codetracer_python_recorder/__init__.py b/codetracer-python-recorder/codetracer_python_recorder/__init__.py new file mode 100644 index 0000000..c4ea7f1 --- /dev/null +++ b/codetracer-python-recorder/codetracer_python_recorder/__init__.py @@ -0,0 +1,13 @@ +"""High-level tracing API built on a Rust backend. + +This module exposes a minimal interface for starting and stopping +runtime traces. The heavy lifting is delegated to the +`codetracer_python_recorder` Rust extension which will eventually hook +into `runtime_tracing` and `sys.monitoring`. For now the Rust side only +maintains placeholder state and performs no actual tracing. +""" + +from .api import * + +__all__ = api.__all__ + diff --git a/codetracer/__init__.py b/codetracer-python-recorder/codetracer_python_recorder/api.py similarity index 98% rename from codetracer/__init__.py rename to codetracer-python-recorder/codetracer_python_recorder/api.py index 99a30ec..1b598e4 100644 --- a/codetracer/__init__.py +++ b/codetracer-python-recorder/codetracer_python_recorder/api.py @@ -13,7 +13,7 @@ from pathlib import Path from typing import Iterable, Iterator, Optional -from codetracer_python_recorder import ( +from .codetracer_python_recorder import ( flush_tracing as _flush_backend, is_tracing as _is_tracing_backend, start_tracing as _start_backend, diff --git a/crates/codetracer-python-recorder/pyproject.toml b/codetracer-python-recorder/pyproject.toml similarity index 87% rename from crates/codetracer-python-recorder/pyproject.toml rename to codetracer-python-recorder/pyproject.toml index b5a79fd..5bfc8ee 100644 --- a/crates/codetracer-python-recorder/pyproject.toml +++ b/codetracer-python-recorder/pyproject.toml @@ -1,11 +1,7 @@ -[build-system] -requires = ["maturin>=1.5,<2"] -build-backend = "maturin" - [project] name = "codetracer-python-recorder" version = "0.1.0" -description = "Rust-backed Python module for CodeTracer recording (PyO3)" +description = "Low-level Rust-backed Python module for CodeTracer recording (PyO3)" authors = [{name = "Metacraft Labs Ltd"}] license = {text = "MIT"} requires-python = ">=3.8" @@ -20,3 +16,7 @@ classifiers = [ # Build the PyO3 extension module bindings = "pyo3" # Use the library name as the Python import name: codetracer_python_recorder + +[build-system] +requires = ["maturin>=1.5,<2"] +build-backend = "maturin" diff --git a/crates/codetracer-python-recorder/src/lib.rs b/codetracer-python-recorder/src/lib.rs similarity index 66% rename from crates/codetracer-python-recorder/src/lib.rs rename to codetracer-python-recorder/src/lib.rs index a5c1ff2..92782b5 100644 --- a/crates/codetracer-python-recorder/src/lib.rs +++ b/codetracer-python-recorder/src/lib.rs @@ -40,19 +40,12 @@ fn flush_tracing() -> PyResult<()> { Ok(()) } -/// Trivial function kept for smoke tests verifying the module builds. -#[pyfunction] -fn hello() -> PyResult { - Ok("Hello from codetracer-python-recorder (Rust)".to_string()) -} - /// Python module definition. #[pymodule] -fn codetracer_python_recorder(_py: Python<'_>, m: Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(start_tracing, &m)?)?; - m.add_function(wrap_pyfunction!(stop_tracing, &m)?)?; - m.add_function(wrap_pyfunction!(is_tracing, &m)?)?; - m.add_function(wrap_pyfunction!(flush_tracing, &m)?)?; - m.add_function(wrap_pyfunction!(hello, &m)?)?; +fn codetracer_python_recorder(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(start_tracing, m)?)?; + m.add_function(wrap_pyfunction!(stop_tracing, m)?)?; + m.add_function(wrap_pyfunction!(is_tracing, m)?)?; + m.add_function(wrap_pyfunction!(flush_tracing, m)?)?; Ok(()) } diff --git a/crates/codetracer-python-recorder/test/smoke.py b/codetracer-python-recorder/test/smoke.py similarity index 100% rename from crates/codetracer-python-recorder/test/smoke.py rename to codetracer-python-recorder/test/smoke.py diff --git a/tests/test_codetracer_api.py b/codetracer-python-recorder/test/test_codetracer_api.py similarity index 90% rename from tests/test_codetracer_api.py rename to codetracer-python-recorder/test/test_codetracer_api.py index 7713adc..353caee 100644 --- a/tests/test_codetracer_api.py +++ b/codetracer-python-recorder/test/test_codetracer_api.py @@ -5,7 +5,7 @@ import unittest from pathlib import Path -import codetracer +import codetracer_python_recorder as codetracer class TracingApiTests(unittest.TestCase): @@ -34,7 +34,7 @@ def test_context_manager(self) -> None: self.assertFalse(codetracer.is_tracing()) def test_environment_auto_start(self) -> None: - script = "import codetracer, sys; sys.stdout.write(str(codetracer.is_tracing()))" + script = "import codetracer_python_recorder as codetracer, sys; sys.stdout.write(str(codetracer.is_tracing()))" with tempfile.TemporaryDirectory() as tmpdir: env = os.environ.copy() env["CODETRACER_TRACE"] = str(Path(tmpdir) / "trace.bin") diff --git a/codetracer-python-recorder/test/test_smoke.py b/codetracer-python-recorder/test/test_smoke.py new file mode 100644 index 0000000..4de501e --- /dev/null +++ b/codetracer-python-recorder/test/test_smoke.py @@ -0,0 +1,5 @@ +import codetracer_python_recorder as m + +def test_is_tracing_returns_false() -> None: + assert m.is_tracing() == False + diff --git a/crates/codetracer-python-recorder/test/test_smoke.py b/crates/codetracer-python-recorder/test/test_smoke.py deleted file mode 100644 index aab35b5..0000000 --- a/crates/codetracer-python-recorder/test/test_smoke.py +++ /dev/null @@ -1,5 +0,0 @@ -import codetracer_python_recorder as m - - -def test_hello_returns_expected_string() -> None: - assert m.hello() == "Hello from codetracer-python-recorder (Rust)" diff --git a/crates/codetracer-python-recorder/uv.lock b/crates/codetracer-python-recorder/uv.lock deleted file mode 100644 index 52f9c5d..0000000 --- a/crates/codetracer-python-recorder/uv.lock +++ /dev/null @@ -1,8 +0,0 @@ -version = 1 -revision = 2 -requires-python = ">=3.8" - -[[package]] -name = "codetracer-python-recorder" -version = "0.1.0" -source = { editable = "." } diff --git a/flake.nix b/flake.nix index 20d149d..3938f41 100644 --- a/flake.nix +++ b/flake.nix @@ -25,7 +25,6 @@ ruff black mypy - python3Packages.pytest # Rust toolchain for the Rust-backed Python module cargo @@ -38,6 +37,12 @@ uv pkg-config ]; + + shellHook = '' + # When having more than one python version in the shell this variable breaks `maturin build` + # because it always leads to having SOABI be the one from the highest version + unset PYTHONPATH + ''; }; }); }; diff --git a/pyproject.toml b/pyproject.toml index 558ea65..9798331 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,28 +1,34 @@ -[build-system] -requires = ["setuptools>=61"] -build-backend = "setuptools.build_meta" - [project] -name = "codetracer-pure-python-recorder" +name = "codetracer-python-recorders" version = "0.1.0" -description = "Pure-Python prototype recorder producing CodeTracer traces" -authors = [{name = "Metacraft Labs Ltd"}] -license = {text = "MIT"} -readme = "README.md" requires-python = ">=3.8" -classifiers = [ - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", +dependencies = [] + +[tool.uv] +managed = true + +[tool.uv.workspace] +members = [ + "codetracer-python-recorder", + "codetracer-pure-python-recorder", ] -[tool.setuptools] -py-modules = ["trace"] -package-dir = {"" = "src"} +[tool.uv.sources] +codetracer-python-recorder = {workspace= true} +codetracer-pure-python-recorder = {workspace= true} + +[tool.ruff] +line-length = 100 -[tool.setuptools.packages.find] -where = ["src"] +[tool.pyright] +typeCheckingMode = "basic" -[project.scripts] -codetracer-record = "codetracer_pure_python_recorder.cli:main" -codetracer-record-pure = "codetracer_pure_python_recorder.cli:main" +[dependency-groups] +dev = [ + "pytest>=8.3.5", +] + +test = [ + "codetracer-python-recorder", + "codetracer-pure-python-recorder" +] diff --git a/uv.lock b/uv.lock index e4956c6..dfbc830 100644 --- a/uv.lock +++ b/uv.lock @@ -1,8 +1,225 @@ version = 1 revision = 2 requires-python = ">=3.8" +resolution-markers = [ + "python_full_version >= '3.9'", + "python_full_version < '3.9'", +] + +[manifest] +members = [ + "codetracer-pure-python-recorder", + "codetracer-python-recorder", + "codetracer-python-recorders", +] [[package]] name = "codetracer-pure-python-recorder" version = "0.1.0" -source = { editable = "." } +source = { editable = "codetracer-pure-python-recorder" } + +[[package]] +name = "codetracer-python-recorder" +version = "0.1.0" +source = { editable = "codetracer-python-recorder" } + +[[package]] +name = "codetracer-python-recorders" +version = "0.1.0" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "pytest", version = "8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +test = [ + { name = "codetracer-pure-python-recorder" }, + { name = "codetracer-python-recorder" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.3.5" }] +test = [ + { name = "codetracer-pure-python-recorder", editable = "codetracer-pure-python-recorder" }, + { name = "codetracer-python-recorder", editable = "codetracer-python-recorder" }, +] + +[[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 = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] +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 = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.9'" }, + { name = "iniconfig", marker = "python_full_version < '3.9'" }, + { name = "packaging", marker = "python_full_version < '3.9'" }, + { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.9' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "iniconfig", marker = "python_full_version >= '3.9'" }, + { name = "packaging", marker = "python_full_version >= '3.9'" }, + { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "pygments", marker = "python_full_version >= '3.9'" }, + { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +]