diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d70ecd2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI + +on: + pull_request: + branches: + - main + push: + branches: + - main + +jobs: + check: + name: Black and pytest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install uv + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + with: + python-version: "3.14" + enable-cache: true + - name: Sync + run: uv sync --locked --group dev + - name: Black + run: uv run black --check . + - name: Tests + run: uv run pytest diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 26c76c0..acd02ba 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,23 +2,26 @@ name: Publish CwMath Package on: push: - branches: - - 'main' + tags: + - "v*" jobs: build-and-publish: name: Build and Publish - runs-on: 'ubuntu-latest' + runs-on: ubuntu-latest environment: - name: pypi - url: https://pypi.org/p/cwmath + name: pypi + url: https://pypi.org/p/cwmath permissions: id-token: write + contents: read steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - run: pip install build - - run: python -m build - - uses: pypa/gh-action-pypi-publish@release/v1 + - name: Install uv + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 + - name: Install Python + run: uv python install 3.14 + - name: Build + run: uv build + - name: Publish + run: uv publish diff --git a/.github/workflows/test_publish.yml b/.github/workflows/test_publish.yml deleted file mode 100644 index 0229ab4..0000000 --- a/.github/workflows/test_publish.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Test Publish CwMath Package - -on: - push: - branches-ignore: - - 'main' - -jobs: - build-and-publish: - name: Test Build and Publish - runs-on: 'ubuntu-latest' - environment: - name: testpypi - url: https://test.pypi.org/p/cwmath - permissions: - id-token: write - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - run: pip install build - - run: python -m build - - uses: pypa/gh-action-pypi-publish@release/v1 - with: - repository-url: https://test.pypi.org/legacy/ - skip-existing: true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 2973dd6..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Python Math Tests - -on: - pull_request: - branches: - - main - push: - branches: - - main - -jobs: - run-math-tests: - name: Run Math Tests - runs-on: 'ubuntu-latest' - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - run: python -m pip install --upgrade pip - - run: pip install pip-tools - - run: pip-sync - - run: python -m pytest --cov --cov-report=xml - - uses: codacy/codacy-coverage-reporter-action@v1 - with: - project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} - coverage-reports: coverage.xml diff --git a/.gitignore b/.gitignore index 9c3bdb1..17b3df2 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,5 @@ cython_debug/ .idea/ .vscode/ + +.lp-scratch/ diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 23bed60..b06eab0 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,11 +3,14 @@ version: 2 build: os: ubuntu-lts-latest tools: - python: "3.12" + python: "3.14" mkdocs: configuration: mkdocs.yml python: - install: - - requirements: requirements.txt + install: + - method: uv + command: sync + groups: + - dev diff --git a/README.md b/README.md index 9bf245c..1fb227f 100644 --- a/README.md +++ b/README.md @@ -9,104 +9,64 @@ [![Pulls](https://img.shields.io/github/issues-pr/cwapi3d/cwmath)](https://github.com/cwapi3d/cwmath/pulls) [![GitHub](https://img.shields.io/github/license/cwapi3d/cwmath)](https://choosealicense.com/licenses/mit/) -This is the Cadwork Math Utilities library in Python. +Dependency-free geometry types for cadwork plugin authors. Frozen `Point` / `Vec` +pairs in 2D and 3D, a right-handed `Frame3`, `Plane3`, `Line3`, and projections. +Python 3.14, managed with UV. Millimetres are the implicit length unit. ## Installation -Install with pip: - ```bash - pip install cwmath +uv add cwmath ``` - -## Contributing -Contributions are always welcome! -Before contributing, please check our [contributing agreement](CONTRIBUTING.md). - -## Roadmap +From a checkout: -- Guides -- Examples -- API Documentation - -## Authors - -- [@jspaquet](https://github.com/jspaquet) -- [@Brunner246](https://github.com/Brunner246) - -## License - -[MIT](https://choosealicense.com/licenses/mit/) +```bash +uv sync --group dev +``` -# Style Guide +## Example -Please follow the style guide below when adding functions. -[Guidelines](GUIDELINES.md) +Copy this into a cadwork plugin (or cadwork IDLE). It converts a host +`point_3d`, builds a frame, drops a local point onto the XY plane, and +converts the foot back for `cwapi3d`. ```python - -# imports: -import math -import sys -from myclass import MyClass - - -# example function -def add_one(number: int) -> int: - """Increase number by one. - - Function written by John Doe. - - Args: - number: a number - - Returns: - number increased by one - """ - return number + 1 - - -# Aligned with opening delimiter. -foo = long_function_name(var_one, var_two, - var_three, var_four) - -if foo == 'blah': - do_blah_thing() -do_one() -do_two() -do_three() - +import cadwork +from cwmath import ( + Frame3, + Plane3, + Point3, + Vec3, + project_point_on_plane, + to_point_3d, +) + +origin = Point3.from_xyz(cadwork.point_3d(1000.0, 2000.0, 0.0)) +x_dir = Vec3.from_xyz(cadwork.point_3d(1.0, 0.0, 0.0)) +y_dir = Vec3.from_xyz(cadwork.point_3d(0.0, 1.0, 0.0)) +frame = Frame3.from_origin_xy(origin, x_dir, y_dir) + +local = Point3(50.0, 25.0, 10.0) +world = frame.to_world(local) +plane = Plane3.from_point_normal(origin, frame.z_axis) +foot = project_point_on_plane(world, plane) +result = to_point_3d(foot) ``` -# Examples -```python -import cadwork -import element_controller as ec -import geometry_controller as gc +`to_point_3d` imports `cadwork` only at the call site. Unit tests inject a +duck-typed factory instead of the host. -import sys +## Contributing -sys.path.append("...\\...\cwmath\\src") +Contributions are always welcome! +Before contributing, please check our [contributing agreement](CONTRIBUTING.md). -from cwmath import cwplane3d -from cwmath import cwvector3d +## Authors -element_ids = ec.get_active_identifiable_element_ids() -if len(element_ids) > 0: - point1 = gc.get_p1(*element_ids) - point2 = gc.get_p2(*element_ids) - yl = gc.get_yl(*element_ids) - plane = cwplane3d.CwPlane3d(point1,yl) - print(point1) - print(yl) - print(plane) +- [@jspaquet](https://github.com/jspaquet) +- [@Brunner246](https://github.com/Brunner246) - distance = plane.distance_to_point(cadwork.point_3d(0.,0.,0.)) - print(distance) +## License - vector1 = cwvector3d.CwVector3d.from_point_3d(point1) - vector2 = cwvector3d.CwVector3d.from_point_3d(point2) - norm =(vector2 - vector1).normalize() - print(norm) -``` +[MIT](https://choosealicense.com/licenses/mit/) diff --git a/docs/frame3.md b/docs/frame3.md new file mode 100644 index 0000000..23d203f --- /dev/null +++ b/docs/frame3.md @@ -0,0 +1,13 @@ +# Frame3 + +A frozen origin plus a right-handed orthonormal basis. Use +`from_origin_and_axes` when the axes are already unit and orthogonal, or +`from_origin_xy` to build `z = x × y` and orthonormalize. + +`to_world` / `to_local` map `Point3` with translation and `Vec3` by rotation +only. + +::: cwmath.frame3.Frame3 + options: + show_root_heading: true + show_source: true diff --git a/docs/functions.md b/docs/functions.md index 6e3ee90..32191c6 100644 --- a/docs/functions.md +++ b/docs/functions.md @@ -1,5 +1,32 @@ -# CwMath +# Functions -::: src.cwmath.cwexamples +Package-level helpers re-exported from `cwmath`. + +## Conversion + +::: cwmath.cadwork.to_point_3d + options: + show_root_heading: true + show_source: true + +## Projections + +::: cwmath.line3.project_point_on_line + options: + show_root_heading: true + show_source: true + +::: cwmath.plane3.project_point_on_plane + options: + show_root_heading: true + show_source: true + +## Tolerance + +`ABS_TOL = 1e-6`, `REL_TOL = 1e-9`. `==` on the types is exact; callers opt +into closeness. + +::: cwmath.tolerance.is_close options: - show_source: true + show_root_heading: true + show_source: true diff --git a/docs/index.md b/docs/index.md index bea3ac3..ef8cbf4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,9 +1,41 @@ # Cadwork Math Utilities -CwMath is an open source project in which Python functions are developed and made available by the community, for the community. CwMath is a subproject of the API of cadwork. +`cwmath` is a small, dependency-free geometry library for cadwork 3d plugin +authors. Frozen `Point` / `Vec` types in 2D and 3D, a right-handed `Frame3`, +`Plane3`, `Line3`, and projections of a point onto a line or a plane. + +Import the public types from the package root: + +```python +from cwmath import Point3, Vec3, Frame3, Plane3, Line3, to_point_3d +``` [Cadwork Python Documentation](https://docs.cadwork.com/projects/cwapi3dpython/en/latest/){.button-63} +## Types + +- [Point3](point3.md) / [Vec3](vec3.md) — 3D affine pair, convertible to `cadwork.point_3d` +- [Point2](point2.md) / [Vec2](vec2.md) — 2D affine pair +- [Frame3](frame3.md) — local ↔ world for points and vectors +- [Plane3](plane3.md) / [Line3](line3.md) — reference plane and infinite line +- [Functions](functions.md) — `to_point_3d`, projections, `is_close` + +Cadwork millimetres are the implicit length unit. `==` is exact IEEE equality; +use `is_close` when you want a tolerance. + +## Host conversion + +`Point3.from_xyz` / `Vec3.from_xyz` accept anything with `.x`, `.y`, `.z` +(including `cadwork.point_3d`). `to_point_3d` imports `cadwork` only at the +call site so CI can run without the host. Smoke-test the real adapter once in +cadwork IDLE: + +```python +from cwmath import Point3, to_point_3d + +to_point_3d(Point3(1, 2, 3)) # a real cadwork.point_3d +``` + ![Backup Text](img/math.jpeg "https://elink.io/p/all-about-paula-incorvati-99ec81e") ## Agreement diff --git a/docs/line3.md b/docs/line3.md new file mode 100644 index 0000000..d2bdc1b --- /dev/null +++ b/docs/line3.md @@ -0,0 +1,10 @@ +# Line3 + +A frozen infinite line: origin plus a unit direction. Build with +`from_origin_direction` or `from_points`. `project` is the closest point; +`parameter` is the corresponding `t`. + +::: cwmath.line3.Line3 + options: + show_root_heading: true + show_source: true diff --git a/docs/plane3.md b/docs/plane3.md new file mode 100644 index 0000000..3a75624 --- /dev/null +++ b/docs/plane3.md @@ -0,0 +1,10 @@ +# Plane3 + +A frozen plane from a point and a (unit) normal. Zero normal raises +`ValueError`. `project` returns the foot; `contains` uses the package +tolerance. + +::: cwmath.plane3.Plane3 + options: + show_root_heading: true + show_source: true diff --git a/docs/plane3d.md b/docs/plane3d.md deleted file mode 100644 index d74cac1..0000000 --- a/docs/plane3d.md +++ /dev/null @@ -1,6 +0,0 @@ -# CwPlane3d - -::: src.cwmath.cwplane3d - options: - show_root_heading: true - show_source: true diff --git a/docs/point2.md b/docs/point2.md new file mode 100644 index 0000000..57bfc55 --- /dev/null +++ b/docs/point2.md @@ -0,0 +1,9 @@ +# Point2 + +A frozen 2D position. Same affine rules as `Point3` in the plane. No cadwork +conversion — the host has no `point_2d`. + +::: cwmath.point2.Point2 + options: + show_root_heading: true + show_source: true diff --git a/docs/point3.md b/docs/point3.md new file mode 100644 index 0000000..2b4e0d3 --- /dev/null +++ b/docs/point3.md @@ -0,0 +1,10 @@ +# Point3 + +A frozen 3D position. Affine: `Point3 + Vec3 → Point3`, `Point3 - Point3 → Vec3`. +Adding two points raises `TypeError`. Build from a host value with +`Point3.from_xyz(...)`. + +::: cwmath.point3.Point3 + options: + show_root_heading: true + show_source: true diff --git a/docs/vec2.md b/docs/vec2.md new file mode 100644 index 0000000..a57da17 --- /dev/null +++ b/docs/vec2.md @@ -0,0 +1,9 @@ +# Vec2 + +A frozen 2D displacement. `cross` returns the signed parallelogram area (a +scalar), not a third-axis vector. + +::: cwmath.vec2.Vec2 + options: + show_root_heading: true + show_source: true diff --git a/docs/vec3.md b/docs/vec3.md new file mode 100644 index 0000000..0159b68 --- /dev/null +++ b/docs/vec3.md @@ -0,0 +1,9 @@ +# Vec3 + +A frozen 3D displacement. Dot, cross, magnitude, `normalized`, and `angle_to` +(radians). Normalizing the zero vector raises `ValueError`. + +::: cwmath.vec3.Vec3 + options: + show_root_heading: true + show_source: true diff --git a/docs/vector3d.md b/docs/vector3d.md deleted file mode 100644 index 4b20bd7..0000000 --- a/docs/vector3d.md +++ /dev/null @@ -1,6 +0,0 @@ -# CwVector3d - -::: src.cwmath.cwvector3d - options: - show_root_heading: true - show_source: true diff --git a/mkdocs.yml b/mkdocs.yml index 2981060..d6c99d7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -35,9 +35,14 @@ nav: - Contributing: contributing.md - Style Guide: style_guide.md - Reference: - - functions.md - - vector3d.md - - plane3d.md + - Point3: point3.md + - Vec3: vec3.md + - Point2: point2.md + - Vec2: vec2.md + - Frame3: frame3.md + - Plane3: plane3.md + - Line3: line3.md + - Functions: functions.md markdown_extensions: - admonition @@ -70,7 +75,13 @@ extra_css: plugins: - search - - mkdocstrings + - mkdocstrings: + handlers: + python: + paths: [src] + options: + show_source: true + show_root_heading: true extra: social: diff --git a/pyproject.toml b/pyproject.toml index b2e95b8..82e27e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,30 +1,43 @@ [project] name = "cwmath" -version = "0.0.9" +version = "1.0.0" authors = [{ name = "Cadwork", email = "it@cadwork.ca" }] -requires-python = ">= 3.10" -description = 'Cadwork Math Utilities' +requires-python = ">=3.14" +description = "Cadwork Math Utilities" readme = "README.md" license = { file = "LICENSE" } keywords = ["cadwork"] classifiers = [ - "Development Status :: 3 - Alpha", + "Development Status :: 5 - Production/Stable", "Environment :: Plugins", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: Microsoft :: Windows", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.14", "Topic :: Software Development :: Libraries", ] +dependencies = [] [project.urls] Homepage = "https://github.com/cwapi3d/cwmath" [build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" +requires = ["hatchling"] +build-backend = "hatchling.build" -[tool.setuptools] -packages = ["cwmath"] -package-dir = { "" = "src" } +[dependency-groups] +dev = [ + "black", + "mkdocs", + "mkdocs-material", + "mkdocstrings[python]", + "pytest", +] + +[tool.black] +line-length = 88 +target-version = ["py314"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/requirements.in b/requirements.in deleted file mode 100644 index f52d8d3..0000000 --- a/requirements.in +++ /dev/null @@ -1,7 +0,0 @@ -build -mkdocs -mkdocs-material[imaging] -mkdocstrings[python] -pygments -pytest -pytest-cov diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 04bdade..0000000 --- a/requirements.txt +++ /dev/null @@ -1,722 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# pip-compile --allow-unsafe --generate-hashes --strip-extras -# -babel==2.14.0 \ - --hash=sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363 \ - --hash=sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287 - # via mkdocs-material -build==1.1.1 \ - --hash=sha256:8ed0851ee76e6e38adce47e4bee3b51c771d86c64cf578d0c2245567ee200e73 \ - --hash=sha256:8eea65bb45b1aac2e734ba2cc8dad3a6d97d97901a395bd0ed3e7b46953d2a31 - # via -r requirements.in -cairocffi==1.6.1 \ - --hash=sha256:78e6bbe47357640c453d0be929fa49cd05cce2e1286f3d2a1ca9cbda7efdb8b7 \ - --hash=sha256:aa78ee52b9069d7475eeac457389b6275aa92111895d78fbaa2202a52dac112e - # via cairosvg -cairosvg==2.7.1 \ - --hash=sha256:432531d72347291b9a9ebfb6777026b607563fd8719c46ee742db0aef7271ba0 \ - --hash=sha256:8a5222d4e6c3f86f1f7046b63246877a63b49923a1cd202184c3a634ef546b3b - # via mkdocs-material -certifi==2024.2.2 \ - --hash=sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f \ - --hash=sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1 - # via requests -cffi==1.16.0 \ - --hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \ - --hash=sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a \ - --hash=sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417 \ - --hash=sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab \ - --hash=sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520 \ - --hash=sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36 \ - --hash=sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743 \ - --hash=sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8 \ - --hash=sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed \ - --hash=sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684 \ - --hash=sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56 \ - --hash=sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324 \ - --hash=sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d \ - --hash=sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235 \ - --hash=sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e \ - --hash=sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088 \ - --hash=sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000 \ - --hash=sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7 \ - --hash=sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e \ - --hash=sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673 \ - --hash=sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c \ - --hash=sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe \ - --hash=sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2 \ - --hash=sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098 \ - --hash=sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8 \ - --hash=sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a \ - --hash=sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0 \ - --hash=sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b \ - --hash=sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896 \ - --hash=sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e \ - --hash=sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9 \ - --hash=sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2 \ - --hash=sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b \ - --hash=sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6 \ - --hash=sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404 \ - --hash=sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f \ - --hash=sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0 \ - --hash=sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4 \ - --hash=sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc \ - --hash=sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936 \ - --hash=sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba \ - --hash=sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872 \ - --hash=sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb \ - --hash=sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614 \ - --hash=sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1 \ - --hash=sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d \ - --hash=sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969 \ - --hash=sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b \ - --hash=sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4 \ - --hash=sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627 \ - --hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \ - --hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357 - # via cairocffi -charset-normalizer==3.3.2 \ - --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \ - --hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \ - --hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \ - --hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \ - --hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \ - --hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \ - --hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \ - --hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \ - --hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \ - --hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \ - --hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \ - --hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \ - --hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \ - --hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \ - --hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \ - --hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \ - --hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \ - --hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \ - --hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \ - --hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \ - --hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \ - --hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \ - --hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \ - --hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \ - --hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \ - --hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \ - --hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \ - --hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \ - --hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \ - --hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \ - --hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \ - --hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \ - --hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \ - --hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \ - --hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \ - --hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \ - --hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \ - --hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \ - --hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \ - --hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \ - --hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \ - --hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \ - --hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \ - --hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \ - --hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \ - --hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \ - --hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \ - --hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \ - --hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \ - --hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \ - --hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \ - --hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \ - --hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \ - --hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \ - --hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \ - --hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \ - --hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \ - --hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \ - --hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \ - --hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \ - --hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \ - --hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \ - --hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \ - --hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \ - --hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \ - --hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \ - --hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \ - --hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \ - --hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \ - --hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \ - --hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \ - --hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \ - --hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \ - --hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \ - --hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \ - --hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \ - --hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \ - --hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \ - --hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \ - --hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \ - --hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \ - --hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \ - --hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \ - --hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \ - --hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \ - --hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \ - --hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \ - --hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \ - --hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \ - --hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561 - # via requests -click==8.1.7 \ - --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ - --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de - # via - # mkdocs - # mkdocstrings -colorama==0.4.6 \ - --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ - --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via - # build - # click - # griffe - # mkdocs - # mkdocs-material - # pytest -coverage==7.4.3 \ - --hash=sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa \ - --hash=sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003 \ - --hash=sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f \ - --hash=sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c \ - --hash=sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e \ - --hash=sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0 \ - --hash=sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9 \ - --hash=sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52 \ - --hash=sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e \ - --hash=sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454 \ - --hash=sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0 \ - --hash=sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079 \ - --hash=sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352 \ - --hash=sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f \ - --hash=sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30 \ - --hash=sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe \ - --hash=sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113 \ - --hash=sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765 \ - --hash=sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc \ - --hash=sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e \ - --hash=sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501 \ - --hash=sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7 \ - --hash=sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2 \ - --hash=sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f \ - --hash=sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4 \ - --hash=sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524 \ - --hash=sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c \ - --hash=sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51 \ - --hash=sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840 \ - --hash=sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6 \ - --hash=sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee \ - --hash=sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e \ - --hash=sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45 \ - --hash=sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba \ - --hash=sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d \ - --hash=sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3 \ - --hash=sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10 \ - --hash=sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e \ - --hash=sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb \ - --hash=sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9 \ - --hash=sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a \ - --hash=sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47 \ - --hash=sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1 \ - --hash=sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3 \ - --hash=sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914 \ - --hash=sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328 \ - --hash=sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6 \ - --hash=sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d \ - --hash=sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0 \ - --hash=sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94 \ - --hash=sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc \ - --hash=sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2 - # via pytest-cov -cssselect2==0.7.0 \ - --hash=sha256:1ccd984dab89fc68955043aca4e1b03e0cf29cad9880f6e28e3ba7a74b14aa5a \ - --hash=sha256:fd23a65bfd444595913f02fc71f6b286c29261e354c41d722ca7a261a49b5969 - # via cairosvg -defusedxml==0.7.1 \ - --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ - --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 - # via cairosvg -ghp-import==2.1.0 \ - --hash=sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 \ - --hash=sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343 - # via mkdocs -griffe==0.42.0 \ - --hash=sha256:384df6b802a60f70e65fdb7e83f5b27e2da869a12eac85b25b55250012dbc263 \ - --hash=sha256:fb83ee602701ffdf99c9a6bf5f0a5a3bd877364b3bffb2c451dc8fbd9645b0cf - # via mkdocstrings-python -idna==3.6 \ - --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ - --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f - # via requests -iniconfig==2.0.0 \ - --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ - --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 - # via pytest -jinja2==3.1.3 \ - --hash=sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa \ - --hash=sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90 - # via - # mkdocs - # mkdocs-material - # mkdocstrings -markdown==3.5.2 \ - --hash=sha256:d43323865d89fc0cb9b20c75fc8ad313af307cc087e84b657d9eec768eddeadd \ - --hash=sha256:e1ac7b3dc550ee80e602e71c1d168002f062e49f1b11e26a36264dafd4df2ef8 - # via - # mkdocs - # mkdocs-autorefs - # mkdocs-material - # mkdocstrings - # pymdown-extensions -markupsafe==2.1.5 \ - --hash=sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf \ - --hash=sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff \ - --hash=sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f \ - --hash=sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3 \ - --hash=sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532 \ - --hash=sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f \ - --hash=sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617 \ - --hash=sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df \ - --hash=sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4 \ - --hash=sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906 \ - --hash=sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f \ - --hash=sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4 \ - --hash=sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8 \ - --hash=sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371 \ - --hash=sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2 \ - --hash=sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465 \ - --hash=sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52 \ - --hash=sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6 \ - --hash=sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169 \ - --hash=sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad \ - --hash=sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2 \ - --hash=sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0 \ - --hash=sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029 \ - --hash=sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f \ - --hash=sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a \ - --hash=sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced \ - --hash=sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5 \ - --hash=sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c \ - --hash=sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf \ - --hash=sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9 \ - --hash=sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb \ - --hash=sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad \ - --hash=sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3 \ - --hash=sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1 \ - --hash=sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46 \ - --hash=sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc \ - --hash=sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a \ - --hash=sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee \ - --hash=sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900 \ - --hash=sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5 \ - --hash=sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea \ - --hash=sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f \ - --hash=sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5 \ - --hash=sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e \ - --hash=sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a \ - --hash=sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f \ - --hash=sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50 \ - --hash=sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a \ - --hash=sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b \ - --hash=sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4 \ - --hash=sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff \ - --hash=sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2 \ - --hash=sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46 \ - --hash=sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b \ - --hash=sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf \ - --hash=sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5 \ - --hash=sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5 \ - --hash=sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab \ - --hash=sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd \ - --hash=sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68 - # via - # jinja2 - # mkdocs - # mkdocs-autorefs - # mkdocstrings -mergedeep==1.3.4 \ - --hash=sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8 \ - --hash=sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307 - # via mkdocs -mkdocs==1.5.3 \ - --hash=sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1 \ - --hash=sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2 - # via - # -r requirements.in - # mkdocs-autorefs - # mkdocs-material - # mkdocstrings -mkdocs-autorefs==1.0.1 \ - --hash=sha256:aacdfae1ab197780fb7a2dac92ad8a3d8f7ca8049a9cbe56a4218cd52e8da570 \ - --hash=sha256:f684edf847eced40b570b57846b15f0bf57fb93ac2c510450775dcf16accb971 - # via mkdocstrings -mkdocs-material==9.5.13 \ - --hash=sha256:5cbe17fee4e3b4980c8420a04cc762d8dc052ef1e10532abd4fce88e5ea9ce6a \ - --hash=sha256:d8e4caae576312a88fd2609b81cf43d233cdbe36860d67a68702b018b425bd87 - # via -r requirements.in -mkdocs-material-extensions==1.3.1 \ - --hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \ - --hash=sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31 - # via mkdocs-material -mkdocstrings==0.24.1 \ - --hash=sha256:b4206f9a2ca8a648e222d5a0ca1d36ba7dee53c88732818de183b536f9042b5d \ - --hash=sha256:cc83f9a1c8724fc1be3c2fa071dd73d91ce902ef6a79710249ec8d0ee1064401 - # via - # -r requirements.in - # mkdocstrings-python -mkdocstrings-python==1.8.0 \ - --hash=sha256:1488bddf50ee42c07d9a488dddc197f8e8999c2899687043ec5dd1643d057192 \ - --hash=sha256:4209970cc90bec194568682a535848a8d8489516c6ed4adbe58bbc67b699ca9d - # via mkdocstrings -packaging==24.0 \ - --hash=sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5 \ - --hash=sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9 - # via - # build - # mkdocs - # pytest -paginate==0.5.6 \ - --hash=sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d - # via mkdocs-material -pathspec==0.12.1 \ - --hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \ - --hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712 - # via mkdocs -pillow==10.2.0 \ - --hash=sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8 \ - --hash=sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39 \ - --hash=sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac \ - --hash=sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869 \ - --hash=sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e \ - --hash=sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04 \ - --hash=sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9 \ - --hash=sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e \ - --hash=sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe \ - --hash=sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef \ - --hash=sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56 \ - --hash=sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa \ - --hash=sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f \ - --hash=sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f \ - --hash=sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e \ - --hash=sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a \ - --hash=sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2 \ - --hash=sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2 \ - --hash=sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5 \ - --hash=sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a \ - --hash=sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2 \ - --hash=sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213 \ - --hash=sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563 \ - --hash=sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591 \ - --hash=sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c \ - --hash=sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2 \ - --hash=sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb \ - --hash=sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757 \ - --hash=sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0 \ - --hash=sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452 \ - --hash=sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad \ - --hash=sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01 \ - --hash=sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f \ - --hash=sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5 \ - --hash=sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61 \ - --hash=sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e \ - --hash=sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b \ - --hash=sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068 \ - --hash=sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9 \ - --hash=sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588 \ - --hash=sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483 \ - --hash=sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f \ - --hash=sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67 \ - --hash=sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7 \ - --hash=sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311 \ - --hash=sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6 \ - --hash=sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72 \ - --hash=sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6 \ - --hash=sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129 \ - --hash=sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13 \ - --hash=sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67 \ - --hash=sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c \ - --hash=sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516 \ - --hash=sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e \ - --hash=sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e \ - --hash=sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364 \ - --hash=sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023 \ - --hash=sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1 \ - --hash=sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04 \ - --hash=sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d \ - --hash=sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a \ - --hash=sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7 \ - --hash=sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb \ - --hash=sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4 \ - --hash=sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e \ - --hash=sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1 \ - --hash=sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48 \ - --hash=sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868 - # via - # cairosvg - # mkdocs-material -platformdirs==4.2.0 \ - --hash=sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068 \ - --hash=sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768 - # via - # mkdocs - # mkdocstrings -pluggy==1.4.0 \ - --hash=sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981 \ - --hash=sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be - # via pytest -pycparser==2.21 \ - --hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \ - --hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206 - # via cffi -pygments==2.17.2 \ - --hash=sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c \ - --hash=sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367 - # via - # -r requirements.in - # mkdocs-material -pymdown-extensions==10.7.1 \ - --hash=sha256:c70e146bdd83c744ffc766b4671999796aba18842b268510a329f7f64700d584 \ - --hash=sha256:f5cc7000d7ff0d1ce9395d216017fa4df3dde800afb1fb72d1c7d3fd35e710f4 - # via - # mkdocs-material - # mkdocstrings -pyproject-hooks==1.0.0 \ - --hash=sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8 \ - --hash=sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5 - # via build -pytest==8.1.1 \ - --hash=sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7 \ - --hash=sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044 - # via - # -r requirements.in - # pytest-cov -pytest-cov==4.1.0 \ - --hash=sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6 \ - --hash=sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a - # via -r requirements.in -python-dateutil==2.9.0.post0 \ - --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ - --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 - # via ghp-import -pyyaml==6.0.1 \ - --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \ - --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ - --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \ - --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ - --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ - --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ - --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ - --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ - --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ - --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ - --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \ - --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \ - --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ - --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \ - --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ - --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ - --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ - --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ - --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ - --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ - --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ - --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \ - --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ - --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ - --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ - --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \ - --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \ - --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ - --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ - --hash=sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef \ - --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ - --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ - --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ - --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ - --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ - --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ - --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ - --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ - --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ - --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ - --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ - --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ - --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \ - --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ - --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \ - --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ - --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ - --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ - --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ - --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ - --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f - # via - # mkdocs - # pymdown-extensions - # pyyaml-env-tag -pyyaml-env-tag==0.1 \ - --hash=sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb \ - --hash=sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069 - # via mkdocs -regex==2023.12.25 \ - --hash=sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5 \ - --hash=sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770 \ - --hash=sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc \ - --hash=sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105 \ - --hash=sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d \ - --hash=sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b \ - --hash=sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9 \ - --hash=sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630 \ - --hash=sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6 \ - --hash=sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c \ - --hash=sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482 \ - --hash=sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6 \ - --hash=sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a \ - --hash=sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80 \ - --hash=sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5 \ - --hash=sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1 \ - --hash=sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f \ - --hash=sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf \ - --hash=sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb \ - --hash=sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2 \ - --hash=sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347 \ - --hash=sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20 \ - --hash=sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060 \ - --hash=sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5 \ - --hash=sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73 \ - --hash=sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f \ - --hash=sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d \ - --hash=sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3 \ - --hash=sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae \ - --hash=sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4 \ - --hash=sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2 \ - --hash=sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457 \ - --hash=sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c \ - --hash=sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4 \ - --hash=sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87 \ - --hash=sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0 \ - --hash=sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704 \ - --hash=sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f \ - --hash=sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f \ - --hash=sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b \ - --hash=sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5 \ - --hash=sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923 \ - --hash=sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715 \ - --hash=sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c \ - --hash=sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca \ - --hash=sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1 \ - --hash=sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756 \ - --hash=sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360 \ - --hash=sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc \ - --hash=sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445 \ - --hash=sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e \ - --hash=sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4 \ - --hash=sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a \ - --hash=sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8 \ - --hash=sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53 \ - --hash=sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697 \ - --hash=sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf \ - --hash=sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a \ - --hash=sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415 \ - --hash=sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f \ - --hash=sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9 \ - --hash=sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400 \ - --hash=sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d \ - --hash=sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392 \ - --hash=sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb \ - --hash=sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd \ - --hash=sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861 \ - --hash=sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232 \ - --hash=sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95 \ - --hash=sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7 \ - --hash=sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39 \ - --hash=sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887 \ - --hash=sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5 \ - --hash=sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39 \ - --hash=sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb \ - --hash=sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586 \ - --hash=sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97 \ - --hash=sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423 \ - --hash=sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69 \ - --hash=sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7 \ - --hash=sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1 \ - --hash=sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7 \ - --hash=sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5 \ - --hash=sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8 \ - --hash=sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91 \ - --hash=sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590 \ - --hash=sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe \ - --hash=sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c \ - --hash=sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64 \ - --hash=sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd \ - --hash=sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa \ - --hash=sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31 \ - --hash=sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988 - # via mkdocs-material -requests==2.31.0 \ - --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ - --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 - # via mkdocs-material -six==1.16.0 \ - --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ - --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 - # via python-dateutil -tinycss2==1.2.1 \ - --hash=sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847 \ - --hash=sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627 - # via - # cairosvg - # cssselect2 -urllib3==2.2.1 \ - --hash=sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d \ - --hash=sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19 - # via requests -watchdog==4.0.0 \ - --hash=sha256:11e12fafb13372e18ca1bbf12d50f593e7280646687463dd47730fd4f4d5d257 \ - --hash=sha256:2895bf0518361a9728773083908801a376743bcc37dfa252b801af8fd281b1ca \ - --hash=sha256:39cb34b1f1afbf23e9562501673e7146777efe95da24fab5707b88f7fb11649b \ - --hash=sha256:45cc09cc4c3b43fb10b59ef4d07318d9a3ecdbff03abd2e36e77b6dd9f9a5c85 \ - --hash=sha256:4986db5e8880b0e6b7cd52ba36255d4793bf5cdc95bd6264806c233173b1ec0b \ - --hash=sha256:5369136a6474678e02426bd984466343924d1df8e2fd94a9b443cb7e3aa20d19 \ - --hash=sha256:557ba04c816d23ce98a06e70af6abaa0485f6d94994ec78a42b05d1c03dcbd50 \ - --hash=sha256:6a4db54edea37d1058b08947c789a2354ee02972ed5d1e0dca9b0b820f4c7f92 \ - --hash=sha256:6a80d5cae8c265842c7419c560b9961561556c4361b297b4c431903f8c33b269 \ - --hash=sha256:6a9c71a0b02985b4b0b6d14b875a6c86ddea2fdbebd0c9a720a806a8bbffc69f \ - --hash=sha256:6c47bdd680009b11c9ac382163e05ca43baf4127954c5f6d0250e7d772d2b80c \ - --hash=sha256:6e949a8a94186bced05b6508faa61b7adacc911115664ccb1923b9ad1f1ccf7b \ - --hash=sha256:73c7a935e62033bd5e8f0da33a4dcb763da2361921a69a5a95aaf6c93aa03a87 \ - --hash=sha256:76ad8484379695f3fe46228962017a7e1337e9acadafed67eb20aabb175df98b \ - --hash=sha256:8350d4055505412a426b6ad8c521bc7d367d1637a762c70fdd93a3a0d595990b \ - --hash=sha256:87e9df830022488e235dd601478c15ad73a0389628588ba0b028cb74eb72fed8 \ - --hash=sha256:8f9a542c979df62098ae9c58b19e03ad3df1c9d8c6895d96c0d51da17b243b1c \ - --hash=sha256:8fec441f5adcf81dd240a5fe78e3d83767999771630b5ddfc5867827a34fa3d3 \ - --hash=sha256:9a03e16e55465177d416699331b0f3564138f1807ecc5f2de9d55d8f188d08c7 \ - --hash=sha256:ba30a896166f0fee83183cec913298151b73164160d965af2e93a20bbd2ab605 \ - --hash=sha256:c17d98799f32e3f55f181f19dd2021d762eb38fdd381b4a748b9f5a36738e935 \ - --hash=sha256:c522392acc5e962bcac3b22b9592493ffd06d1fc5d755954e6be9f4990de932b \ - --hash=sha256:d0f9bd1fd919134d459d8abf954f63886745f4660ef66480b9d753a7c9d40927 \ - --hash=sha256:d18d7f18a47de6863cd480734613502904611730f8def45fc52a5d97503e5101 \ - --hash=sha256:d31481ccf4694a8416b681544c23bd271f5a123162ab603c7d7d2dd7dd901a07 \ - --hash=sha256:e3e7065cbdabe6183ab82199d7a4f6b3ba0a438c5a512a68559846ccb76a78ec \ - --hash=sha256:eed82cdf79cd7f0232e2fdc1ad05b06a5e102a43e331f7d041e5f0e0a34a51c4 \ - --hash=sha256:f970663fa4f7e80401a7b0cbeec00fa801bf0287d93d48368fc3e6fa32716245 \ - --hash=sha256:f9b2fdca47dc855516b2d66eef3c39f2672cbf7e7a42e7e67ad2cbfcd6ba107d - # via mkdocs -webencodings==0.5.1 \ - --hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \ - --hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923 - # via - # cssselect2 - # tinycss2 diff --git a/src/cwmath/__init__.py b/src/cwmath/__init__.py index e69de29..cb0a75a 100644 --- a/src/cwmath/__init__.py +++ b/src/cwmath/__init__.py @@ -0,0 +1,27 @@ +"""Cadwork Math Utilities — frozen geometry types for plugin authors.""" + +from cwmath.cadwork import to_point_3d +from cwmath.frame3 import Frame3 +from cwmath.line3 import Line3, project_point_on_line +from cwmath.plane3 import Plane3, project_point_on_plane +from cwmath.point2 import Point2 +from cwmath.point3 import Point3 +from cwmath.tolerance import ABS_TOL, REL_TOL, is_close +from cwmath.vec2 import Vec2 +from cwmath.vec3 import Vec3 + +__all__ = [ + "ABS_TOL", + "REL_TOL", + "Frame3", + "Line3", + "Plane3", + "Point2", + "Point3", + "Vec2", + "Vec3", + "is_close", + "project_point_on_line", + "project_point_on_plane", + "to_point_3d", +] diff --git a/src/cwmath/cadwork.py b/src/cwmath/cadwork.py new file mode 100644 index 0000000..a14ab48 --- /dev/null +++ b/src/cwmath/cadwork.py @@ -0,0 +1,42 @@ +"""Lazy ``cadwork.point_3d`` adapter. Domain types never import the host.""" + +from __future__ import annotations + +from typing import Protocol + +from cwmath.point3 import Point3, Xyz +from cwmath.vec3 import Vec3 + + +class CadworkNotAvailableError(RuntimeError): + """Raised when ``to_point_3d`` has neither a factory nor a host module.""" + + +class Point3dFactory(Protocol): + """Callable host / fake that builds an ``Xyz`` from three coordinates.""" + + def point_3d(self, x: float, y: float, z: float) -> Xyz: ... + + +def to_point_3d( + value: Point3 | Vec3, + *, + factory: Point3dFactory | None = None, +) -> Xyz: + """Convert a ``Point3`` or ``Vec3`` to a host (or fake) ``point_3d``. + + ``factory`` is the test seam (architecture §5). When omitted, ``cadwork`` + is imported only at this call site. + """ + if not isinstance(value, (Point3, Vec3)): + raise TypeError("to_point_3d expects a Point3 or Vec3") + if factory is None: + try: + import cadwork as host + except ImportError as exc: + raise CadworkNotAvailableError( + "cadwork is not available; pass factory= to construct " + "a point_3d outside the cadwork host" + ) from exc + factory = host + return factory.point_3d(value.x, value.y, value.z) diff --git a/src/cwmath/cwexamples.py b/src/cwmath/cwexamples.py deleted file mode 100644 index 20f2a3f..0000000 --- a/src/cwmath/cwexamples.py +++ /dev/null @@ -1,37 +0,0 @@ -from math import acos, pi -import cadwork - -def add_one(number:int) -> int: - """Increase number by one. - - Examples: - >>> add_one(5) - 6 - >>> add_one(6) - 7 - - Args: - number (int): a number - - Returns: - int: number increased by one - """ - return number + 1 - -def angle_between_vectors(v1:cadwork.point_3d, v2:cadwork.point_3d) -> float: - """Get angle between two vectors. - - Examples: - >>> v1 = point_3d(1., 0., 0.) - >>> v2 = point_3d(0., 1., 0.) - >>> angle_between_vectors(v1, v2) - 90.0 - - Args: - v1 (point_3d): a first vector - v2 (point_3d): a second vector - - Returns: - float: angle in degrees - """ - return acos(v1.dot(v2) / (v1.magnitude() * v2.magnitude())) * (180 /pi) diff --git a/src/cwmath/cwplane3d.py b/src/cwmath/cwplane3d.py deleted file mode 100644 index 351aebf..0000000 --- a/src/cwmath/cwplane3d.py +++ /dev/null @@ -1,182 +0,0 @@ -__author__ = 'Brunner' -__date__ = '13.03.2024' - -from math import sqrt -from cwmath import cwvector3d - - -class CwPlane3d: - """Plane class for 3D planes. """ - - def __init__(self, point_3d, normal_vector): - self._coefficient_a = normal_vector.x - self._coefficient_b = normal_vector.y - self._coefficient_c = normal_vector.z - self._constant_d = -(self._coefficient_a * point_3d.x + self._coefficient_b * point_3d.y + self._coefficient_c * point_3d.z) - - @property - def coefficient_a(self) -> float: - return self._coefficient_a - - @coefficient_a.setter - def coefficient_a(self, value: float) -> None: - self._coefficient_a = value - - @property - def coefficient_b(self) -> float: - return self._coefficient_b - - @coefficient_b.setter - def coefficient_b(self, value: float) -> None: - self._coefficient_b = value - - @property - def coefficient_c(self) -> float: - return self._coefficient_c - - @coefficient_c.setter - def coefficient_c(self, value: float) -> None: - self._coefficient_c = value - - @property - def constant_d(self) -> float: - return self._constant_d - - @constant_d.setter - def constant_d(self, value: float) -> None: - self._constant_d = value - - def __call__(self, x: float, y: float, z: float) -> float: - """ Calculate the value of the plane at a given point. - - Args: - x: x-coordinate of the point - y: y-coordinate of the point - z: z-coordinate of the point - - - Returns: - float: value of the plane at the given point - """ - return self._coefficient_a * x + self._coefficient_b * y + self._coefficient_c * z + self._constant_d - - def __str__(self) -> str: - return f'{self._coefficient_a}x + {self._coefficient_b}y + {self._coefficient_c}z + {self._constant_d} = 0' - - def __repr__(self) -> str: - return f'CwPlane3d({self._coefficient_a}, {self._coefficient_b}, {self._coefficient_c}, {self._constant_d})' - - def __eq__(self, other: 'CwPlane3d') -> bool: - return abs(self._coefficient_a - other._coefficient_a) < 1e-6 and abs( - self._coefficient_b - other._coefficient_b) < 1e-6 and abs( - self._coefficient_c - other._coefficient_c) < 1e-6 and abs( - self._constant_d - other._constant_d) < 1e-6 - - def __ne__(self, other: 'CwPlane3d') -> bool: - return not self.__eq__(other) - - def __iter__(self): - yield self._coefficient_a - yield self._coefficient_b - yield self._coefficient_c - yield self._constant_d - - def __getitem__(self, index: int) -> float: - return (self._coefficient_a, self._coefficient_b, self._coefficient_c, self._constant_d)[index] - - def __setitem__(self, index: int, value: float) -> None: - if index == 0: - self._coefficient_a = value - elif index == 1: - self._coefficient_b = value - elif index == 2: - self._coefficient_c = value - elif index == 3: - self._constant_d = value - - def is_parallel(self, other: 'CwPlane3d') -> bool: - """ Checks if two planes are parallel. - - Args: - other: 3d plane - - Returns: - if the planes are parallel - """ - return abs(self._coefficient_a * other._coefficient_b - self._coefficient_b * other._coefficient_a) < 1e-6 and abs( - self._coefficient_a * other._coefficient_c - self._coefficient_c * other._coefficient_a) < 1e-6 and abs( - self._coefficient_b * other._coefficient_c - self._coefficient_c * other._coefficient_b) < 1e-6 - - def is_perpendicular(self, other: 'CwPlane3d') -> bool: - """ Checks if two planes are perpendicular. - - Args: - other: 3d plane - - Returns: - if the planes are perpendicular - """ - return abs( - self._coefficient_a * other._coefficient_a + self._coefficient_b * other._coefficient_b + self._coefficient_c * other._coefficient_c) < 1e-6 - - def is_coplanar(self, other: 'CwPlane3d') -> bool: - """ Checks if two planes are coplanar. - - Args: - other: 3d plane - - Returns: - if the planes are coplanar - """ - if other._coefficient_a == 0: - return self._coefficient_a == 0 - if other._coefficient_b == 0: - return self._coefficient_b == 0 - if other._coefficient_c == 0: - return self._coefficient_c == 0 - - return self._coefficient_a / other._coefficient_a \ - == self._coefficient_b / other._coefficient_b \ - == self._coefficient_c / other._coefficient_c - - def is_point_on_plane(self, point: 'cwvector3d.CwVector3d') -> bool: - """ Checks if a point is on the plane. - - Args: - point: point - - Returns: - if the point is on the plane - """ - return abs( - self._coefficient_a * point.x + self._coefficient_b * point.y + self._coefficient_c * point.z + self._constant_d) < 1e-6 - - def distance_to_point(self, point: 'cwvector3d.CwVector3d') -> float: - """ Calculates the distance from a point to the plane. - The distance from a point to a plane is given by the formula: - |ax + by + cz + d| / sqrt(a^2 + b^2 + c^2) - where (a, b, c) is the normal vector of the plane, - (x, y, z) are the coordinates of the point, and - d is the constant term in the plane equation. - - Args: - point: point - - Returns: - distance from the point to the plane - """ - numerator = abs(self._coefficient_a * point.x + self._coefficient_b * point.y + self._coefficient_c * point.z + self._constant_d) - denominator = sqrt(self._coefficient_a**2 + self._coefficient_b**2 + self._coefficient_c**2) - return numerator / denominator - - def distance_to_plane(self, other: 'CwPlane3d') -> float: - """ Calculates the distance from a plane to another plane. - - Args: - other: 3d plane - - Returns: - distance to plane - """ - return abs(self._constant_d - other._constant_d) / sqrt( - self._coefficient_a ** 2 + self._coefficient_b ** 2 + self._coefficient_c ** 2) diff --git a/src/cwmath/cwvector3d.py b/src/cwmath/cwvector3d.py deleted file mode 100644 index 70e8ce9..0000000 --- a/src/cwmath/cwvector3d.py +++ /dev/null @@ -1,119 +0,0 @@ -__author__ = 'Brunner' -__date__ = '13.03.2024' - - -class CwVector3d: - """Vector class for 3D vectors.""" - - def __init__(self, x: float, y: float, z: float): - self._x = x - self._y = y - self._z = z - - @classmethod - def from_point_3d(cls, point_3d: 'cadwork.point_3d') -> 'CwVector3d': - """Create a CwVector3d from a cadwork.point_3d. - - Args: - point_3d: 3d point - - Returns: - vector - """ - return cls(point_3d.x, point_3d.y, point_3d.z) - - @property - def x(self) -> float: - return self._x - - @x.setter - def x(self, value: float) -> None: - self._x = value - - @property - def y(self) -> float: - return self._y - - @y.setter - def y(self, value: float) -> None: - self._y = value - - @property - def z(self) -> float: - return self._z - - @z.setter - def z(self, value: float) -> None: - self._z = value - - def dot(self, other: 'CwVector3d') -> float: - """ Calculates the dot product of two vectors. - - Args: - other: vector - - Returns: - dot product - """ - return self._x * other._x + self._y * other._y + self._z * other._z - - def magnitude(self) -> float: - """ Calculates the magnitude of the vector. - - Returns: - magnitude - """ - return (self._x**2 + self._y**2 + self._z**2)**0.5 - - def normalize(self) -> 'CwVector3d': - """ Normalizes the vector. - - Returns: - normalized vector - """ - return self / self.magnitude() - - def __add__(self, other: 'CwVector3d') -> 'CwVector3d': - return CwVector3d(self._x + other._x, self._y + other._y, self._z + other._z) - - def __sub__(self, other: 'CwVector3d') -> 'CwVector3d': - return CwVector3d(self._x - other._x, self._y - other._y, self._z - other._z) - - def __mul__(self, scalar: float) -> 'CwVector3d': - return CwVector3d(self._x * scalar, self._y * scalar, self._z * scalar) - - def __truediv__(self, scalar: float) -> 'CwVector3d': - return CwVector3d(self._x / scalar, self._y / scalar, self._z / scalar) - - def __neg__(self) -> 'CwVector3d': - return CwVector3d(-self._x, -self._y, -self._z) - - def __eq__(self, other: 'CwVector3d') -> bool: - return abs(self._x - other._x) < 1e-6 and abs(self._y - other._y) < 1e-6 and abs(self._z - other._z) < 1e-6 - - def __ne__(self, other: 'CwVector3d') -> bool: - return not self.__eq__(other) - - def __str__(self) -> str: - return f'({self._x}, {self._y}, {self._z})' - - def __repr__(self) -> str: - return f'CwVector({self._x}, {self._y}, {self._z})' - - def __iter__(self): - yield self._x - yield self._y - yield self._z - - def __getitem__(self, index: int) -> float: - return (self._x, self._y, self._z)[index] - - def __setitem__(self, index: int, value: float) -> None: - if index == 0: - self._x = value - elif index == 1: - self._y = value - elif index == 2: - self._z = value - else: - raise IndexError('Index out of range') diff --git a/src/cwmath/frame3.py b/src/cwmath/frame3.py new file mode 100644 index 0000000..945bcb8 --- /dev/null +++ b/src/cwmath/frame3.py @@ -0,0 +1,129 @@ +"""Right-handed rigid frame. Transforms Point3/Vec3 between local and world.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import overload + +from cwmath.point3 import Point3 +from cwmath.tolerance import is_close as close_scalars +from cwmath.vec3 import Vec3 + + +def _require_point3(value: object, name: str) -> None: + if not isinstance(value, Point3): + raise TypeError(f"{name} must be a Point3") + + +def _require_vec3(value: object, name: str) -> None: + if not isinstance(value, Vec3): + raise TypeError(f"{name} must be a Vec3") + + +def _is_unit(axis: Vec3) -> bool: + return close_scalars(axis.magnitude(), 1.0) + + +def _is_orthogonal(a: Vec3, b: Vec3) -> bool: + return close_scalars(a.dot(b), 0.0) + + +def _require_orthonormal_right_handed(x: Vec3, y: Vec3, z: Vec3) -> None: + if not (_is_unit(x) and _is_unit(y) and _is_unit(z)): + raise ValueError("frame axes must be unit length") + if not (_is_orthogonal(x, y) and _is_orthogonal(y, z) and _is_orthogonal(z, x)): + raise ValueError("frame axes must be mutually orthogonal") + if not x.cross(y).is_close(z): + raise ValueError("frame axes must form a right-handed basis") + + +@dataclass(frozen=True, slots=True) +class Frame3: + """A frozen origin plus a right-handed orthonormal basis.""" + + origin: Point3 + x_axis: Vec3 + y_axis: Vec3 + z_axis: Vec3 + + def __post_init__(self) -> None: + _require_point3(self.origin, "origin") + _require_vec3(self.x_axis, "x_axis") + _require_vec3(self.y_axis, "y_axis") + _require_vec3(self.z_axis, "z_axis") + _require_orthonormal_right_handed(self.x_axis, self.y_axis, self.z_axis) + + @classmethod + def from_origin_and_axes(cls, origin: Point3, x: Vec3, y: Vec3, z: Vec3) -> Frame3: + """Build a frame from a known-good unit, orthogonal, right-handed basis.""" + return cls(origin=origin, x_axis=x, y_axis=y, z_axis=z) + + @classmethod + def from_origin_xy(cls, origin: Point3, x: Vec3, y: Vec3) -> Frame3: + """Build a right-handed frame from origin + xy, via Gram–Schmidt. + + ``z = x × y``; then orthonormalize x, then y, then z. A zero or + parallel pair is degenerate and raises ``ValueError``. + """ + _require_point3(origin, "origin") + _require_vec3(x, "x") + _require_vec3(y, "y") + + z = x.cross(y) + if z.is_close(Vec3.zero()): + raise ValueError("from_origin_xy: x and y are degenerate") + + try: + x_hat = x.normalized() + except ValueError: + raise ValueError("from_origin_xy: x and y are degenerate") from None + + y_proj = y - x_hat * y.dot(x_hat) + try: + y_hat = y_proj.normalized() + except ValueError: + raise ValueError("from_origin_xy: x and y are degenerate") from None + + z_proj = z - x_hat * z.dot(x_hat) - y_hat * z.dot(y_hat) + try: + z_hat = z_proj.normalized() + except ValueError: + raise ValueError("from_origin_xy: x and y are degenerate") from None + + return cls(origin=origin, x_axis=x_hat, y_axis=y_hat, z_axis=z_hat) + + @overload + def to_world(self, value: Point3) -> Point3: ... + + @overload + def to_world(self, value: Vec3) -> Vec3: ... + + def to_world(self, value: Point3 | Vec3) -> Point3 | Vec3: + if isinstance(value, Point3): + return ( + self.origin + + self.x_axis * value.x + + self.y_axis * value.y + + self.z_axis * value.z + ) + if isinstance(value, Vec3): + return self.x_axis * value.x + self.y_axis * value.y + self.z_axis * value.z + raise TypeError("to_world expects a Point3 or Vec3") + + @overload + def to_local(self, value: Point3) -> Point3: ... + + @overload + def to_local(self, value: Vec3) -> Vec3: ... + + def to_local(self, value: Point3 | Vec3) -> Point3 | Vec3: + if isinstance(value, Point3): + q = value - self.origin + return Point3(q.dot(self.x_axis), q.dot(self.y_axis), q.dot(self.z_axis)) + if isinstance(value, Vec3): + return Vec3( + value.dot(self.x_axis), + value.dot(self.y_axis), + value.dot(self.z_axis), + ) + raise TypeError("to_local expects a Point3 or Vec3") diff --git a/src/cwmath/line3.py b/src/cwmath/line3.py new file mode 100644 index 0000000..f5b4cab --- /dev/null +++ b/src/cwmath/line3.py @@ -0,0 +1,68 @@ +"""Frozen infinite line in 3-space: an origin plus a unit direction.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from cwmath.point3 import Point3 +from cwmath.vec3 import Vec3 + + +def _require_point3(value: object, name: str) -> None: + if not isinstance(value, Point3): + raise TypeError(f"{name} must be a Point3") + + +def _require_vec3(value: object, name: str) -> None: + if not isinstance(value, Vec3): + raise TypeError(f"{name} must be a Vec3") + + +@dataclass(frozen=True, slots=True) +class Line3: + """A frozen infinite line defined by an origin and a unit direction.""" + + origin: Point3 + direction: Vec3 + + def __post_init__(self) -> None: + _require_point3(self.origin, "origin") + _require_vec3(self.direction, "direction") + try: + unit = self.direction.normalized() + except ValueError: + raise ValueError("line direction must be non-zero") from None + object.__setattr__(self, "direction", unit) + + @classmethod + def from_origin_direction(cls, origin: Point3, direction: Vec3) -> Line3: + """Build a line through ``origin`` with a unit ``direction``.""" + return cls(origin=origin, direction=direction) + + @classmethod + def from_points(cls, a: Point3, b: Point3) -> Line3: + """Build a line through two distinct points, directed ``a → b``.""" + _require_point3(a, "a") + _require_point3(b, "b") + try: + direction = (b - a).normalized() + except ValueError: + raise ValueError("from_points: points are coincident") from None + return cls(origin=a, direction=direction) + + def parameter(self, point: Point3) -> float: + """Return ``t`` such that ``origin + t * direction`` is the foot of ``point``.""" + _require_point3(point, "point") + return (point - self.origin).dot(self.direction) + + def point_at(self, t: float) -> Point3: + return self.origin + self.direction * t + + def project(self, point: Point3) -> Point3: + """Return the closest point on this line to ``point``.""" + return self.point_at(self.parameter(point)) + + +def project_point_on_line(point: Point3, line: Line3) -> Point3: + """Return the closest point on ``line`` to ``point``.""" + return line.project(point) diff --git a/src/cwmath/plane3.py b/src/cwmath/plane3.py new file mode 100644 index 0000000..b5265c4 --- /dev/null +++ b/src/cwmath/plane3.py @@ -0,0 +1,70 @@ +"""Frozen plane in 3-space: a point plus a unit normal.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from cwmath.point3 import Point3 +from cwmath.tolerance import is_close as close_scalars +from cwmath.vec3 import Vec3 + + +def _require_point3(value: object, name: str) -> None: + if not isinstance(value, Point3): + raise TypeError(f"{name} must be a Point3") + + +def _require_vec3(value: object, name: str) -> None: + if not isinstance(value, Vec3): + raise TypeError(f"{name} must be a Vec3") + + +@dataclass(frozen=True, slots=True) +class Plane3: + """A frozen plane defined by a point and a unit normal.""" + + point: Point3 + normal: Vec3 + + def __post_init__(self) -> None: + _require_point3(self.point, "point") + _require_vec3(self.normal, "normal") + try: + unit = self.normal.normalized() + except ValueError: + raise ValueError("plane normal must be non-zero") from None + object.__setattr__(self, "normal", unit) + + @classmethod + def from_point_normal(cls, point: Point3, normal: Vec3) -> Plane3: + """Build a plane through ``point`` with a unit ``normal``.""" + return cls(point=point, normal=normal) + + def signed_distance(self, point: Point3) -> float: + _require_point3(point, "point") + return (point - self.point).dot(self.normal) + + def distance(self, point: Point3) -> float: + return abs(self.signed_distance(point)) + + def contains(self, point: Point3) -> bool: + return close_scalars(self.signed_distance(point), 0.0) + + def project(self, point: Point3) -> Point3: + """Return the foot of ``point`` on this plane.""" + _require_point3(point, "point") + return point - self.normal * self.signed_distance(point) + + def as_abcd(self) -> tuple[float, float, float, float]: + """Return the implicit coefficients ``ax + by + cz + d = 0``.""" + d = -( + self.normal.x * self.point.x + + self.normal.y * self.point.y + + self.normal.z * self.point.z + ) + return (self.normal.x, self.normal.y, self.normal.z, d) + + +def project_point_on_plane(point: Point3, plane: Plane3) -> Point3: + """Return the foot of ``point`` on ``plane``.""" + return plane.project(point) diff --git a/src/cwmath/point2.py b/src/cwmath/point2.py new file mode 100644 index 0000000..1b008a6 --- /dev/null +++ b/src/cwmath/point2.py @@ -0,0 +1,94 @@ +"""Frozen 2D point. Affine: Point2 + Vec2 → Point2, Point2 − Point2 → Vec2.""" + +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass +from typing import Protocol, Self + +from cwmath.tolerance import ABS_TOL, REL_TOL +from cwmath.tolerance import is_close as close_scalars + + +class Xy(Protocol): + """Read-only duck type for anything with ``.x`` / ``.y``.""" + + @property + def x(self) -> float: ... + + @property + def y(self) -> float: ... + + +@dataclass(frozen=True, slots=True) +class Point2: + """A frozen position in 2-space (millimetres, implicit).""" + + x: float + y: float + + def __post_init__(self) -> None: + object.__setattr__(self, "x", float(self.x)) + object.__setattr__(self, "y", float(self.y)) + + @classmethod + def from_xy(cls, value: Xy) -> Self: + return cls(value.x, value.y) + + def as_tuple(self) -> tuple[float, float]: + return (self.x, self.y) + + def is_close( + self, + other: object, + *, + abs_tol: float = ABS_TOL, + rel_tol: float = REL_TOL, + ) -> bool: + if not isinstance(other, Point2): + return False + return close_scalars( + self.x, other.x, abs_tol=abs_tol, rel_tol=rel_tol + ) and close_scalars(self.y, other.y, abs_tol=abs_tol, rel_tol=rel_tol) + + def with_x(self, x: float) -> Point2: + return Point2(x, self.y) + + def with_y(self, y: float) -> Point2: + return Point2(self.x, y) + + def distance(self, other: Point2) -> float: + if not isinstance(other, Point2): + raise TypeError("distance expects a Point2") + return (other - self).magnitude() + + def lerp(self, other: Point2, t: float) -> Point2: + if not isinstance(other, Point2): + raise TypeError("lerp expects a Point2") + return self + (other - self) * t + + def __add__(self, other: object) -> Point2: + from cwmath.vec2 import Vec2 + + if isinstance(other, Vec2): + return Point2(self.x + other.x, self.y + other.y) + return NotImplemented + + def __sub__(self, other: object) -> Point2 | Vec2: + from cwmath.vec2 import Vec2 + + if isinstance(other, Point2): + return Vec2(self.x - other.x, self.y - other.y) + if isinstance(other, Vec2): + return Point2(self.x - other.x, self.y - other.y) + return NotImplemented + + def __iter__(self) -> Iterator[float]: + yield self.x + yield self.y + + def __getitem__(self, index: int) -> float: + try: + return (self.x, self.y)[index] + except IndexError: + raise IndexError(index) from None diff --git a/src/cwmath/point3.py b/src/cwmath/point3.py new file mode 100644 index 0000000..28a7379 --- /dev/null +++ b/src/cwmath/point3.py @@ -0,0 +1,105 @@ +"""Frozen 3D point. Affine: Point3 + Vec3 → Point3, Point3 − Point3 → Vec3.""" + +from __future__ import annotations + +from collections.abc import Iterator +from dataclasses import dataclass +from typing import Protocol, Self + +from cwmath.tolerance import ABS_TOL, REL_TOL +from cwmath.tolerance import is_close as close_scalars + + +class Xyz(Protocol): + """Read-only duck type for anything with ``.x`` / ``.y`` / ``.z``.""" + + @property + def x(self) -> float: ... + + @property + def y(self) -> float: ... + + @property + def z(self) -> float: ... + + +@dataclass(frozen=True, slots=True) +class Point3: + """A frozen position in 3-space (millimetres, implicit).""" + + x: float + y: float + z: float + + def __post_init__(self) -> None: + object.__setattr__(self, "x", float(self.x)) + object.__setattr__(self, "y", float(self.y)) + object.__setattr__(self, "z", float(self.z)) + + @classmethod + def from_xyz(cls, value: Xyz) -> Self: + return cls(value.x, value.y, value.z) + + def as_tuple(self) -> tuple[float, float, float]: + return (self.x, self.y, self.z) + + def is_close( + self, + other: object, + *, + abs_tol: float = ABS_TOL, + rel_tol: float = REL_TOL, + ) -> bool: + if not isinstance(other, Point3): + return False + return ( + close_scalars(self.x, other.x, abs_tol=abs_tol, rel_tol=rel_tol) + and close_scalars(self.y, other.y, abs_tol=abs_tol, rel_tol=rel_tol) + and close_scalars(self.z, other.z, abs_tol=abs_tol, rel_tol=rel_tol) + ) + + def with_x(self, x: float) -> Point3: + return Point3(x, self.y, self.z) + + def with_y(self, y: float) -> Point3: + return Point3(self.x, y, self.z) + + def with_z(self, z: float) -> Point3: + return Point3(self.x, self.y, z) + + def distance(self, other: Point3) -> float: + if not isinstance(other, Point3): + raise TypeError("distance expects a Point3") + return (other - self).magnitude() + + def lerp(self, other: Point3, t: float) -> Point3: + if not isinstance(other, Point3): + raise TypeError("lerp expects a Point3") + return self + (other - self) * t + + def __add__(self, other: object) -> Point3: + from cwmath.vec3 import Vec3 + + if isinstance(other, Vec3): + return Point3(self.x + other.x, self.y + other.y, self.z + other.z) + return NotImplemented + + def __sub__(self, other: object) -> Point3 | Vec3: + from cwmath.vec3 import Vec3 + + if isinstance(other, Point3): + return Vec3(self.x - other.x, self.y - other.y, self.z - other.z) + if isinstance(other, Vec3): + return Point3(self.x - other.x, self.y - other.y, self.z - other.z) + return NotImplemented + + def __iter__(self) -> Iterator[float]: + yield self.x + yield self.y + yield self.z + + def __getitem__(self, index: int) -> float: + try: + return (self.x, self.y, self.z)[index] + except IndexError: + raise IndexError(index) from None diff --git a/src/cwmath/py.typed b/src/cwmath/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/cwmath/tests/__init__.py b/src/cwmath/tests/__init__.py deleted file mode 100644 index 03acc40..0000000 --- a/src/cwmath/tests/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -import sys -sys.path.append('.') diff --git a/src/cwmath/tests/test_cwplane3d.py b/src/cwmath/tests/test_cwplane3d.py deleted file mode 100644 index 79afde3..0000000 --- a/src/cwmath/tests/test_cwplane3d.py +++ /dev/null @@ -1,86 +0,0 @@ -import pytest -from _pytest.python_api import approx - -from cwmath import cwvector3d -from cwmath import cwplane3d - - -@pytest.fixture -def plane_1(): - vector_1 = cwvector3d.CwVector3d(-42.500000, 47.500000, 2480.000000) - vector_2 = cwvector3d.CwVector3d(0.000000, 0.000000, -1.000000) - return cwplane3d.CwPlane3d(vector_1, vector_2) - - -def test_call(plane_1): - result = plane_1(-42.500000, 47.500000, 0.000000) - assert result == approx(2480.0) - - -def test_str(plane_1): - result = str(plane_1) - assert result == "0.0x + 0.0y + -1.0z + 2480.0 = 0" - - -def test_repr(plane_1): - result = repr(plane_1) - assert result == "CwPlane3d(0.0, 0.0, -1.0, 2480.0)" - - -def test_eq(plane_1): - vector_1 = cwvector3d.CwVector3d(-42.500000, 47.500000, 2480.000000) - vector_2 = cwvector3d.CwVector3d(0.000000, 0.000000, -1.000000) - plane_2 = cwplane3d.CwPlane3d(vector_1, vector_2) - assert plane_1 == plane_2 - - -def test_ne(plane_1): - plane_2 = cwplane3d.CwPlane3d(cwvector3d.CwVector3d(-42.500000, 47.500000, 2480.000000), cwvector3d.CwVector3d(1.000000, 0.000000, -1.000000)) - assert plane_1 != plane_2 - - -def test_iter(plane_1): - result = list(plane_1) - assert result == [0.0, 0.0, -1.0, 2480.0] - - -def test_getitem(plane_1): - result = plane_1[2] - assert result == -1.0 - - -def test_setitem(plane_1): - plane_1[2] = -2.0 - assert plane_1[2] == -2.0 - - -def test_is_parallel(plane_1): - plane_2 = cwplane3d.CwPlane3d(cwvector3d.CwVector3d(-42.500000, 47.500000, 2480.000000), cwvector3d.CwVector3d(0.000000, 0.000000, -1.000000)) - assert plane_1.is_parallel(plane_2) - - -def test_is_perpendicular(plane_1): - plane_2 = cwplane3d.CwPlane3d(cwvector3d.CwVector3d(-42.500000, 47.500000, 2480.000000), cwvector3d.CwVector3d(1.000000, 0.000000, 0.000000)) - assert plane_1.is_perpendicular(plane_2) - - -def test_is_coplanar(plane_1): - plane_2 = cwplane3d.CwPlane3d(cwvector3d.CwVector3d(-42.500000, 47.500000, 280.000000), cwvector3d.CwVector3d(0.000000, 0.000000, -1.000000)) - assert plane_1.is_coplanar(plane_2) - - -def test_is_point_on_plane(plane_1): - point = cwvector3d.CwVector3d(-42.500000, 47.500000, 2480.000000) - assert plane_1.is_point_on_plane(point) - - -def test_distance_to_point(plane_1): - point = cwvector3d.CwVector3d(-42.500000, 47.500000, 0.000000) - result = plane_1.distance_to_point(point) - assert result == approx(2480.0) - - -def test_distance_to_plane(plane_1): - plane2 = cwplane3d.CwPlane3d(cwvector3d.CwVector3d(-42.500000, 47.500000, 2480.000000), cwvector3d.CwVector3d(0.000000, 0.000000, -1.000000)) - result = plane_1.distance_to_plane(plane2) - assert result == approx(0.0) diff --git a/src/cwmath/tests/test_cwvector3d.py b/src/cwmath/tests/test_cwvector3d.py deleted file mode 100644 index 3adf4cc..0000000 --- a/src/cwmath/tests/test_cwvector3d.py +++ /dev/null @@ -1,84 +0,0 @@ -import pytest -from _pytest.python_api import approx - -from cwmath import cwvector3d - - -@pytest.fixture -def vector_1(): - return cwvector3d.CwVector3d(1, 2, 3) - - -@pytest.fixture -def vector_2(): - return cwvector3d.CwVector3d(4, 5, 6) - - -def test_dot_product(vector_1, vector_2): - result = vector_1.dot(vector_2) - assert result == 32 - - -def test_magnitude(vector_1): - result = vector_1.magnitude() - assert result == approx(3.7416573867739413) - - -def test_normalization(vector_1): - result = vector_1.normalize() - assert result == cwvector3d.CwVector3d(0.2672612419124244, 0.5345224838248488, 0.8017837257372732) - - -def test_normalization_magnitude(vector_1): - result = vector_1.normalize().magnitude() - assert result == approx(1.0) - - -def test_normalization_direction(): - start_pt = cwvector3d.CwVector3d(0, 0, 100) - end_pt = cwvector3d.CwVector3d(0, 0, 0) - result = (end_pt - start_pt).normalize() - assert result == cwvector3d.CwVector3d(0, 0, -1) - - -def test_addition(vector_1, vector_2): - result = vector_1 + vector_2 - assert result == cwvector3d.CwVector3d(5, 7, 9) - - -def test_subtraction(vector_1, vector_2): - result = vector_1 - vector_2 - assert result == cwvector3d.CwVector3d(-3, -3, -3) - - -def test_scalar_multiplication(vector_1): - result = vector_1 * 2 - assert result == cwvector3d.CwVector3d(2, 4, 6) - - -def test_scalar_division(vector_1): - result = vector_1 / 2 - assert result == cwvector3d.CwVector3d(0.5, 1, 1.5) - - -def test_negation(vector_1): - result = -vector_1 - assert result == cwvector3d.CwVector3d(-1, -2, -3) - - -def test_equality(vector_1, vector_2): - assert vector_1 == cwvector3d.CwVector3d(1, 2, 3) - assert vector_1 != vector_2 - - -def test_string_representation(vector_1): - result = str(vector_1) - assert result, "(1, 2, 3)" - - -def test_indexing(vector_1): - assert vector_1[0] == 1 - assert vector_1[1] == 2 - assert vector_1[2] == 3 - with pytest.raises(IndexError): - vector_1[3] = 4 diff --git a/src/cwmath/tolerance.py b/src/cwmath/tolerance.py new file mode 100644 index 0000000..e4d91ba --- /dev/null +++ b/src/cwmath/tolerance.py @@ -0,0 +1,23 @@ +"""Shared closeness policy for scalar and geometry comparisons.""" + +from __future__ import annotations + +import math + +ABS_TOL = 1e-6 +REL_TOL = 1e-9 + + +def is_close( + a: float, + b: float, + *, + abs_tol: float = ABS_TOL, + rel_tol: float = REL_TOL, +) -> bool: + """Return whether two scalars are close under the package tolerances. + + Uses :func:`math.isclose` so every type shares one millimetre-scale policy. + ``==`` on public types stays exact IEEE equality; callers opt in here. + """ + return math.isclose(a, b, rel_tol=rel_tol, abs_tol=abs_tol) diff --git a/src/cwmath/vec2.py b/src/cwmath/vec2.py new file mode 100644 index 0000000..a6d4ec6 --- /dev/null +++ b/src/cwmath/vec2.py @@ -0,0 +1,119 @@ +"""Frozen 2D vector. Affine: Vec2 + Vec2 → Vec2; Vec2 + Point2 is illegal.""" + +from __future__ import annotations + +import math +import numbers +from collections.abc import Iterator +from dataclasses import dataclass +from typing import Self + +from cwmath.point2 import Xy +from cwmath.tolerance import ABS_TOL, REL_TOL +from cwmath.tolerance import is_close as close_scalars + + +@dataclass(frozen=True, slots=True) +class Vec2: + """A frozen displacement / direction in 2-space.""" + + x: float + y: float + + def __post_init__(self) -> None: + object.__setattr__(self, "x", float(self.x)) + object.__setattr__(self, "y", float(self.y)) + + @classmethod + def zero(cls) -> Self: + return cls(0.0, 0.0) + + @classmethod + def from_xy(cls, value: Xy) -> Self: + return cls(value.x, value.y) + + def as_tuple(self) -> tuple[float, float]: + return (self.x, self.y) + + def is_close( + self, + other: object, + *, + abs_tol: float = ABS_TOL, + rel_tol: float = REL_TOL, + ) -> bool: + if not isinstance(other, Vec2): + return False + return close_scalars( + self.x, other.x, abs_tol=abs_tol, rel_tol=rel_tol + ) and close_scalars(self.y, other.y, abs_tol=abs_tol, rel_tol=rel_tol) + + def with_x(self, x: float) -> Vec2: + return Vec2(x, self.y) + + def with_y(self, y: float) -> Vec2: + return Vec2(self.x, y) + + def dot(self, other: Vec2) -> float: + if not isinstance(other, Vec2): + raise TypeError("dot expects a Vec2") + return self.x * other.x + self.y * other.y + + def cross(self, other: Vec2) -> float: + """Signed parallelogram area of ``self`` × ``other``.""" + if not isinstance(other, Vec2): + raise TypeError("cross expects a Vec2") + return self.x * other.y - self.y * other.x + + def magnitude(self) -> float: + return math.sqrt(self.x * self.x + self.y * self.y) + + def normalized(self) -> Vec2: + mag = self.magnitude() + if mag == 0.0: + raise ValueError("cannot normalize a zero vector") + return self / mag + + def angle_to(self, other: Vec2) -> float: + if not isinstance(other, Vec2): + raise TypeError("angle_to expects a Vec2") + cosine = self.normalized().dot(other.normalized()) + return math.acos(max(-1.0, min(1.0, cosine))) + + def __add__(self, other: object) -> Vec2: + if isinstance(other, Vec2): + return Vec2(self.x + other.x, self.y + other.y) + return NotImplemented + + def __sub__(self, other: object) -> Vec2: + if isinstance(other, Vec2): + return Vec2(self.x - other.x, self.y - other.y) + return NotImplemented + + def __neg__(self) -> Vec2: + return Vec2(-self.x, -self.y) + + def __mul__(self, other: object) -> Vec2: + if isinstance(other, bool) or not isinstance(other, numbers.Real): + return NotImplemented + scalar = float(other) + return Vec2(self.x * scalar, self.y * scalar) + + def __rmul__(self, other: object) -> Vec2: + return self.__mul__(other) + + def __truediv__(self, other: object) -> Vec2: + if isinstance(other, bool) or not isinstance(other, numbers.Real): + return NotImplemented + scalar = float(other) + return Vec2(self.x / scalar, self.y / scalar) + + def __iter__(self) -> Iterator[float]: + yield self.x + yield self.y + + def __getitem__(self, index: int) -> float: + try: + return (self.x, self.y)[index] + except IndexError: + raise IndexError(index) from None diff --git a/src/cwmath/vec3.py b/src/cwmath/vec3.py new file mode 100644 index 0000000..0d15812 --- /dev/null +++ b/src/cwmath/vec3.py @@ -0,0 +1,130 @@ +"""Frozen 3D vector. Affine: Vec3 + Vec3 → Vec3; Vec3 + Point3 is illegal.""" + +from __future__ import annotations + +import math +import numbers +from collections.abc import Iterator +from dataclasses import dataclass +from typing import Self + +from cwmath.point3 import Xyz +from cwmath.tolerance import ABS_TOL, REL_TOL +from cwmath.tolerance import is_close as close_scalars + + +@dataclass(frozen=True, slots=True) +class Vec3: + """A frozen displacement / direction in 3-space.""" + + x: float + y: float + z: float + + def __post_init__(self) -> None: + object.__setattr__(self, "x", float(self.x)) + object.__setattr__(self, "y", float(self.y)) + object.__setattr__(self, "z", float(self.z)) + + @classmethod + def zero(cls) -> Self: + return cls(0.0, 0.0, 0.0) + + @classmethod + def from_xyz(cls, value: Xyz) -> Self: + return cls(value.x, value.y, value.z) + + def as_tuple(self) -> tuple[float, float, float]: + return (self.x, self.y, self.z) + + def is_close( + self, + other: object, + *, + abs_tol: float = ABS_TOL, + rel_tol: float = REL_TOL, + ) -> bool: + if not isinstance(other, Vec3): + return False + return ( + close_scalars(self.x, other.x, abs_tol=abs_tol, rel_tol=rel_tol) + and close_scalars(self.y, other.y, abs_tol=abs_tol, rel_tol=rel_tol) + and close_scalars(self.z, other.z, abs_tol=abs_tol, rel_tol=rel_tol) + ) + + def with_x(self, x: float) -> Vec3: + return Vec3(x, self.y, self.z) + + def with_y(self, y: float) -> Vec3: + return Vec3(self.x, y, self.z) + + def with_z(self, z: float) -> Vec3: + return Vec3(self.x, self.y, z) + + def dot(self, other: Vec3) -> float: + if not isinstance(other, Vec3): + raise TypeError("dot expects a Vec3") + return self.x * other.x + self.y * other.y + self.z * other.z + + def cross(self, other: Vec3) -> Vec3: + if not isinstance(other, Vec3): + raise TypeError("cross expects a Vec3") + return Vec3( + self.y * other.z - self.z * other.y, + self.z * other.x - self.x * other.z, + self.x * other.y - self.y * other.x, + ) + + def magnitude(self) -> float: + return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z) + + def normalized(self) -> Vec3: + mag = self.magnitude() + if mag == 0.0: + raise ValueError("cannot normalize a zero vector") + return self / mag + + def angle_to(self, other: Vec3) -> float: + if not isinstance(other, Vec3): + raise TypeError("angle_to expects a Vec3") + cosine = self.normalized().dot(other.normalized()) + return math.acos(max(-1.0, min(1.0, cosine))) + + def __add__(self, other: object) -> Vec3: + if isinstance(other, Vec3): + return Vec3(self.x + other.x, self.y + other.y, self.z + other.z) + return NotImplemented + + def __sub__(self, other: object) -> Vec3: + if isinstance(other, Vec3): + return Vec3(self.x - other.x, self.y - other.y, self.z - other.z) + return NotImplemented + + def __neg__(self) -> Vec3: + return Vec3(-self.x, -self.y, -self.z) + + def __mul__(self, other: object) -> Vec3: + if isinstance(other, bool) or not isinstance(other, numbers.Real): + return NotImplemented + scalar = float(other) + return Vec3(self.x * scalar, self.y * scalar, self.z * scalar) + + def __rmul__(self, other: object) -> Vec3: + return self.__mul__(other) + + def __truediv__(self, other: object) -> Vec3: + if isinstance(other, bool) or not isinstance(other, numbers.Real): + return NotImplemented + scalar = float(other) + return Vec3(self.x / scalar, self.y / scalar, self.z / scalar) + + def __iter__(self) -> Iterator[float]: + yield self.x + yield self.y + yield self.z + + def __getitem__(self, index: int) -> float: + try: + return (self.x, self.y, self.z)[index] + except IndexError: + raise IndexError(index) from None diff --git a/tests/fakes.py b/tests/fakes.py new file mode 100644 index 0000000..52d29e7 --- /dev/null +++ b/tests/fakes.py @@ -0,0 +1,35 @@ +"""Duck-typed doubles for the cadwork host types. + +CI never imports ``cadwork``. Later seeds inject ``FakeCadwork`` as the +``point_3d`` factory and build values through ``FakePoint3d``. +""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True) +class FakePoint3d: + """Stand-in for ``cadwork.point_3d`` (``.x`` / ``.y`` / ``.z``).""" + + x: float + y: float + z: float + + def __getitem__(self, index: int) -> float: + if index == 0: + return self.x + if index == 1: + return self.y + if index == 2: + return self.z + raise IndexError(index) + + +class FakeCadwork: + """Stand-in for the host ``cadwork`` module object.""" + + @staticmethod + def point_3d(x: float, y: float, z: float) -> FakePoint3d: + return FakePoint3d(x, y, z) diff --git a/tests/test_cadwork.py b/tests/test_cadwork.py new file mode 100644 index 0000000..b5ff4f1 --- /dev/null +++ b/tests/test_cadwork.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import sys + +import pytest + +from cwmath.cadwork import CadworkNotAvailableError, to_point_3d +from cwmath.point3 import Point3 +from cwmath.vec3 import Vec3 +from fakes import FakeCadwork, FakePoint3d + + +def test_from_xyz_point3() -> None: + assert Point3.from_xyz(FakePoint3d(1, 2, 3)) == Point3(1, 2, 3) + + +def test_from_xyz_vec3() -> None: + assert Vec3.from_xyz(FakePoint3d(1, 2, 3)) == Vec3(1, 2, 3) + + +def test_to_point_3d_with_factory_point3() -> None: + result = to_point_3d(Point3(1, 2, 3), factory=FakeCadwork) + assert isinstance(result, FakePoint3d) + assert (result.x, result.y, result.z) == (1.0, 2.0, 3.0) + + +def test_to_point_3d_with_factory_vec3() -> None: + result = to_point_3d(Vec3(4, 5, 6), factory=FakeCadwork()) + assert isinstance(result, FakePoint3d) + assert (result.x, result.y, result.z) == (4.0, 5.0, 6.0) + + +def test_to_point_3d_not_available_without_host( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setitem(sys.modules, "cadwork", None) + with pytest.raises(CadworkNotAvailableError): + to_point_3d(Point3(1, 2, 3)) + + +def test_to_point_3d_rejects_raw_xyz() -> None: + with pytest.raises(TypeError): + to_point_3d(FakePoint3d(1, 2, 3)) # type: ignore[arg-type] diff --git a/tests/test_exports.py b/tests/test_exports.py new file mode 100644 index 0000000..3385b5e --- /dev/null +++ b/tests/test_exports.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import importlib.metadata + +import cwmath + +EXPECTED_EXPORTS = { + "ABS_TOL", + "REL_TOL", + "Frame3", + "Line3", + "Plane3", + "Point2", + "Point3", + "Vec2", + "Vec3", + "is_close", + "project_point_on_line", + "project_point_on_plane", + "to_point_3d", +} + +LEGACY_NAMES = ("CwVector3d", "CwPlane3d", "cwexamples") + + +def test_top_level_import_list() -> None: + from cwmath import ( + ABS_TOL, + REL_TOL, + Frame3, + Line3, + Plane3, + Point2, + Point3, + Vec2, + Vec3, + is_close, + project_point_on_line, + project_point_on_plane, + to_point_3d, + ) + + assert Point3 is cwmath.Point3 + assert Vec3 is cwmath.Vec3 + assert Point2 is cwmath.Point2 + assert Vec2 is cwmath.Vec2 + assert Frame3 is cwmath.Frame3 + assert Plane3 is cwmath.Plane3 + assert Line3 is cwmath.Line3 + assert to_point_3d is cwmath.to_point_3d + assert project_point_on_line is cwmath.project_point_on_line + assert project_point_on_plane is cwmath.project_point_on_plane + assert is_close is cwmath.is_close + assert ABS_TOL is cwmath.ABS_TOL + assert REL_TOL is cwmath.REL_TOL + assert set(cwmath.__all__) == EXPECTED_EXPORTS + + +def test_no_legacy_names_or_version() -> None: + for name in LEGACY_NAMES: + assert not hasattr(cwmath, name) + assert name not in cwmath.__all__ + assert importlib.metadata.version("cwmath") == "1.0.0" diff --git a/tests/test_fakes.py b/tests/test_fakes.py new file mode 100644 index 0000000..027bcae --- /dev/null +++ b/tests/test_fakes.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import pytest + +from fakes import FakeCadwork, FakePoint3d + + +def test_fake_point3d_xyz() -> None: + point = FakePoint3d(1.0, 2.0, 3.0) + assert point.x == 1.0 + assert point.y == 2.0 + assert point.z == 3.0 + + +def test_fake_point3d_getitem() -> None: + point = FakePoint3d(1.0, 2.0, 3.0) + assert point[0] == 1.0 + assert point[1] == 2.0 + assert point[2] == 3.0 + with pytest.raises(IndexError): + _ = point[3] + + +def test_fake_point3d_is_frozen() -> None: + point = FakePoint3d(1.0, 2.0, 3.0) + with pytest.raises(AttributeError): + point.x = 9.0 # type: ignore[misc] + + +def test_fake_cadwork_point_3d() -> None: + point = FakeCadwork.point_3d(4.0, 5.0, 6.0) + assert isinstance(point, FakePoint3d) + assert (point.x, point.y, point.z) == (4.0, 5.0, 6.0) diff --git a/tests/test_frame3.py b/tests/test_frame3.py new file mode 100644 index 0000000..63174cb --- /dev/null +++ b/tests/test_frame3.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +import math + +import pytest + +from cwmath.frame3 import Frame3 +from cwmath.point3 import Point3 +from cwmath.tolerance import ABS_TOL +from cwmath.vec3 import Vec3 + + +def _identity(origin: Point3 | None = None) -> Frame3: + return Frame3.from_origin_and_axes( + origin if origin is not None else Point3(0, 0, 0), + Vec3(1, 0, 0), + Vec3(0, 1, 0), + Vec3(0, 0, 1), + ) + + +def _rotated_translated() -> Frame3: + """90° about Z, origin at (10, 20, 30).""" + return Frame3.from_origin_and_axes( + Point3(10, 20, 30), + Vec3(0, 1, 0), + Vec3(-1, 0, 0), + Vec3(0, 0, 1), + ) + + +def test_construct_identity_axes_are_unit() -> None: + frame = _identity() + assert frame.x_axis.is_close(Vec3(1, 0, 0)) + assert frame.y_axis.is_close(Vec3(0, 1, 0)) + assert frame.z_axis.is_close(Vec3(0, 0, 1)) + assert abs(frame.x_axis.magnitude() - 1.0) <= ABS_TOL + assert abs(frame.y_axis.magnitude() - 1.0) <= ABS_TOL + assert abs(frame.z_axis.magnitude() - 1.0) <= ABS_TOL + + +def test_construct_rotated_translated_frame() -> None: + frame = _rotated_translated() + assert frame.origin == Point3(10, 20, 30) + assert abs(frame.x_axis.magnitude() - 1.0) <= ABS_TOL + assert abs(frame.y_axis.magnitude() - 1.0) <= ABS_TOL + assert abs(frame.z_axis.magnitude() - 1.0) <= ABS_TOL + assert frame.x_axis.cross(frame.y_axis).is_close(frame.z_axis) + + +def test_construct_from_origin_xy_builds_right_handed_unit() -> None: + frame = Frame3.from_origin_xy( + Point3(1, 2, 3), + Vec3(2, 0, 0), + Vec3(1, 1, 0), + ) + assert frame.origin == Point3(1, 2, 3) + assert frame.x_axis.is_close(Vec3(1, 0, 0)) + assert frame.y_axis.is_close(Vec3(0, 1, 0)) + assert frame.z_axis.is_close(Vec3(0, 0, 1)) + assert frame.x_axis.cross(frame.y_axis).is_close(frame.z_axis) + + +def test_construct_bad_basis_non_unit_raises() -> None: + with pytest.raises(ValueError): + Frame3.from_origin_and_axes( + Point3(0, 0, 0), + Vec3(2, 0, 0), + Vec3(0, 1, 0), + Vec3(0, 0, 1), + ) + + +def test_construct_bad_basis_not_orthogonal_raises() -> None: + diagonal = Vec3(1 / math.sqrt(2), 1 / math.sqrt(2), 0) + with pytest.raises(ValueError): + Frame3.from_origin_and_axes( + Point3(0, 0, 0), + Vec3(1, 0, 0), + diagonal, + Vec3(0, 0, 1), + ) + + +def test_construct_bad_basis_left_handed_raises() -> None: + with pytest.raises(ValueError): + Frame3.from_origin_and_axes( + Point3(0, 0, 0), + Vec3(1, 0, 0), + Vec3(0, 1, 0), + Vec3(0, 0, -1), + ) + + +def test_construct_from_origin_xy_degenerate_raises() -> None: + origin = Point3(0, 0, 0) + with pytest.raises(ValueError): + Frame3.from_origin_xy(origin, Vec3(1, 0, 0), Vec3(2, 0, 0)) + with pytest.raises(ValueError): + Frame3.from_origin_xy(origin, Vec3(0, 0, 0), Vec3(0, 1, 0)) + + +def test_point_roundtrip_identity() -> None: + frame = _identity() + point = Point3(1, 2, 3) + assert frame.to_world(point) == point + assert frame.to_local(point) == point + assert frame.to_local(frame.to_world(point)).is_close(point) + + +def test_point_roundtrip_translated_rotated() -> None: + frame = _rotated_translated() + point = Point3(1, 2, 3) + world = frame.to_world(point) + assert world.is_close(Point3(8, 21, 33)) + assert frame.to_local(world).is_close(point) + assert frame.to_local(frame.to_world(point)).is_close(point) + + +def test_vec_roundtrip_ignores_origin() -> None: + vector = Vec3(1, 2, 3) + at_origin = _identity() + shifted = _identity(Point3(100, -40, 7)) + assert at_origin.to_world(vector).is_close(shifted.to_world(vector)) + assert at_origin.to_world(vector).is_close(vector) + assert shifted.to_world(vector).is_close(vector) + assert isinstance(shifted.to_world(vector), Vec3) + + +def test_vec_roundtrip_translated_rotated() -> None: + frame = _rotated_translated() + vector = Vec3(1, 2, 3) + world = frame.to_world(vector) + assert world.is_close(Vec3(-2, 1, 3)) + assert isinstance(world, Vec3) + assert frame.to_local(world).is_close(vector) + assert frame.to_local(frame.to_world(vector)).is_close(vector) + + +def test_frame3_is_frozen() -> None: + frame = _identity() + with pytest.raises(AttributeError): + frame.origin = Point3(1, 0, 0) # type: ignore[misc] diff --git a/tests/test_line3.py b/tests/test_line3.py new file mode 100644 index 0000000..5f62195 --- /dev/null +++ b/tests/test_line3.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +import pytest + +from cwmath.line3 import Line3, project_point_on_line +from cwmath.point3 import Point3 +from cwmath.vec3 import Vec3 + + +def test_line3_construct_from_origin_direction_stores_unit_direction() -> None: + line = Line3.from_origin_direction(Point3(1, 2, 3), Vec3(2, 0, 0)) + assert line.origin == Point3(1, 2, 3) + assert line.direction.is_close(Vec3(1, 0, 0)) + assert line.direction.magnitude() == pytest.approx(1.0) + + +def test_line3_construct_zero_direction_raises() -> None: + with pytest.raises(ValueError): + Line3.from_origin_direction(Point3(0, 0, 0), Vec3(0, 0, 0)) + + +def test_line3_construct_from_points() -> None: + line = Line3.from_points(Point3(0, 0, 0), Point3(0, 4, 0)) + assert line.origin == Point3(0, 0, 0) + assert line.direction.is_close(Vec3(0, 1, 0)) + + +def test_line3_construct_from_points_coincident_raises() -> None: + with pytest.raises(ValueError): + Line3.from_points(Point3(1, 2, 3), Point3(1, 2, 3)) + + +def test_line3_project_onto_x_axis() -> None: + line = Line3.from_origin_direction(Point3(0, 0, 0), Vec3(1, 0, 0)) + foot = line.project(Point3(1, 1, 0)) + assert foot.is_close(Point3(1, 0, 0)) + assert line.parameter(Point3(1, 1, 0)) == pytest.approx(1.0) + + +def test_line3_project_stable_when_already_on_line() -> None: + line = Line3.from_origin_direction(Point3(0, 0, 0), Vec3(1, 0, 0)) + on_line = Point3(3, 0, 0) + assert line.project(on_line).is_close(on_line) + assert line.project(line.origin).is_close(line.origin) + + +def test_project_point_on_line_matches_method() -> None: + line = Line3.from_origin_direction(Point3(0, 0, 0), Vec3(1, 0, 0)) + point = Point3(1, 1, 0) + assert project_point_on_line(point, line) == line.project(point) diff --git a/tests/test_packaging.py b/tests/test_packaging.py new file mode 100644 index 0000000..efa043e --- /dev/null +++ b/tests/test_packaging.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +import subprocess +import zipfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def test_wheel_excludes_tests(tmp_path: Path) -> None: + subprocess.run( + ["uv", "build", "--wheel", "--out-dir", str(tmp_path)], + check=True, + cwd=ROOT, + ) + wheels = list(tmp_path.glob("cwmath-*.whl")) + assert len(wheels) == 1 + names = zipfile.ZipFile(wheels[0]).namelist() + assert any(name.startswith("cwmath/") for name in names) + assert any( + name == "cwmath/py.typed" or name.endswith("/cwmath/py.typed") for name in names + ) + assert not any( + name.startswith("cwmath/tests/") or name.startswith("tests/") for name in names + ) + lowered = [name.lower() for name in names] + assert not any("cwvector3d" in name or "cwplane3d" in name for name in lowered) + + +def test_kept_files_present() -> None: + assert (ROOT / "CODEOWNERS").is_file() + assert (ROOT / "LICENSE").is_file() diff --git a/tests/test_plane3.py b/tests/test_plane3.py new file mode 100644 index 0000000..e0e7701 --- /dev/null +++ b/tests/test_plane3.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import pytest + +from cwmath.plane3 import Plane3, project_point_on_plane +from cwmath.point3 import Point3 +from cwmath.vec3 import Vec3 + + +def test_plane3_construct_from_point_normal_stores_unit_normal() -> None: + plane = Plane3.from_point_normal(Point3(1, 2, 3), Vec3(0, 0, 2)) + assert plane.point == Point3(1, 2, 3) + assert plane.normal.is_close(Vec3(0, 0, 1)) + assert plane.normal.magnitude() == pytest.approx(1.0) + + +def test_plane3_construct_zero_normal_raises() -> None: + with pytest.raises(ValueError): + Plane3.from_point_normal(Point3(0, 0, 0), Vec3(0, 0, 0)) + + +def test_plane3_distance_axis_aligned() -> None: + plane = Plane3.from_point_normal(Point3(0, 0, 0), Vec3(0, 0, 1)) + above = Point3(0, 0, 5) + below = Point3(0, 0, -3) + assert plane.signed_distance(above) == 5.0 + assert plane.distance(above) == 5.0 + assert plane.signed_distance(below) == -3.0 + assert plane.distance(below) == 3.0 + + +def test_plane3_contains_constructing_point() -> None: + origin = Point3(1, 2, 3) + plane = Plane3.from_point_normal(origin, Vec3(0, 0, 1)) + assert plane.contains(origin) is True + assert plane.contains(Point3(4, 5, 3)) is True + assert plane.contains(Point3(1, 2, 4)) is False + + +def test_plane3_project_onto_xy() -> None: + plane = Plane3.from_point_normal(Point3(0, 0, 0), Vec3(0, 0, 1)) + foot = plane.project(Point3(0, 0, 5)) + assert foot.is_close(Point3(0, 0, 0)) + assert plane.contains(foot) + + +def test_plane3_project_stable_when_already_on_plane() -> None: + plane = Plane3.from_point_normal(Point3(0, 0, 0), Vec3(0, 0, 1)) + on_plane = Point3(4, 5, 0) + assert plane.project(on_plane).is_close(on_plane) + assert plane.project(plane.point).is_close(plane.point) + + +def test_project_point_on_plane_matches_method() -> None: + plane = Plane3.from_point_normal(Point3(0, 0, 0), Vec3(0, 0, 1)) + point = Point3(1, 2, 5) + assert project_point_on_plane(point, plane) == plane.project(point) diff --git a/tests/test_point2.py b/tests/test_point2.py new file mode 100644 index 0000000..48a578b --- /dev/null +++ b/tests/test_point2.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import math + +import pytest + +from cwmath.point2 import Point2 +from cwmath.tolerance import ABS_TOL +from cwmath.vec2 import Vec2 + + +def test_point2_plus_vec2_is_point2() -> None: + result = Point2(1, 2) + Vec2(4, 5) + assert isinstance(result, Point2) + assert result == Point2(5, 7) + + +def test_point2_minus_point2_is_vec2() -> None: + result = Point2(5, 7) - Point2(1, 2) + assert isinstance(result, Vec2) + assert result == Vec2(4, 5) + + +def test_point2_plus_point2_raises_typeerror() -> None: + with pytest.raises(TypeError): + _ = Point2(1, 2) + Point2(4, 5) # type: ignore[operator] + + +def test_point2_minus_vec2_is_point2() -> None: + result = Point2(5, 7) - Vec2(4, 5) + assert result == Point2(1, 2) + + +def test_affine_roundtrip() -> None: + p = Point2(1, 2) + q = Point2(4, 6) + assert p + (q - p) == q + + +def test_point2_is_frozen() -> None: + point = Point2(1, 2) + with pytest.raises(AttributeError): + point.x = 9.0 # type: ignore[misc] + + +def test_point2_equality_is_exact() -> None: + assert Point2(1, 2) == Point2(1, 2) + assert Point2(1, 2) != Point2(1, 2 + 1e-12) + assert Point2(1, 2) != Vec2(1, 2) + + +def test_point2_is_close() -> None: + assert Point2(0, 0).is_close(Point2(0, ABS_TOL)) + assert not Point2(0, 0).is_close(Point2(0, ABS_TOL * 2)) + assert not Point2(1, 2).is_close(Vec2(1, 2)) + + +def test_point2_from_tuple_helpers() -> None: + point = Point2(1, 2) + assert point.as_tuple() == (1.0, 2.0) + assert point.with_x(9) == Point2(9, 2) + assert point.with_y(9) == Point2(1, 9) + + +def test_point2_from_xy() -> None: + class _Xy: + def __init__(self, x: float, y: float) -> None: + self.x = x + self.y = y + + assert Point2.from_xy(_Xy(3, 4)) == Point2(3, 4) + + +def test_point2_distance_and_lerp() -> None: + a = Point2(0, 0) + b = Point2(3, 4) + assert a.distance(b) == 5.0 + assert a.lerp(b, 0.5) == Point2(1.5, 2.0) + + +def test_point2_indexing_and_iteration_are_read_only() -> None: + point = Point2(1, 2) + assert point[0] == 1.0 + assert point[1] == 2.0 + assert list(point) == [1.0, 2.0] + with pytest.raises(IndexError): + _ = point[2] + with pytest.raises(TypeError): + point[0] = 9.0 # type: ignore[index] + + +def test_point2_distance_rejects_vec2() -> None: + with pytest.raises(TypeError): + Point2(0, 0).distance(Vec2(1, 0)) # type: ignore[arg-type] + + +def test_point2_lerp_endpoints() -> None: + a = Point2(1, 2) + b = Point2(4, 6) + assert a.lerp(b, 0.0) == a + assert a.lerp(b, 1.0) == b + assert math.isclose(a.lerp(b, 0.25).x, 1.75, abs_tol=ABS_TOL) diff --git a/tests/test_point3.py b/tests/test_point3.py new file mode 100644 index 0000000..9aac266 --- /dev/null +++ b/tests/test_point3.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +import math + +import pytest + +from cwmath.point3 import Point3 +from cwmath.tolerance import ABS_TOL +from cwmath.vec3 import Vec3 + + +def test_point3_plus_vec3_is_point3() -> None: + result = Point3(1, 2, 3) + Vec3(4, 5, 6) + assert isinstance(result, Point3) + assert result == Point3(5, 7, 9) + + +def test_point3_minus_point3_is_vec3() -> None: + result = Point3(5, 7, 9) - Point3(1, 2, 3) + assert isinstance(result, Vec3) + assert result == Vec3(4, 5, 6) + + +def test_point3_plus_point3_raises_typeerror() -> None: + with pytest.raises(TypeError): + _ = Point3(1, 2, 3) + Point3(4, 5, 6) # type: ignore[operator] + + +def test_point3_minus_vec3_is_point3() -> None: + result = Point3(5, 7, 9) - Vec3(4, 5, 6) + assert result == Point3(1, 2, 3) + + +def test_affine_roundtrip() -> None: + p = Point3(1, 2, 3) + q = Point3(4, 6, 8) + assert p + (q - p) == q + + +def test_point3_is_frozen() -> None: + point = Point3(1, 2, 3) + with pytest.raises(AttributeError): + point.x = 9.0 # type: ignore[misc] + + +def test_point3_equality_is_exact() -> None: + assert Point3(1, 2, 3) == Point3(1, 2, 3) + assert Point3(1, 2, 3) != Point3(1, 2, 3 + 1e-12) + assert Point3(1, 2, 3) != Vec3(1, 2, 3) + + +def test_point3_is_close() -> None: + assert Point3(0, 0, 0).is_close(Point3(0, 0, ABS_TOL)) + assert not Point3(0, 0, 0).is_close(Point3(0, 0, ABS_TOL * 2)) + assert not Point3(1, 2, 3).is_close(Vec3(1, 2, 3)) + + +def test_point3_from_tuple_helpers() -> None: + point = Point3(1, 2, 3) + assert point.as_tuple() == (1.0, 2.0, 3.0) + assert point.with_x(9) == Point3(9, 2, 3) + assert point.with_y(9) == Point3(1, 9, 3) + assert point.with_z(9) == Point3(1, 2, 9) + + +def test_point3_distance_and_lerp() -> None: + a = Point3(0, 0, 0) + b = Point3(3, 4, 0) + assert a.distance(b) == 5.0 + assert a.lerp(b, 0.5) == Point3(1.5, 2.0, 0.0) + + +def test_point3_indexing_and_iteration_are_read_only() -> None: + point = Point3(1, 2, 3) + assert point[0] == 1.0 + assert point[1] == 2.0 + assert point[2] == 3.0 + assert list(point) == [1.0, 2.0, 3.0] + with pytest.raises(IndexError): + _ = point[3] + with pytest.raises(TypeError): + point[0] = 9.0 # type: ignore[index] + + +def test_point3_distance_rejects_vec3() -> None: + with pytest.raises(TypeError): + Point3(0, 0, 0).distance(Vec3(1, 0, 0)) # type: ignore[arg-type] + + +def test_point3_lerp_endpoints() -> None: + a = Point3(1, 2, 3) + b = Point3(4, 6, 8) + assert a.lerp(b, 0.0) == a + assert a.lerp(b, 1.0) == b + assert math.isclose(a.lerp(b, 0.25).x, 1.75, abs_tol=ABS_TOL) diff --git a/tests/test_readme_example.py b/tests/test_readme_example.py new file mode 100644 index 0000000..010872d --- /dev/null +++ b/tests/test_readme_example.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import re +import sys +import types +from pathlib import Path + +from cwmath import Point3 +from fakes import FakeCadwork, FakePoint3d + +README = Path(__file__).resolve().parents[1] / "README.md" + + +def _readme_python_example() -> str: + text = README.read_text(encoding="utf-8") + match = re.search(r"```python\n(.*?)```", text, re.S) + assert match is not None, "README is missing a python example fence" + return match.group(1) + + +def test_readme_example_converts_frames_and_projects() -> None: + cadwork = types.ModuleType("cadwork") + cadwork.point_3d = FakeCadwork.point_3d # type: ignore[attr-defined] + sys.modules["cadwork"] = cadwork + try: + namespace: dict[str, object] = {} + exec(_readme_python_example(), namespace) # noqa: S102 + finally: + sys.modules.pop("cadwork", None) + + world = namespace["world"] + foot = namespace["foot"] + result = namespace["result"] + assert isinstance(world, Point3) + assert isinstance(foot, Point3) + assert world.is_close(Point3(1050.0, 2025.0, 10.0)) + assert foot.is_close(Point3(1050.0, 2025.0, 0.0)) + assert isinstance(result, FakePoint3d) + assert (result.x, result.y, result.z) == (1050.0, 2025.0, 0.0) diff --git a/tests/test_tolerance.py b/tests/test_tolerance.py new file mode 100644 index 0000000..a76ddf8 --- /dev/null +++ b/tests/test_tolerance.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +import cwmath +from cwmath.tolerance import ABS_TOL, REL_TOL, is_close + + +def test_package_imports() -> None: + assert cwmath.__name__ == "cwmath" + + +def test_default_tolerances() -> None: + assert ABS_TOL == 1e-6 + assert REL_TOL == 1e-9 + + +def test_is_close_exact() -> None: + assert is_close(1.0, 1.0) + assert is_close(0.0, 0.0) + + +def test_is_close_within_abs_tol() -> None: + assert is_close(0.0, ABS_TOL) + assert is_close(1.0, 1.0 + ABS_TOL) + + +def test_is_close_outside_abs_tol() -> None: + assert not is_close(0.0, ABS_TOL * 2) + assert not is_close(1.0, 1.0 + 1e-3) + + +def test_is_close_respects_rel_tol_for_large_values() -> None: + large = 1e9 + # |Δ| = 0.5 is above ABS_TOL but within REL_TOL * |large| (= 1.0). + assert is_close(large, large + 0.5) + assert not is_close(large, large + 2.0) + + +def test_is_close_accepts_override_tolerances() -> None: + assert is_close(0.0, 0.5, abs_tol=0.5, rel_tol=0.0) + assert not is_close(0.0, 0.5, abs_tol=0.1, rel_tol=0.0) diff --git a/tests/test_vec2.py b/tests/test_vec2.py new file mode 100644 index 0000000..610ef03 --- /dev/null +++ b/tests/test_vec2.py @@ -0,0 +1,110 @@ +from __future__ import annotations + +import math + +import pytest + +from cwmath.point2 import Point2 +from cwmath.tolerance import ABS_TOL +from cwmath.vec2 import Vec2 + + +def test_vec2_plus_vec2_is_vec2() -> None: + result = Vec2(1, 2) + Vec2(4, 5) + assert isinstance(result, Vec2) + assert result == Vec2(5, 7) + + +def test_vec2_plus_point2_raises_typeerror() -> None: + with pytest.raises(TypeError): + _ = Vec2(1, 2) + Point2(4, 5) # type: ignore[operator] + + +def test_vec2_dot_exact_on_integers() -> None: + assert Vec2(1, 2).dot(Vec2(4, 5)) == 14 + assert Vec2(1, 0).dot(Vec2(0, 1)) == 0 + + +def test_vec2_cross_axis_pair_is_scalar() -> None: + assert Vec2(1, 0).cross(Vec2(0, 1)) == 1.0 + assert Vec2(0, 1).cross(Vec2(1, 0)) == -1.0 + assert Vec2(2, 0).cross(Vec2(0, 3)) == 6.0 + assert Vec2(1, 0).cross(Vec2(1, 0)) == 0.0 + + +def test_vec2_magnitude() -> None: + assert Vec2(3, 4).magnitude() == 5.0 + assert Vec2.zero().magnitude() == 0.0 + assert abs(Vec2(1, 1).magnitude() - math.sqrt(2.0)) <= ABS_TOL + + +def test_vec2_normalized_unit_length() -> None: + unit = Vec2(3, 4).normalized() + assert abs(unit.magnitude() - 1.0) <= ABS_TOL + assert unit.is_close(Vec2(0.6, 0.8)) + + +def test_vec2_normalize_zero_raises() -> None: + with pytest.raises(ValueError): + Vec2(0, 0).normalized() + + +def test_vec2_angle_to_radians() -> None: + angle = Vec2(1, 0).angle_to(Vec2(0, 1)) + assert abs(angle - math.pi / 2) <= ABS_TOL + assert abs(Vec2(1, 0).angle_to(Vec2(1, 0))) <= ABS_TOL + + +def test_vec2_angle_to_zero_raises() -> None: + with pytest.raises(ValueError): + Vec2(1, 0).angle_to(Vec2.zero()) + + +def test_vec2_scale_and_neg() -> None: + assert Vec2(1, 2) * 2 == Vec2(2, 4) + assert 2 * Vec2(1, 2) == Vec2(2, 4) + assert Vec2(2, 4) / 2 == Vec2(1, 2) + assert -Vec2(1, 2) == Vec2(-1, -2) + + +def test_vec2_is_frozen() -> None: + vector = Vec2(1, 2) + with pytest.raises(AttributeError): + vector.x = 9.0 # type: ignore[misc] + + +def test_vec2_equality_is_exact() -> None: + assert Vec2(1, 2) == Vec2(1, 2) + assert Vec2(1, 2) != Vec2(1, 2 + 1e-12) + + +def test_vec2_is_close() -> None: + assert Vec2(0, 0).is_close(Vec2(0, ABS_TOL)) + assert not Vec2(0, 0).is_close(Vec2(0, ABS_TOL * 2)) + + +def test_vec2_indexing_and_iteration_are_read_only() -> None: + vector = Vec2(1, 2) + assert vector[0] == 1.0 + assert vector[1] == 2.0 + assert list(vector) == [1.0, 2.0] + with pytest.raises(IndexError): + _ = vector[2] + with pytest.raises(TypeError): + vector[0] = 9.0 # type: ignore[index] + + +def test_vec2_as_tuple_and_with() -> None: + vector = Vec2(1, 2) + assert vector.as_tuple() == (1.0, 2.0) + assert vector.with_x(0) == Vec2(0, 2) + assert vector.with_y(0) == Vec2(1, 0) + + +def test_vec2_from_xy() -> None: + class _Xy: + def __init__(self, x: float, y: float) -> None: + self.x = x + self.y = y + + assert Vec2.from_xy(_Xy(3, 4)) == Vec2(3, 4) diff --git a/tests/test_vec3.py b/tests/test_vec3.py new file mode 100644 index 0000000..e817805 --- /dev/null +++ b/tests/test_vec3.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +import math + +import pytest + +from cwmath.point3 import Point3 +from cwmath.tolerance import ABS_TOL +from cwmath.vec3 import Vec3 + + +def test_vec3_plus_vec3_is_vec3() -> None: + result = Vec3(1, 2, 3) + Vec3(4, 5, 6) + assert isinstance(result, Vec3) + assert result == Vec3(5, 7, 9) + + +def test_vec3_plus_point3_raises_typeerror() -> None: + with pytest.raises(TypeError): + _ = Vec3(1, 2, 3) + Point3(4, 5, 6) # type: ignore[operator] + + +def test_vec3_dot_exact_on_integers() -> None: + assert Vec3(1, 2, 3).dot(Vec3(4, 5, 6)) == 32 + assert Vec3(1, 0, 0).dot(Vec3(0, 1, 0)) == 0 + + +def test_vec3_cross_axis_triple_is_orthogonal() -> None: + i = Vec3(1, 0, 0) + j = Vec3(0, 1, 0) + k = i.cross(j) + assert k == Vec3(0, 0, 1) + assert k.dot(i) == 0 + assert k.dot(j) == 0 + + +def test_vec3_magnitude() -> None: + assert Vec3(3, 4, 0).magnitude() == 5.0 + assert Vec3.zero().magnitude() == 0.0 + assert abs(Vec3(1, 1, 1).magnitude() - math.sqrt(3.0)) <= ABS_TOL + + +def test_vec3_normalized_unit_length() -> None: + unit = Vec3(3, 4, 0).normalized() + assert abs(unit.magnitude() - 1.0) <= ABS_TOL + assert unit.is_close(Vec3(0.6, 0.8, 0.0)) + + +def test_vec3_normalize_zero_raises() -> None: + with pytest.raises(ValueError): + Vec3(0, 0, 0).normalized() + + +def test_vec3_angle_to_radians() -> None: + angle = Vec3(1, 0, 0).angle_to(Vec3(0, 1, 0)) + assert abs(angle - math.pi / 2) <= ABS_TOL + assert abs(Vec3(1, 0, 0).angle_to(Vec3(1, 0, 0))) <= ABS_TOL + + +def test_vec3_angle_to_zero_raises() -> None: + with pytest.raises(ValueError): + Vec3(1, 0, 0).angle_to(Vec3.zero()) + + +def test_vec3_scale_and_neg() -> None: + assert Vec3(1, 2, 3) * 2 == Vec3(2, 4, 6) + assert 2 * Vec3(1, 2, 3) == Vec3(2, 4, 6) + assert Vec3(2, 4, 6) / 2 == Vec3(1, 2, 3) + assert -Vec3(1, 2, 3) == Vec3(-1, -2, -3) + + +def test_vec3_is_frozen() -> None: + vector = Vec3(1, 2, 3) + with pytest.raises(AttributeError): + vector.x = 9.0 # type: ignore[misc] + + +def test_vec3_equality_is_exact() -> None: + assert Vec3(1, 2, 3) == Vec3(1, 2, 3) + assert Vec3(1, 2, 3) != Vec3(1, 2, 3 + 1e-12) + + +def test_vec3_is_close() -> None: + assert Vec3(0, 0, 0).is_close(Vec3(0, 0, ABS_TOL)) + assert not Vec3(0, 0, 0).is_close(Vec3(0, 0, ABS_TOL * 2)) + + +def test_vec3_indexing_and_iteration_are_read_only() -> None: + vector = Vec3(1, 2, 3) + assert vector[0] == 1.0 + assert list(vector) == [1.0, 2.0, 3.0] + with pytest.raises(IndexError): + _ = vector[3] + with pytest.raises(TypeError): + vector[0] = 9.0 # type: ignore[index] + + +def test_vec3_as_tuple_and_with() -> None: + vector = Vec3(1, 2, 3) + assert vector.as_tuple() == (1.0, 2.0, 3.0) + assert vector.with_x(0) == Vec3(0, 2, 3) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..4b05c0e --- /dev/null +++ b/uv.lock @@ -0,0 +1,626 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backrefs" +version = "8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/56/4744bcd0c82184e80c52b0ac4076c261a8ffa1f1b343ff2f6e89ce0e1cef/backrefs-8.0.tar.gz", hash = "sha256:b556cd7d36c3a3a2f256b89590b176b8eddfb73bcfaee3a3ddd84ea66d21ce50", size = 7013081, upload-time = "2026-07-26T19:54:24.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/fd/9bf53b6a6f6f519ffaac765df2f2a25e5c2fc6d32cfd2b2747099e72c911/backrefs-8.0-py310-none-any.whl", hash = "sha256:4a627b817fd2dce43b79ab48da63613340509381cd8ce0897078a0bce79a2ab8", size = 380377, upload-time = "2026-07-26T19:54:17.457Z" }, + { url = "https://files.pythonhosted.org/packages/e1/29/4bd7ae72a2634da00379c2b3bcc5439e7c94620235c6afea8af15229a973/backrefs-8.0-py311-none-any.whl", hash = "sha256:f0c35cf0102ba6b6070c12a492be3c1c1d3f5839529784b9a9565d6d04569a01", size = 392169, upload-time = "2026-07-26T19:54:18.782Z" }, + { url = "https://files.pythonhosted.org/packages/29/13/232505664e8e2a0c7a2eb0c505cfade9d715538f89a5d62bc4c272968f62/backrefs-8.0-py312-none-any.whl", hash = "sha256:87f0fae8c5f207fe9f4b2887efc71d42f4900ac78faa1af08d675ef303692dc5", size = 398084, upload-time = "2026-07-26T19:54:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/8a/69/47a3dc20abc4fa5486655fde681bd55e63211b46c886d8c02223d6468431/backrefs-8.0-py313-none-any.whl", hash = "sha256:601ce68ca12385dbda06ce264406b4c4210cf5b79fd0fd627592365c92f29a88", size = 400040, upload-time = "2026-07-26T19:54:21.194Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cf/e5f9b68a5b0e939a2fb933a66c20180d0c9241bf8927f7a47fa48c1675e9/backrefs-8.0-py314-none-any.whl", hash = "sha256:9ec96efa080938be92323e8e730e57718c9c88eb15ad70bbef4e1766df591408", size = 411903, upload-time = "2026-07-26T19:54:23.221Z" }, +] + +[[package]] +name = "black" +version = "26.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/37/5628dd55bf2b34257fc7603f0fe97c40e3aaf24265f416a9c85c95ca1436/black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73", size = 679439, upload-time = "2026-05-18T16:53:36.107Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/16/a8da8eb208c51c7f4ce74609a45d0dcc6d8a2141e45e81ee5289d1bb0d59/black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168", size = 2004800, upload-time = "2026-05-18T17:05:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/11/8a/a479296a19e383b70a725882a6cf3d786540601ff03cabbaaf1cce864c5a/black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3", size = 1815576, upload-time = "2026-05-18T17:05:40.309Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/cfaf3d39f25132c156a068f6b805576c9103a84086019507c70e1911ee7d/black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18", size = 1877927, upload-time = "2026-05-18T17:05:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/66/76/302e313964bcff7e28df329d39f84f5270095730d85ff0acc260610a0d82/black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50", size = 1511860, upload-time = "2026-05-18T17:05:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/27/4e/a3827e35e0e567f9f9ee59e2a0ab979267dca98718f25547ca8c6733afd4/black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae", size = 1316632, upload-time = "2026-05-18T17:05:45.521Z" }, + { url = "https://files.pythonhosted.org/packages/94/51/f975cae76d44274cc2868dc9040ac5d58d464784610234455b4e7b19c6ef/black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", size = 213693, upload-time = "2026-05-18T16:53:33.964Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/31/4971872b3ed8715346231fb6eb4da8fcba65a4143c189db151ee28a2812b/charset_normalizer-3.5.0.tar.gz", hash = "sha256:49bd5feb59b0bf3cbf6ebcf4352e371c95b9da9bacd4449f8b64d0ad2c10a26e", size = 169295, upload-time = "2026-08-12T14:35:31.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/14/d098868dac5ff27e0258f548b1c74c6484be528384965d8fcf8fc6a4011d/charset_normalizer-3.5.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:d95244906ed69d0f79f190893c65e336c15959003e21449256dc05c001b52ea2", size = 211664, upload-time = "2026-08-12T14:33:14.153Z" }, + { url = "https://files.pythonhosted.org/packages/e7/da/a944b32a46601ae5a4c3499e8d64ecd14fe82313f00da74dcdf00273a0b4/charset_normalizer-3.5.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d788e2ded0c4c47efa4d73cfe59eaf975ee32f425219873d2cb3e3fbaa00f636", size = 224375, upload-time = "2026-08-12T14:33:15.472Z" }, + { url = "https://files.pythonhosted.org/packages/f7/db/eabb5996be2f529744755e7b2fc9396eff4a64961f034e7fd49d54b9afb2/charset_normalizer-3.5.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:f9f91d3e8382900f3a68fa0ce94294479de9cd2de6bc0c70acd0f0dfd511836b", size = 194364, upload-time = "2026-08-12T14:33:16.607Z" }, + { url = "https://files.pythonhosted.org/packages/78/65/4ad3c5be108930310d8003f5602861d5b89f728293b9f09c3a4837f7ba10/charset_normalizer-3.5.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d54625cbf4e6b60bf0639728cb8b4cb541e340f6d7cafae5806051a40ddf4c45", size = 197643, upload-time = "2026-08-12T14:33:17.88Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8fee3201b98d52289be60a775797d69be05a04fb6cfb48c1587dad33e649/charset_normalizer-3.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:1f99a8c3a1da5d955edbad18208b3d627bdd54c48a6e739fa877bdca98c686d6", size = 341384, upload-time = "2026-08-12T14:33:19.239Z" }, + { url = "https://files.pythonhosted.org/packages/a7/dd/9e757101d1f76c35c0643684ba499ac3a181fb2b264c68174bf727d627e8/charset_normalizer-3.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf1e75dc07a3850b53d1e5f75e04d3ae12afe56284be7821771eaa2466350c73", size = 241637, upload-time = "2026-08-12T14:33:20.619Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/7857023015400bc4aa0a82fbcca29fa2dc7ec25f971a130764cb2dc7a589/charset_normalizer-3.5.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ac5a9cc079c67d75f4ddf343276031879eadbb333d1bb231cce297b8d7b9aae8", size = 226170, upload-time = "2026-08-12T14:33:21.773Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ae/8b52935b304f7b6bbf33151ed2b75266b09aa4b6f8f04230d948885b2577/charset_normalizer-3.5.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0dfe83c1b4d00abbf433998117a14f56a5c2bc68226c0d331709eed0d1ce539b", size = 265093, upload-time = "2026-08-12T14:33:22.999Z" }, + { url = "https://files.pythonhosted.org/packages/dc/78/6e838f6bb059f2c0afc60a4e7f294252f043c254656ad4114c50302cae4d/charset_normalizer-3.5.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e4e8fa586df2208ef040684751345f10f503834a757c9a74ecd19c1a2f9b1ccd", size = 262789, upload-time = "2026-08-12T14:33:24.214Z" }, + { url = "https://files.pythonhosted.org/packages/c2/08/189b27e51fddc9d6b3695331da0e31792c1d88b953ad854e57f06e9b2cc8/charset_normalizer-3.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82cc5835997ec78afe293a192e385099355770a7db94b2fb1239d36b32796f1c", size = 250580, upload-time = "2026-08-12T14:33:25.707Z" }, + { url = "https://files.pythonhosted.org/packages/ac/55/64854e99b25841f83e8e37d9df2f3d1f96f693439f80e5fabd542a7e47ab/charset_normalizer-3.5.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:19e52bda45086df8a4be4bb5910af6f5d9d3b538c78712c8ae09ef10b85bf458", size = 245008, upload-time = "2026-08-12T14:33:26.971Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/7720c904fa635d4260b4dced6029cf3d298c57b26741365d5a8d28c54043/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3418edd0ecb72a0a3861cf72f31be0ad9b7fe338ce2b58fb5cc80b9aeb792700", size = 243892, upload-time = "2026-08-12T14:33:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/70/50/7bfcb327631d4870c720872b548745f6ec8baa044d51c21b5d1d32ac4e3a/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:125ee619611019471b177c70bc3e9d4cda9fad7e01d93523501d3b188df0193a", size = 230996, upload-time = "2026-08-12T14:33:29.511Z" }, + { url = "https://files.pythonhosted.org/packages/24/51/40c45d6d940c04005ed721aa54bdebf1ebb2930f8a2ae537e8d60484fb27/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a3ad0e3da22852533858663848608f3f24c0d35e5cde415a4903476f2b4c88ec", size = 265834, upload-time = "2026-08-12T14:33:30.689Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d4/ef7a227ef89d215b47f9df79c3966610b17faa13bb2f236989207a631622/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:4ebebb410bc517e1d284c52a123e82704b21e4e7e26a21ebecf7439d0647b8a3", size = 245544, upload-time = "2026-08-12T14:33:31.859Z" }, + { url = "https://files.pythonhosted.org/packages/37/a9/a4ca9156964ded61c7718eba410ce11be2fd2b263fda4bcf08367b6578cd/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:91f9f7c151e772acebe489eaec96e96a2877202d7dd144e3f96b8676881715a0", size = 264110, upload-time = "2026-08-12T14:33:33.13Z" }, + { url = "https://files.pythonhosted.org/packages/38/6a/838364bb8702229c6e5f8b23f80ff0f052a12dfaf3113a12fd6acbe92a44/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:401ea6e7af9e7852ed818f64714b579c1935482049670847ca3bd7ba45dc63fb", size = 252303, upload-time = "2026-08-12T14:33:34.98Z" }, + { url = "https://files.pythonhosted.org/packages/9c/5f/d88032edce951f499a2321cf7ae0d35a043c74be12bc22d81084cc7afbcc/charset_normalizer-3.5.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f7496aed56b06325a1ad419c5bf23c6dd042558e874f71dd1b958f3e255f3053", size = 139964, upload-time = "2026-08-12T14:33:36.195Z" }, + { url = "https://files.pythonhosted.org/packages/37/ae/1c4a46b6b00d1c34d2ee355ef99ad6173674166800d1af0f05f85028d513/charset_normalizer-3.5.0-cp314-cp314-win32.whl", hash = "sha256:606a86c1c3196f3738de39a67a7490bbd61cb31c0e0436070bd0c6a48170b38e", size = 179790, upload-time = "2026-08-12T14:33:37.356Z" }, + { url = "https://files.pythonhosted.org/packages/01/51/f94dcf34fa8eba48c1fb89b6490a5f1426e19488fe5f38aac6c648c99057/charset_normalizer-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:ec6c464cf45867f66a2273e2214d9199a8fbad5cb95ca0fd45f6a2fe1d9d2cf4", size = 203723, upload-time = "2026-08-12T14:33:38.639Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ba/91d386870b5d9e4b0d8c4034f63877cc2e47b99c81ef05f3e6d42bf9a53f/charset_normalizer-3.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:dc28949de1bb5f7f30a46f15d74ce7ac5aaa63e03c5de04d68f571c7423af834", size = 183423, upload-time = "2026-08-12T14:33:39.899Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c9/534ecb17b7fb95f9052c4a44cf316316a27d4a8f73e8475ff55e778dcdd7/charset_normalizer-3.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:68b7e84ae8239a94f8d2c8f3f3a3a81bcde54805ec8f42a34de927d155688ec6", size = 368967, upload-time = "2026-08-12T14:33:41.093Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/ad7c3242d7fe55cd55126c22c65cb1b49779782cdf8932fd01d12232d86a/charset_normalizer-3.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58ca5dc0a0ef99f2801ec0574214c978e9574055bc783830bbb6e7433218609f", size = 239478, upload-time = "2026-08-12T14:33:42.428Z" }, + { url = "https://files.pythonhosted.org/packages/7e/62/77f0b850048e430fc350ec58876b0c020f5c8d0d3956fd1a4d6ae2fa292f/charset_normalizer-3.5.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6c57af4084c10cb3286688d65e4c654190ff5edcbc2411d08cdca0a8a44c59a1", size = 227036, upload-time = "2026-08-12T14:33:43.635Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ba/47d951e1a51dddbaad0a1410baf49fb1d897ceb00281568f1183b79bce9a/charset_normalizer-3.5.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1619a3cc174a7e3963dd34348e6fceb6e50db0ddeb0031bd7c73a58286454fa", size = 260772, upload-time = "2026-08-12T14:33:44.96Z" }, + { url = "https://files.pythonhosted.org/packages/b0/61/8c7ff4c81b2a88271126acf4b83ab3e31f6d63868b0f01d331eaa0f9cb67/charset_normalizer-3.5.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1328cc57dd4372be1265f68232cee890e087416e3e6e93e6ffb32c2bad4d36a4", size = 259273, upload-time = "2026-08-12T14:33:46.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fd/36129689be08dc287b951306946657ff70d76e287dd57018861f86d0e474/charset_normalizer-3.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06f4fb62a9139bef056b8b2da6773c94c2f259f90e4b8e53b166f3d0372d7cf6", size = 248086, upload-time = "2026-08-12T14:33:47.54Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f8/bcae67f994c8fd31dda445e5ebf84045823c31443fe46f0e9ee6aca99aa0/charset_normalizer-3.5.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:478650a70a750d75d5add401606c77f77069c32e4ba2c9131dc6cee566962ca0", size = 242671, upload-time = "2026-08-12T14:33:48.746Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/0472cdad1061c2f0e4d3aee29973eb6e81bb8fe256ff2860cf115b15f1c9/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:48920bf6fe83eb2226756ac623fa54940487154eb18f80889d5735cf234965c0", size = 241311, upload-time = "2026-08-12T14:33:50.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/89/04a03de5d27c77c624d9fcf6287073754bd438df1b58cb7d030c57c2824d/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:168a0cb536b5123a77bc42ecf5e0bf6f923d0d9ae43c42a14eb0677c19ac6c19", size = 229898, upload-time = "2026-08-12T14:33:51.523Z" }, + { url = "https://files.pythonhosted.org/packages/42/a2/639c4278adcb7ed1f4db608dd9ac19b6774fa2285a96b1c0bdb9c124ccbd/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f278e131afa96a3622cef9211c406ea2ad1b68eb06f8837cd443684a40e0ae50", size = 262852, upload-time = "2026-08-12T14:33:52.924Z" }, + { url = "https://files.pythonhosted.org/packages/9d/95/02e34c97bedfd0c5574efb9179c850591acc7f967ba039ed8dd29d332b73/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d6100f877d2ed95f0856a3fde25334153add94bf2224c43f45f88e7039262aaa", size = 242913, upload-time = "2026-08-12T14:33:54.18Z" }, + { url = "https://files.pythonhosted.org/packages/a3/64/0946aeab6462dad9f160a50dfb4704d3f58a5ee708f085abc2105fbbff0c/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:4c440122e1ea68b1f8b44a631ebf49c39180f6869b1da22d76e8a724208ec6e9", size = 257938, upload-time = "2026-08-12T14:33:55.802Z" }, + { url = "https://files.pythonhosted.org/packages/79/77/36787d41ead124746506a4425c729f4f17c68280af8a6a5baa0a598cae86/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e5f834965c2fe589837bac1002e07e25734ff70381903ccd95b3d649e22bfa40", size = 249467, upload-time = "2026-08-12T14:33:57.081Z" }, + { url = "https://files.pythonhosted.org/packages/65/10/d9f6c5589cd24198d4ce6cd2948191c18e657272f433e5a00d258d9f5c22/charset_normalizer-3.5.0-cp314-cp314t-win32.whl", hash = "sha256:076cf9d3f3c7e410295c09d96355cf3b1bcae74990034d80e4371e20fe1ba4c6", size = 190624, upload-time = "2026-08-12T14:33:58.449Z" }, + { url = "https://files.pythonhosted.org/packages/6c/81/43e0584a802051a22c725795ebe1df78263abc7de858eef6cdc9b36637e9/charset_normalizer-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3288a560dc3114d5d2ebe309b1ef43f8af355eafe25856832415c2a8196c9db3", size = 215902, upload-time = "2026-08-12T14:33:59.753Z" }, + { url = "https://files.pythonhosted.org/packages/30/f3/af6a1160fef0eac4510d035241e11eccf78e5350e4cd4de79e79fe02a5e5/charset_normalizer-3.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a284c36b9c6616bf0a8aa4aabba668a0c75ba65ccf40a79868aeaa69ad996897", size = 193452, upload-time = "2026-08-12T14:34:01.017Z" }, + { url = "https://files.pythonhosted.org/packages/42/a4/dee470afb7a55c4f78b6fef37306c51fed17ebf94dbe530798c91d394350/charset_normalizer-3.5.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:c38d1e9bc2073b0984d2099ea647fd7f6c0d8f83a1e14e0cd32926f16e4c44ce", size = 341595, upload-time = "2026-08-12T14:34:02.4Z" }, + { url = "https://files.pythonhosted.org/packages/6a/32/9c3126dc429c6d9d7f79c52681a7c4453ed20a26267c9a8275d7ab620aba/charset_normalizer-3.5.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9f45186390aee4d1f26f723c615b67df346766c3b16df000d84d6e374f06757", size = 242177, upload-time = "2026-08-12T14:34:03.741Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9d/5b616a887301ff4cc0916b39ba44257390d3da80deeed6e8b6f2f26b14a8/charset_normalizer-3.5.0-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f0fde5e5100c735b2274ab898f0742a5dcde492796296cfbe7e0ad6a4cd1a396", size = 236730, upload-time = "2026-08-12T14:34:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/d6d3e70be93ebe5fabef65e4c7ac113e1d1705cbaeb5fb72467e713aca17/charset_normalizer-3.5.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d00e18e7bbf47e332ab63903d18bae31efc701b1d8cca0382b97784a621fc44", size = 265158, upload-time = "2026-08-12T14:34:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/30/e7/3f1fafa87e2643257474f9c4eec609f2193a61d907dce7dd4f3f2390ebd5/charset_normalizer-3.5.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b81980668800dd1c69faad8aea6e85a8cee0e13bcd3bba7671695ff16260293", size = 262931, upload-time = "2026-08-12T14:34:07.511Z" }, + { url = "https://files.pythonhosted.org/packages/0a/df/ebeb224a949d91829e5e114c6b64372a3c792b00762a9e951ce416f3a32d/charset_normalizer-3.5.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb3e0d1345b9c0fe73673ea656375f38a78ec679c2edeae0c24800f04798a85", size = 251388, upload-time = "2026-08-12T14:34:08.949Z" }, + { url = "https://files.pythonhosted.org/packages/a0/64/9a6ce2e7acc5cf1b4636f78f82e89ff581e06a0216a40678b28bd4d832c4/charset_normalizer-3.5.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2401f7671242e921e604f609d429f6b282ea4ca787a6ffd22ed7372011ddb9d1", size = 251821, upload-time = "2026-08-12T14:34:10.138Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b1/6e69b8056f615e5ccff6b91ca16db2d47922251f016821a300c115267fef/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:96720f2aeed3434bc48f4d52fbad64ecc820cfed88915d664780ed9ba09ede78", size = 244507, upload-time = "2026-08-12T14:34:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/0f/34/02c15d6a0aa6b934dcdc136b111da63ae857b9fd51cf5505b0736337c2eb/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:c455829625df983f716cbaecbba77f2d1dc2e0e0ed1638c059cece15a279344b", size = 240951, upload-time = "2026-08-12T14:34:12.991Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/0fe893d3e1c7d111280bd6c4bd4c1e431487a1124a1bcbce78dfeda3a3a8/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:c41b067eddcfa5ee6b1169c287605be7fb6b0ea22bba6474c5bb978a668def4f", size = 266162, upload-time = "2026-08-12T14:34:14.232Z" }, + { url = "https://files.pythonhosted.org/packages/55/ea/eca03527307670f5d102c295671a800c404ca958cf94fefd10fc963a72f0/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:e31786a947b136329bfdc458c82c06d4ec539b4a4436b7da4df4aafc9902ee80", size = 251835, upload-time = "2026-08-12T14:34:15.48Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/fee5633081e595fe9e191df6f215106c791ad596eddf5e41e39b8ea0f2e2/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:c5c6d47a865147e0ae3322ce92e7fb52ba3169d94b447deda56897ea2aa6fac9", size = 264314, upload-time = "2026-08-12T14:34:16.679Z" }, + { url = "https://files.pythonhosted.org/packages/cf/fb/17f47ae6ca35b562fb6e6f4b05f7aec6034217353eb4a23aaa3566dc7340/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:c75191e3c8052045179646cb40e280800a4e0bdfda34d9c949c2f268d44e80e4", size = 253194, upload-time = "2026-08-12T14:34:17.975Z" }, + { url = "https://files.pythonhosted.org/packages/59/88/f2b0f7ebb92493e925889ff29239b3b0073ffafd91230dbfc69e5cf9389c/charset_normalizer-3.5.0-cp315-cp315-win32.whl", hash = "sha256:83b62410bd36bb1178a7d563e2ee0cf21eb1c980c912ab99c2c78f06227f1731", size = 179800, upload-time = "2026-08-12T14:34:19.409Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f0/afb5bfdea52fd943b1960403847a276b8e900c6e4cd6a38752321b4eda64/charset_normalizer-3.5.0-cp315-cp315-win_amd64.whl", hash = "sha256:e3b9eaa99a6d8c9ace4cd303915947ef55088d4cd87c6676874f98c5c03aa040", size = 203726, upload-time = "2026-08-12T14:34:20.656Z" }, + { url = "https://files.pythonhosted.org/packages/fc/71/219783eb691aa2ec879c0e521afdfe2b826f9678eed51b9c039d03e0db2b/charset_normalizer-3.5.0-cp315-cp315-win_arm64.whl", hash = "sha256:fec352b793cdc183cc9e7e0b6c10fd7bff38ec54ba44cc43599b9b56f7f3db2e", size = 183428, upload-time = "2026-08-12T14:34:21.975Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d0/14aef3b9f80f2593c039d897e89034635b9eb0eb44b6ce5173bbd79ff338/charset_normalizer-3.5.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:c9bde7a960720c8b8e1b5ef7afaa0c9a2f3b55c44abd635b2b29dd066b298e3a", size = 368728, upload-time = "2026-08-12T14:34:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/10/fc/b249466ddbbeffa448b6597631e9091d1f01b5132ff8e7a0e21a6eb72b63/charset_normalizer-3.5.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8cd1283a9fe6c2065c807e9d5da81afe5e1e004caef39adc0d8ae86dd883698", size = 240925, upload-time = "2026-08-12T14:34:24.504Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b9/c17e72aaa1b3e1ca6c184e8025cf138ed492d01a54f85286ff7d31253a4b/charset_normalizer-3.5.0-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1c010dd86d3f4c4433c9634d33ce8147393b270dfa54f217f965540b8ae8e075", size = 234932, upload-time = "2026-08-12T14:34:25.822Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/f84ef0966bbe216f71029e34e7fa425a16b1682e2a40265e679dedf2b655/charset_normalizer-3.5.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5e68229977b2dea28e7061c0c0630a23f2f9f6e9c6fb38d77d3d6dbfe3768b74", size = 261733, upload-time = "2026-08-12T14:34:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/3b/73/3e887fa0781a395339355ed934ab6561ceb5bb52574160f070224039c630/charset_normalizer-3.5.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a60773eb5fda796e6e6f76b9c152d270fe59f9788a51a6ff8ba44082d8548ae4", size = 258460, upload-time = "2026-08-12T14:34:28.431Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/52ebd9849bf9e35d0b21fff115cb6543162a8e1f2f564e8f87121a336b8c/charset_normalizer-3.5.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9419f44e568f7fafcdc0b3b5c766a2364e705a9b34fb8a56b431e0d1f3f4258", size = 249894, upload-time = "2026-08-12T14:34:29.698Z" }, + { url = "https://files.pythonhosted.org/packages/85/f3/9366492b8a5fe0187de282e001d61345740cf79eb4a5f20181d769be02b5/charset_normalizer-3.5.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75e243abbb528c1a774390ed71e3f868a9f37b1373442e4bbadd401cfc505ff4", size = 249540, upload-time = "2026-08-12T14:34:30.96Z" }, + { url = "https://files.pythonhosted.org/packages/0b/af/28bb5e5dbd3e67cb9196a62781ac2b6d79492f4fc7a069b6ca7d6d6c8d58/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:54c963ce6404e52255b737e8a06d356fc762d59096ae566203a67cf2b7d050f2", size = 242734, upload-time = "2026-08-12T14:34:32.481Z" }, + { url = "https://files.pythonhosted.org/packages/26/d6/7ccfa62b53b40fc06b2d3504825aa400764740bd10cf248fdc4272441b93/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:63ea0cc840c66670183578c2630d138c0e944aeadfc33f25173ee240f5db780d", size = 239580, upload-time = "2026-08-12T14:34:33.916Z" }, + { url = "https://files.pythonhosted.org/packages/0b/82/71c0c9b046697b8da66b3acefa8d5f92d00a9ef433ad7c3522b971d0369a/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6c06875a1d4a7537bef70f659b55c6b55b9a47ec3ba8f2db610350c2d9915e6e", size = 263281, upload-time = "2026-08-12T14:34:35.333Z" }, + { url = "https://files.pythonhosted.org/packages/d6/01/d027583c869f40ba980c1c76994adbd522c360a6327e72beb44d7c267385/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:4253da1b4456b633651a8d59eb1dc7a8a8fa38241014dd7c217b353e547ae394", size = 250027, upload-time = "2026-08-12T14:34:36.991Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e9/6475d739e0ec8bb1236e06263dc3affaffdf947d8114ad27024932f325da/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:f044cb1cf44012184715f46584658993b5fee9344d71c4b0c455a17a299730c0", size = 257547, upload-time = "2026-08-12T14:34:38.277Z" }, + { url = "https://files.pythonhosted.org/packages/a5/60/d1f502fcaa048a2aca3ab80bfef8407659c131e4f1792fa805fec14b4960/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a17864853f7c518ae7d4b368af98f427f9396805476af40af8698560f09d7d97", size = 251718, upload-time = "2026-08-12T14:34:39.562Z" }, + { url = "https://files.pythonhosted.org/packages/c3/69/76343dcf4381a698807ff8a20d89f66bbdd9f6222b0b17740f77ab764335/charset_normalizer-3.5.0-cp315-cp315t-win32.whl", hash = "sha256:9e726478d7a213847860219d74665a6892a643ac93b8f76580f6cf9ed39996b7", size = 190757, upload-time = "2026-08-12T14:34:41.053Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ea/d18147626a1667cc773c42104ab155a4ca5d6d4d174b7a35e01062213ea5/charset_normalizer-3.5.0-cp315-cp315t-win_amd64.whl", hash = "sha256:d7229a99120c6c2792d96f4857c2648ce5530e93667a2c2388c5ef69a6b84775", size = 215431, upload-time = "2026-08-12T14:34:42.501Z" }, + { url = "https://files.pythonhosted.org/packages/47/21/4869598aae0872d94faa5933918a4fe37ab2c5af9d095786e241f9506fed/charset_normalizer-3.5.0-cp315-cp315t-win_arm64.whl", hash = "sha256:527e28a5e751d9e11369b9c5f9ab35c748eb9c109101920c7deb40d6eadf8d03", size = 193205, upload-time = "2026-08-12T14:34:43.781Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f3/7b523d807cb5e73562ef8acf21d39cdb9d704955327362c781bc3478a73d/charset_normalizer-3.5.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:5a4ee37248dfac25107c758bda99d545ce73e60b44d2dd39e4a2bb9f2831e9f5", size = 330840, upload-time = "2026-08-12T14:34:45.06Z" }, + { url = "https://files.pythonhosted.org/packages/f0/de/fc68978fe78ca97063c96d764e41ff92ca639948f319271e0ff450e577a2/charset_normalizer-3.5.0-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a864bdcacd8bff58bb4845304e031f821a3ec64b2b7259f2d409cd49c9e59ca3", size = 251862, upload-time = "2026-08-12T14:34:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/a9/cb/82b41a0ab7fb1a88065f1d78ad32696ad88ea3fe8e25b8189d08833938de/charset_normalizer-3.5.0-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84b736e3b391601bc47b86da381c749c0f894e9191aaca9f31f30c2632206df3", size = 239484, upload-time = "2026-08-12T14:34:47.869Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0c/19608b631f4538f908098d4a2d56a8f79a665e27cc58e9d90479761a9227/charset_normalizer-3.5.0-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6abb1f356fb865baeb6ebc3fadd843e9a96fbf49b9adcca55037f3cceccb7438", size = 230602, upload-time = "2026-08-12T14:34:49.265Z" }, + { url = "https://files.pythonhosted.org/packages/29/db/f648eb30e14eba301aed61e11672156f137905c1bdbb530151abe8065943/charset_normalizer-3.5.0-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d366548d2ee28a8cfdcc4296363978cc644a728333be9824d2de4652e83df0a", size = 259208, upload-time = "2026-08-12T14:34:50.632Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e0/ed2c8bdbac484d69614d6993143aeb6cb0f4dd1561c883402517b623c8ef/charset_normalizer-3.5.0-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d672f329ae504ee240eb39b6effb3318aa8e7e8924c0ce8eee5760b3fad98539", size = 253659, upload-time = "2026-08-12T14:34:52.11Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/b4907cb9ec5b521d9d024ced13611240b86ef065c2eb15b3ad2334dc9940/charset_normalizer-3.5.0-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c54036a518748b6c02e666f6d46c3817561998fb904c3be25b56fb4fe3dc5706", size = 248821, upload-time = "2026-08-12T14:34:53.399Z" }, + { url = "https://files.pythonhosted.org/packages/12/b2/e2d1abcfbc05822f0030869efb4e9f8a3658e13b4821796d4b62da917327/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:22a1889f1c9b752c63c36758a0c2145458e3cadb20fced7a0790002e9dd12b26", size = 240271, upload-time = "2026-08-12T14:34:55.09Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f9/4ba127ad610542fa3eabfa41c45bf12d357860a815b3566374ec0188e213/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:b7eb3eab5c646d3de7dcb14a7c9caebace5249c5767da39e1761cb1576e521a3", size = 232155, upload-time = "2026-08-12T14:34:56.543Z" }, + { url = "https://files.pythonhosted.org/packages/01/68/40613182366d00bd6dbd5f6c84a926cbd120960e038a8269e9ae7d782762/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:2080aa129a28267984cdc902898993d788c995c384e285d0d19199f56760d52e", size = 259674, upload-time = "2026-08-12T14:34:57.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/53/4574a14fa4c9de4a6c9f31725354bfa40b67f653e6d594ce1654f9a41b32/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:fd68c825548a611158230e2f9222e210ceb2e3391995c0aa5865cbdf3ab4bd49", size = 246122, upload-time = "2026-08-12T14:34:59.337Z" }, + { url = "https://files.pythonhosted.org/packages/5a/02/bd8030d13d92c058ca7b2b9615bbb3169569e144db64d65c149cd45abf5e/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:d8a9316f4da85e937242642b537c6d55d7e9287dd38e5634732f8233932aff45", size = 255221, upload-time = "2026-08-12T14:35:00.71Z" }, + { url = "https://files.pythonhosted.org/packages/77/9d/10ecd3bcbe2666b3d4d4026c97b48f73990682815db516052a1e8f4a31c5/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d90254c8f609338c53ec180fcd4c4f9c16502e238e3fc88ca7fd4c2f38d445b8", size = 253450, upload-time = "2026-08-12T14:35:02.217Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/d044c4872c0938a84f87b5027a698c0e61bacfc5c3551a4e749ca9b7bc5c/charset_normalizer-3.5.0-cp37-abi3-win32.whl", hash = "sha256:8b3e9e29b8b07cc461b9ce7768db7693a93979d0dadf22046f6f3555ded2f516", size = 173594, upload-time = "2026-08-12T14:35:03.845Z" }, + { url = "https://files.pythonhosted.org/packages/10/6b/6046773901f1944b9a89436351529811ee958afc7b774563be9d74a6f0c3/charset_normalizer-3.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:0c8953d9d1617794cfc40d81179571c9ba3805dd029623a15c93f1fb70e60a74", size = 198959, upload-time = "2026-08-12T14:35:05.187Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a6/b57708ac92aefc8e8389d51d5178129b81f03196da61ee2c23e687b8178a/charset_normalizer-3.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:562d24ca7797c1af8852994950c2e623a907b201fc4b0ed29e92af173d3828ca", size = 267055, upload-time = "2026-08-12T14:35:06.533Z" }, + { url = "https://files.pythonhosted.org/packages/22/c7/754d09943a616937df61e4ba367c409ded2a987e872972098d51a6fcf73b/charset_normalizer-3.5.0-py3-none-any.whl", hash = "sha256:993dfcbe75a85a3784abb5084f2c41b915767c90546fcc92803cffa28611baea", size = 67943, upload-time = "2026-08-12T14:35:30.363Z" }, +] + +[[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 = "cwmath" +version = "1.0.0" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "black" }, + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extra = ["python"] }, + { name = "pytest" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "black" }, + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extras = ["python"] }, + { name = "pytest" }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "griffelib" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/e4/8d187ea29c2e30b3a09505c567513077d6117861bde1fbd997a167f262ec/griffelib-2.1.0.tar.gz", hash = "sha256:762a186d2c6fd6794d4ea20d428d597ffb857cb56b66421651cbba15bdd5e813", size = 216234, upload-time = "2026-06-19T12:05:42.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, +] + +[[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 = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/6f/da4c6aea59b3001f2e8c0ec7497475aadaf3b021c10cab5b2858f0f32b26/markdown-3.10.3.tar.gz", hash = "sha256:3589362618f743188b4d955b874402bc814f4f83f544dc207719f4baa7d9c45f", size = 372596, upload-time = "2026-07-30T19:05:29.005Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/69/4a5af2bc115a9a33fefe51709749de8262be3f9ba063d1753a837cdbc49c/markdown-3.10.3-py3-none-any.whl", hash = "sha256:fa6c92a00a4a3c98b22728c64a935ae1928250ae65058a6ded814d2cc29a4cea", size = 110757, upload-time = "2026-07-30T19:05:27.883Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/cd/c05d3a530ba7934f144fb45f7203cd236adc25c7bdcc34673d202f4b0278/mkdocs_material-9.7.7.tar.gz", hash = "sha256:c0649c065b1b0512d60aad8c10f947f8e455284475239b364b610f2deb4d0855", size = 4097923, upload-time = "2026-07-17T16:21:33.156Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/21/17c1bc9e6f47c972ad66fb2ac2568f99f90f1207eeb6fc3b34d094dba7b5/mkdocs_material-9.7.7-py3-none-any.whl", hash = "sha256:8ea9bb1737a5b524a5f9dcf2e1b4ebda8274ae3008aa7845720a97083bef708f", size = 9305438, upload-time = "2026-07-17T16:21:30.017Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/71/f85bdf13355073ae15a7375f09879375a830553552e58c1c4b7e0bbc5c8b/mkdocstrings-1.0.6.tar.gz", hash = "sha256:a0b8c2bdd29a6416c80d717aa369bbf7831946bd9f23c2a66db1b1dbe7693dbd", size = 100649, upload-time = "2026-07-11T19:38:05.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/5b/4c1902e8bdd5c4db63284e9d101dece4038d4025d6d88850ffe0a1578980/mkdocstrings-1.0.6-py3-none-any.whl", hash = "sha256:2703708697487d1b6d6d7b412e176fa436edf120c1bf81dc9e126b12d00893c7", size = 35787, upload-time = "2026-07-11T19:38:04.417Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/b6/e858701499d57eee8b3fd8e78168083956c6683ddbe727b46758b19e1119/mkdocstrings_python-2.0.5.tar.gz", hash = "sha256:3a4d92556ad39637e88af94a5374213af9a8e3040c3824ceaed04b486c017594", size = 199578, upload-time = "2026-06-19T10:41:08.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/fc/10ab7e80650a9c9e8f4f1105f8c8e73567f88ed0c06ada589ab81d38687c/mkdocstrings_python-2.0.5-py3-none-any.whl", hash = "sha256:30c837bbff016549f659fcba6539ac351303f0fd7e713c89a040611072236e9d", size = 104951, upload-time = "2026-06-19T10:41:07.378Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.11.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/d7/e7bfbc86e9f99ff7807e24de7703f032e9c9ba80bb355cf26e0e9bc5a75e/platformdirs-4.11.3.tar.gz", hash = "sha256:66a73d38a849810252df809a3d8bcbda8e26f6c189920e7535ad608a48dbb5ab", size = 33050, upload-time = "2026-08-13T22:43:27.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/a9/c34aebedd3a4c9afe5101b1b8713710b3fec18087c8a36c35d2f909861bd/platformdirs-4.11.3-py3-none-any.whl", hash = "sha256:5ed065d443751de711da036041a7a214122efc4a4de393b3f4137ba5576540e7", size = 23491, upload-time = "2026-08-13T22:43:26.121Z" }, +] + +[[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 = "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 = "pymdown-extensions" +version = "11.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/a9/5f0c535ba3b08fe09270c16808e053a968868242ecbd5676d4e3a488bf28/pymdown_extensions-11.0.1.tar.gz", hash = "sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0", size = 857113, upload-time = "2026-07-02T17:59:22.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +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 = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[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 = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +]