From f78180127f3724257175871d5553a377cc2a1816 Mon Sep 17 00:00:00 2001 From: Oliver Meyer Date: Tue, 24 Mar 2026 12:05:20 +0100 Subject: [PATCH 1/2] chore: use adr-tools --- .adr-dir | 1 + .../0001-record-architecture-decisions.md | 20 +++++ docs/decisions/README.md | 1 + mise.lock | 76 +++++++++++++++++++ mise.toml | 1 + 5 files changed, 99 insertions(+) create mode 100644 .adr-dir create mode 100644 docs/decisions/0001-record-architecture-decisions.md create mode 100644 docs/decisions/README.md diff --git a/.adr-dir b/.adr-dir new file mode 100644 index 0000000..51c5f29 --- /dev/null +++ b/.adr-dir @@ -0,0 +1 @@ +docs/decisions diff --git a/docs/decisions/0001-record-architecture-decisions.md b/docs/decisions/0001-record-architecture-decisions.md new file mode 100644 index 0000000..4cf03c2 --- /dev/null +++ b/docs/decisions/0001-record-architecture-decisions.md @@ -0,0 +1,20 @@ +# 1. Record architecture decisions + +Date: 2026-03-24 + +## Status + +Accepted + +## Context + +We need to record the architectural decisions made on this project. + +## Decision + +We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). +We will use `adr-tools` to manage our ADRs, and we will store them in the `docs/decisions` directory. + +## Consequences + +See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools). diff --git a/docs/decisions/README.md b/docs/decisions/README.md new file mode 100644 index 0000000..7f7cb01 --- /dev/null +++ b/docs/decisions/README.md @@ -0,0 +1 @@ +To document a decision: `adr new "My decision title".` diff --git a/mise.lock b/mise.lock index 56daeb8..65f4608 100644 --- a/mise.lock +++ b/mise.lock @@ -8,10 +8,18 @@ backend = "aqua:nektos/act" checksum = "sha256:116c7f27c4862fb01730c956c9bc9f51f9d44d8f3850fe6166b7da9f68677a5c" url = "https://github.com/nektos/act/releases/download/v0.2.84/act_Linux_arm64.tar.gz" +[tools.act."platforms.linux-arm64-musl"] +checksum = "sha256:116c7f27c4862fb01730c956c9bc9f51f9d44d8f3850fe6166b7da9f68677a5c" +url = "https://github.com/nektos/act/releases/download/v0.2.84/act_Linux_arm64.tar.gz" + [tools.act."platforms.linux-x64"] checksum = "sha256:19d4525fb0d80ff50bd3252d3fa47ee1265a331fe4c39249ac65347e527e16e2" url = "https://github.com/nektos/act/releases/download/v0.2.84/act_Linux_x86_64.tar.gz" +[tools.act."platforms.linux-x64-musl"] +checksum = "sha256:19d4525fb0d80ff50bd3252d3fa47ee1265a331fe4c39249ac65347e527e16e2" +url = "https://github.com/nektos/act/releases/download/v0.2.84/act_Linux_x86_64.tar.gz" + [tools.act."platforms.macos-arm64"] checksum = "sha256:6739345c2bf7bbd1a3ceaf6503a9ffbe98b33dd3789edce39e8c844c6b49036b" url = "https://github.com/nektos/act/releases/download/v0.2.84/act_Darwin_arm64.tar.gz" @@ -24,6 +32,32 @@ url = "https://github.com/nektos/act/releases/download/v0.2.84/act_Darwin_x86_64 checksum = "sha256:3b4fcf9497d1f23afbdd40bfa8e78e154d69754da73c2d792a4152b4f8ae5c96" url = "https://github.com/nektos/act/releases/download/v0.2.84/act_Windows_x86_64.zip" +[[tools.adr-tools]] +version = "3.0.0" +backend = "aqua:npryce/adr-tools" + +[tools.adr-tools."platforms.linux-arm64"] +url = "https://github.com/npryce/adr-tools/archive/refs/tags/3.0.0.tar.gz" + +[tools.adr-tools."platforms.linux-arm64-musl"] +url = "https://github.com/npryce/adr-tools/archive/refs/tags/3.0.0.tar.gz" + +[tools.adr-tools."platforms.linux-x64"] +url = "https://github.com/npryce/adr-tools/archive/refs/tags/3.0.0.tar.gz" + +[tools.adr-tools."platforms.linux-x64-musl"] +url = "https://github.com/npryce/adr-tools/archive/refs/tags/3.0.0.tar.gz" + +[tools.adr-tools."platforms.macos-arm64"] +checksum = "blake3:1b011324e104b8957270f29fde38aae5f80a8131f7854c521c8f237233ea4d2b" +url = "https://github.com/npryce/adr-tools/archive/refs/tags/3.0.0.tar.gz" + +[tools.adr-tools."platforms.macos-x64"] +url = "https://github.com/npryce/adr-tools/archive/refs/tags/3.0.0.tar.gz" + +[tools.adr-tools."platforms.windows-x64"] +url = "https://github.com/npryce/adr-tools/archive/refs/tags/3.0.0.tar.gz" + [[tools.gh]] version = "2.87.3" backend = "aqua:cli/cli" @@ -32,10 +66,18 @@ backend = "aqua:cli/cli" checksum = "sha256:5f5d89563bf26751e2173b37e594065504e85b6b781c1f1832d24bf2c2b4554f" url = "https://github.com/cli/cli/releases/download/v2.87.3/gh_2.87.3_linux_arm64.tar.gz" +[tools.gh."platforms.linux-arm64-musl"] +checksum = "sha256:5f5d89563bf26751e2173b37e594065504e85b6b781c1f1832d24bf2c2b4554f" +url = "https://github.com/cli/cli/releases/download/v2.87.3/gh_2.87.3_linux_arm64.tar.gz" + [tools.gh."platforms.linux-x64"] checksum = "sha256:c6e5537631fca45f277ef405ce8751d139b491e9402cc20891a003525a8773b2" url = "https://github.com/cli/cli/releases/download/v2.87.3/gh_2.87.3_linux_amd64.tar.gz" +[tools.gh."platforms.linux-x64-musl"] +checksum = "sha256:c6e5537631fca45f277ef405ce8751d139b491e9402cc20891a003525a8773b2" +url = "https://github.com/cli/cli/releases/download/v2.87.3/gh_2.87.3_linux_amd64.tar.gz" + [tools.gh."platforms.macos-arm64"] checksum = "sha256:dedfc6f569e9dbc5b92d47dce44acadbdf5b6b7a861510db0c748dfac55002f6" url = "https://github.com/cli/cli/releases/download/v2.87.3/gh_2.87.3_macOS_arm64.zip" @@ -56,10 +98,18 @@ backend = "aqua:jqlang/jq" checksum = "sha256:6bc62f25981328edd3cfcfe6fe51b073f2d7e7710d7ef7fcdac28d4e384fc3d4" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-linux-arm64" +[tools.jq."platforms.linux-arm64-musl"] +checksum = "sha256:6bc62f25981328edd3cfcfe6fe51b073f2d7e7710d7ef7fcdac28d4e384fc3d4" +url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-linux-arm64" + [tools.jq."platforms.linux-x64"] checksum = "sha256:020468de7539ce70ef1bceaf7cde2e8c4f2ca6c3afb84642aabc5c97d9fc2a0d" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-linux-amd64" +[tools.jq."platforms.linux-x64-musl"] +checksum = "sha256:020468de7539ce70ef1bceaf7cde2e8c4f2ca6c3afb84642aabc5c97d9fc2a0d" +url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-linux-amd64" + [tools.jq."platforms.macos-arm64"] checksum = "sha256:a9fe3ea2f86dfc72f6728417521ec9067b343277152b114f4e98d8cb0e263603" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-macos-arm64" @@ -87,10 +137,18 @@ backend = "core:python" checksum = "sha256:c4a5cc7681da3ed2066194f6fac24207a764db38f23fc5a2b1b56671ee3142d6" url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260320/cpython-3.14.3+20260320-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz" +[tools.python."platforms.linux-arm64-musl"] +checksum = "sha256:c4a5cc7681da3ed2066194f6fac24207a764db38f23fc5a2b1b56671ee3142d6" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260320/cpython-3.14.3+20260320-aarch64-unknown-linux-gnu-install_only_stripped.tar.gz" + [tools.python."platforms.linux-x64"] checksum = "sha256:bc35984c271adb7b0d2f730eced89fbbb96246c4d716c415e4af7ed686627831" url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260320/cpython-3.14.3+20260320-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz" +[tools.python."platforms.linux-x64-musl"] +checksum = "sha256:bc35984c271adb7b0d2f730eced89fbbb96246c4d716c415e4af7ed686627831" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260320/cpython-3.14.3+20260320-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz" + [tools.python."platforms.macos-arm64"] checksum = "sha256:f9b4a94d2d62ca77e08873b861b8cd989eda922ad3c3eafd2dfad57375e6ae61" url = "https://github.com/astral-sh/python-build-standalone/releases/download/20260320/cpython-3.14.3+20260320-aarch64-apple-darwin-install_only_stripped.tar.gz" @@ -112,11 +170,21 @@ checksum = "sha256:7e3924a974e912e57b4a99f65ece7931f8079584dae12eb7845024f97087b url = "https://github.com/aquasecurity/trivy/releases/download/v0.69.3/trivy_0.69.3_Linux-ARM64.tar.gz" provenance = "cosign" +[tools.trivy."platforms.linux-arm64-musl"] +checksum = "sha256:7e3924a974e912e57b4a99f65ece7931f8079584dae12eb7845024f97087bdfd" +url = "https://github.com/aquasecurity/trivy/releases/download/v0.69.3/trivy_0.69.3_Linux-ARM64.tar.gz" +provenance = "cosign" + [tools.trivy."platforms.linux-x64"] checksum = "sha256:1816b632dfe529869c740c0913e36bd1629cb7688bd5634f4a858c1d57c88b75" url = "https://github.com/aquasecurity/trivy/releases/download/v0.69.3/trivy_0.69.3_Linux-64bit.tar.gz" provenance = "cosign" +[tools.trivy."platforms.linux-x64-musl"] +checksum = "sha256:1816b632dfe529869c740c0913e36bd1629cb7688bd5634f4a858c1d57c88b75" +url = "https://github.com/aquasecurity/trivy/releases/download/v0.69.3/trivy_0.69.3_Linux-64bit.tar.gz" +provenance = "cosign" + [tools.trivy."platforms.macos-arm64"] checksum = "sha256:a2f2179afd4f8bb265ca3c7aefb56a666bc4a9a411663bc0f22c3549fbc643a5" url = "https://github.com/aquasecurity/trivy/releases/download/v0.69.3/trivy_0.69.3_macOS-ARM64.tar.gz" @@ -140,10 +208,18 @@ backend = "aqua:astral-sh/uv" checksum = "sha256:685e47f8f88b6845a9fc2ca27c3d246c0f53af8c017daf8e98ac0a97fe20365b" url = "https://github.com/astral-sh/uv/releases/download/0.10.2/uv-aarch64-unknown-linux-musl.tar.gz" +[tools.uv."platforms.linux-arm64-musl"] +checksum = "sha256:685e47f8f88b6845a9fc2ca27c3d246c0f53af8c017daf8e98ac0a97fe20365b" +url = "https://github.com/astral-sh/uv/releases/download/0.10.2/uv-aarch64-unknown-linux-musl.tar.gz" + [tools.uv."platforms.linux-x64"] checksum = "sha256:c162182ba7dd692794362d76dd183990d6e51553217954106da19bdb6ced211b" url = "https://github.com/astral-sh/uv/releases/download/0.10.2/uv-x86_64-unknown-linux-musl.tar.gz" +[tools.uv."platforms.linux-x64-musl"] +checksum = "sha256:c162182ba7dd692794362d76dd183990d6e51553217954106da19bdb6ced211b" +url = "https://github.com/astral-sh/uv/releases/download/0.10.2/uv-x86_64-unknown-linux-musl.tar.gz" + [tools.uv."platforms.macos-arm64"] checksum = "sha256:3828b2de196687f60e9d199aea8b504299629300831eea0935ff3fe339903d0a" url = "https://github.com/astral-sh/uv/releases/download/0.10.2/uv-aarch64-apple-darwin.tar.gz" diff --git a/mise.toml b/mise.toml index ee8404f..ca6a949 100644 --- a/mise.toml +++ b/mise.toml @@ -1,5 +1,6 @@ [tools] act = "0.2.84" +adr-tools = "3.0.0" gh = "2.87.3" jq = "1.8.1" "pipx:keyring" = { version = "25.7.0", uvx_args = "--with keyrings.google-artifactregistry-auth" } From dce00372af115553f25479d385eea263617d1b8e Mon Sep 17 00:00:00 2001 From: Oliver Meyer Date: Tue, 24 Mar 2026 11:22:20 +0100 Subject: [PATCH 2/2] feat: add health module --- ATTRIBUTIONS.md | 136 ++++++++++ README.md | 5 +- docs/decisions/0002-module-structure.md | 53 ++++ pyproject.toml | 4 +- src/aignostics_foundry_core/AGENTS.md | 53 ++-- src/aignostics_foundry_core/__init__.py | 4 - src/aignostics_foundry_core/greet.py | 17 -- src/aignostics_foundry_core/health.py | 137 +++++++++++ tests/aignostics_foundry_core/greet_test.py | 31 --- tests/aignostics_foundry_core/health_test.py | 245 +++++++++++++++++++ uv.lock | 137 +++++++++++ 11 files changed, 746 insertions(+), 76 deletions(-) create mode 100644 docs/decisions/0002-module-structure.md delete mode 100644 src/aignostics_foundry_core/greet.py create mode 100644 src/aignostics_foundry_core/health.py delete mode 100644 tests/aignostics_foundry_core/greet_test.py create mode 100644 tests/aignostics_foundry_core/health_test.py diff --git a/ATTRIBUTIONS.md b/ATTRIBUTIONS.md index fd95ce7..8d12b5a 100644 --- a/ATTRIBUTIONS.md +++ b/ATTRIBUTIONS.md @@ -273,6 +273,40 @@ without prior written permission from Aignostics GmbH. ``` +## annotated-types (0.7.0) - MIT License + +Reusable constraint types to use with typing.Annotated + +* URL: https://github.com/annotated-types/annotated-types +* Author(s): Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>, Samuel Colvin , Zac Hatfield-Dodds + +### License Text + +``` +The MIT License (MIT) + +Copyright (c) 2022 the contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + ## argcomplete (3.6.3) - Apache Software License Bash tab completion for argparse @@ -5123,6 +5157,74 @@ Library for serializing and deserializing Python Objects to and from JSON and XM ``` +## pydantic (2.12.5) - UNKNOWN + +Data validation using Python type hints + +* URL: https://github.com/pydantic/pydantic +* Author(s): Samuel Colvin , Eric Jolibois , Hasan Ramezani , Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>, Terrence Dorsey , David Montague , Serge Matveenko , Marcelo Trylesinski , Sydney Runkle , David Hewitt , Alex Hall , Victorien Plot , Douwe Maan + +### License Text + +``` +The MIT License (MIT) + +Copyright (c) 2017 to present Pydantic Services Inc. and individual contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + +## pydantic_core (2.41.5) - UNKNOWN + +Core functionality for Pydantic validation and serialization + +* URL: https://github.com/pydantic/pydantic-core +* Author(s): Samuel Colvin , Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>, David Montague , David Hewitt , Sydney Runkle , Victorien Plot + +### License Text + +``` +The MIT License (MIT) + +Copyright (c) 2022 Samuel Colvin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + ## pyparsing (3.3.2) - UNKNOWN pyparsing - Classes and methods to define and execute parsing grammars @@ -7747,6 +7849,40 @@ DEALINGS IN THE SOFTWARE. ``` +## typing-inspection (0.4.2) - UNKNOWN + +Runtime typing introspection tools + +* URL: https://github.com/pydantic/typing-inspection +* Author(s): Victorien Plot + +### License Text + +``` +MIT License + +Copyright (c) Pydantic Services Inc. 2025 to present + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +``` + ## typing_extensions (4.15.0) - UNKNOWN Backported and Experimental Type Hints for Python 3.9+ diff --git a/README.md b/README.md index 8f33624..db3a808 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,9 @@ Or follow the [installation guide](https://mise.jdx.dev/getting-started.html) fo ## Usage ```python -from aignostics_foundry_core import greet +from aignostics_foundry_core.health import Health, HealthStatus -message = greet("World") -print(message) # Output: Hello, World! +health = Health(status=HealthStatus.UP) ``` ## Further Reading diff --git a/docs/decisions/0002-module-structure.md b/docs/decisions/0002-module-structure.md new file mode 100644 index 0000000..77c5863 --- /dev/null +++ b/docs/decisions/0002-module-structure.md @@ -0,0 +1,53 @@ +# 2. Module structure + +Date: 2026-03-24 + +## Status + +Accepted + +## Context + +We need to decide how to structure the internal modules of `aignostics_foundry_core`. Three options were considered: + +**#1 Private modules with package-level exports** +``` +aignostics_foundry_core/ +├── __init__.py # exposes Health class +└── _health.py # contains Health class +``` +Import: `from aignostics_foundry_core import Health` +Pros: matches Bridge/Python SDK conventions. +Cons: top-level `__init__.py` exports grow very long; hard to know which submodule an object comes from. + +**#2 Public modules** +``` +aignostics_foundry_core/ +├── __init__.py +└── health.py # contains and exposes Health class +``` +Import: `from aignostics_foundry_core.health import Health` +Pros: simple; can be migrated to #3 without breaking imports if needed. +Cons: modules cannot hide implementation details; everything in a module is public. + +**#3 Public modules with module-level exports** +``` +aignostics_foundry_core/ +├── __init__.py +└── health/ + ├── __init__.py # exposes Health class + └── _health.py # contains Health class +``` +Import: `from aignostics_foundry_core.health import Health` +Pros: modules can control what they advertise. +Cons: overhead (one directory + one file per module); migration from #2 is non-breaking if needed. + +## Decision + +We will use **#2 (public modules)**. Each concept lives in a single public module file, and is imported directly from that module. + +## Consequences + +- Module structure is simple and easy to navigate. +- All symbols in a module file are implicitly public; internal helpers should be prefixed with `_` by convention. +- If a module later needs to hide implementation details (e.g. a complex subpackage), it can be migrated to option #3 without breaking existing imports, since the import path `from aignostics_foundry_core.health import Health` is stable across both structures. diff --git a/pyproject.toml b/pyproject.toml index 4c4956a..fb3d768 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,9 @@ classifiers = [ requires-python = ">=3.11, <3.15" -dependencies = [] +dependencies = [ + "pydantic>=2,<3", +] [dependency-groups] dev = [ diff --git a/src/aignostics_foundry_core/AGENTS.md b/src/aignostics_foundry_core/AGENTS.md index 1374f9f..93f3587 100644 --- a/src/aignostics_foundry_core/AGENTS.md +++ b/src/aignostics_foundry_core/AGENTS.md @@ -8,21 +8,27 @@ This file provides an overview of all modules in `aignostics_foundry_core`, thei | Module | Purpose | Description | |--------|---------|-------------| -| **greet** | Example module | Provides greeting functionality | +| **health** | Service health checks | `Health` model and `HealthStatus` enum for tree-structured health status | ## Module Descriptions -### greet +### health -**Greeting functionality for Foundry Python Core** +**Tree-structured health status for service health checks** -- **Purpose**: Provides greeting utilities -- **Key Features**: Simple greeting with logging -- **Location**: `aignostics_foundry_core/greet.py` - - +- **Purpose**: Provides `Health` and `HealthStatus` for modelling UP / DEGRADED / DOWN status across a tree of service components +- **Key Features**: + - `HealthStatus(StrEnum)` — `UP`, `DEGRADED`, `DOWN` values + - `Health(BaseModel)` — pydantic model with `status`, `reason`, `components`, `uptime_statistics` + - `compute_health_from_components()` — recursively propagates DOWN/DEGRADED from children to parent (DOWN trumps DEGRADED) + - `validate_health_state()` — model validator: DOWN/DEGRADED require a reason; UP must not have one + - `__str__` — returns `"UP"`, `"DEGRADED: "`, or `"DOWN: "` + - `__bool__` — `True` iff status is `UP` + - `Health.Code` — `ClassVar` alias for `HealthStatus` (convenience) +- **Location**: `aignostics_foundry_core/health.py` +- **Dependencies**: `pydantic>=2` ## Architecture @@ -34,30 +40,37 @@ This file provides an overview of all modules in `aignostics_foundry_core`, thei --> ```text - ``` ## Usage Examples - - ```python -from aignostics_foundry_core import greet - -# Example usage -result = greet("World") -print(result) # Hello, World! +from aignostics_foundry_core.health import Health, HealthStatus + +# Simple UP status +health = Health(status=HealthStatus.UP) +assert bool(health) # True +assert str(health) == "UP" + +# Composite health — DOWN propagates from components automatically +system = Health( + status=HealthStatus.UP, + components={ + "db": Health(status=HealthStatus.UP), + "cache": Health(status=HealthStatus.DOWN, reason="Connection refused"), + }, +) +assert system.status == HealthStatus.DOWN +assert "cache" in system.reason ``` ## Development Guidelines diff --git a/src/aignostics_foundry_core/__init__.py b/src/aignostics_foundry_core/__init__.py index f77c8a4..8501c33 100644 --- a/src/aignostics_foundry_core/__init__.py +++ b/src/aignostics_foundry_core/__init__.py @@ -1,5 +1 @@ """Foundational infrastructure for Foundry components.""" - -from .greet import greet - -__all__ = ["greet"] diff --git a/src/aignostics_foundry_core/greet.py b/src/aignostics_foundry_core/greet.py deleted file mode 100644 index 8357045..0000000 --- a/src/aignostics_foundry_core/greet.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Greeting module for Foundry Python Core.""" - - -def greet(name: str) -> str: - """Return a greeting message for the given name. - - Args: - name: The name to greet. - - Returns: - A greeting message in the format "Hello, {name}!". - - Examples: - >>> greet("World") - 'Hello, World!' - """ - return f"Hello, {name}!" diff --git a/src/aignostics_foundry_core/health.py b/src/aignostics_foundry_core/health.py new file mode 100644 index 0000000..84ddd74 --- /dev/null +++ b/src/aignostics_foundry_core/health.py @@ -0,0 +1,137 @@ +"""Health models and status definitions for service health checks.""" + +from enum import StrEnum +from typing import Any, ClassVar, Self + +from pydantic import BaseModel, Field, model_validator + + +class HealthStatus(StrEnum): + """Health status enumeration for service health checks. + + Values: + UP: Service is operating normally + DEGRADED: Service is operational but with reduced functionality + DOWN: Service is not operational + """ + + UP = "UP" + DEGRADED = "DEGRADED" + DOWN = "DOWN" + + +class Health(BaseModel): + """Represents the health status of a service with optional components and failure reasons. + + - A health object can have child components, i.e. health forms a tree. + - Any node in the tree can set itself to DOWN or DEGRADED. If DOWN, the node is required + to set the reason attribute. If reason is not set when DOWN, automatic model validation fails. + - DOWN trumps DEGRADED, DEGRADED trumps UP. If any child is DOWN, parent is DOWN. + If none are DOWN but any are DEGRADED, parent is DEGRADED. + - The root of the health tree is computed in the system module. + The health of other modules is automatically picked up by the system module. + """ + + Code: ClassVar[type[HealthStatus]] = HealthStatus + status: HealthStatus + reason: str | None = None + components: dict[str, "Health"] = Field(default_factory=dict) + uptime_statistics: dict[str, dict[str, Any]] | None = None # Optional uptime stats + + def compute_health_from_components(self) -> Self: + """Recursively compute health status from components. + + - If health is already DOWN, it remains DOWN with its original reason. + - If health is UP but any component is DOWN, health becomes DOWN with + a reason listing all failed components. + - If no components are DOWN but any are DEGRADED, health becomes DEGRADED with a reason. + + Returns: + Self: The updated health instance with computed status. + """ + # Skip recomputation if already known to be DOWN + if self.status == HealthStatus.DOWN: + return self + + # No components means we keep the existing status + if not self.components: + return self + + # Find all DOWN and DEGRADED components + down_components: list[tuple[str, str | None]] = [] + degraded_components: list[tuple[str, str | None]] = [] + for component_name, component in self.components.items(): + # Recursively compute health for each component + component.compute_health_from_components() + if component.status == HealthStatus.DOWN: + down_components.append((component_name, component.reason)) + elif component.status == HealthStatus.DEGRADED: + degraded_components.append((component_name, component.reason)) + + # If any components are DOWN, mark the parent as DOWN + if down_components: + self.status = HealthStatus.DOWN + if len(down_components) == 1: + component_name, component_reason = down_components[0] + self.reason = f"Component '{component_name}' is DOWN ({component_reason})" + else: + component_list = ", ".join(f"'{name}' ({reason})" for name, reason in down_components) + self.reason = f"Components {component_list} are DOWN" + # If no components are DOWN but any are DEGRADED, mark parent as DEGRADED + elif degraded_components: + self.status = HealthStatus.DEGRADED + if len(degraded_components) == 1: + component_name, component_reason = degraded_components[0] + self.reason = f"Component '{component_name}' is DEGRADED ({component_reason})" + else: + component_list = ", ".join(f"'{name}' ({reason})" for name, reason in degraded_components) + self.reason = f"Components {component_list} are DEGRADED" + + return self + + @model_validator(mode="after") + def validate_health_state(self) -> Self: + """Validate the health state and ensure consistency. + + - Compute overall health based on component health + - Ensure UP status has no associated reason + - Ensure DOWN and DEGRADED status always have a reason + + Returns: + Self: The validated model instance with correct health status. + + Raises: + ValueError: If validation fails due to inconsistency. + """ + # First compute health from components + self.compute_health_from_components() + + # Validate that UP status has no reason + if (self.status == HealthStatus.UP) and self.reason: + msg = f"Health {self.status} must not have reason" + raise ValueError(msg) + + # Validate that DOWN and DEGRADED status always have a reason + if (self.status in {HealthStatus.DOWN, HealthStatus.DEGRADED}) and not self.reason: + msg = f"Health {self.status} must have a reason" + raise ValueError(msg) + + return self + + def __str__(self) -> str: + """Return string representation of health status with optional reason for DOWN/DEGRADED state. + + Returns: + str: The health status value, with reason appended if status is DOWN or DEGRADED. + """ + if self.status in {HealthStatus.DOWN, HealthStatus.DEGRADED} and self.reason: + return f"{self.status.value}: {self.reason}" + return self.status.value + + def __bool__(self) -> bool: + """Convert health status to a boolean value. + + Returns: + bool: True if the status is UP, False otherwise. + """ + return self.status == HealthStatus.UP diff --git a/tests/aignostics_foundry_core/greet_test.py b/tests/aignostics_foundry_core/greet_test.py deleted file mode 100644 index 64e9fd7..0000000 --- a/tests/aignostics_foundry_core/greet_test.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Tests for the greet module.""" - -import pytest - -from aignostics_foundry_core import greet - - -class TestGreet: - """Test cases for the greet function.""" - - @pytest.mark.unit - def test_greet_returns_hello_message(self) -> None: - """Test that greet returns the expected greeting format.""" - result = greet("World") - assert result == "Hello, World!" - - @pytest.mark.unit - def test_greet_with_different_names(self) -> None: - """Test greet with various names.""" - assert greet("Python") == "Hello, Python!" - assert greet("Alice") == "Hello, Alice!" - - @pytest.mark.unit - def test_greet_with_empty_string(self) -> None: - """Test greet with an empty string.""" - assert greet("") == "Hello, !" - - @pytest.mark.unit - def test_greet_with_special_characters(self) -> None: - """Test greet handles special characters.""" - assert greet("Test-User_123") == "Hello, Test-User_123!" diff --git a/tests/aignostics_foundry_core/health_test.py b/tests/aignostics_foundry_core/health_test.py new file mode 100644 index 0000000..9444596 --- /dev/null +++ b/tests/aignostics_foundry_core/health_test.py @@ -0,0 +1,245 @@ +"""Tests for health models and status definitions.""" + +import pytest + +from aignostics_foundry_core.health import Health, HealthStatus + +# Test constants +DB_FAILURE = "DB failure" +SLOW_QUERIES = "Slow queries" +PARTIAL_OUTAGE = "Partial outage" +CACHE_FAILURE = "Cache failure" +ORIGINAL_FAILURE = "Original failure" +AUTH_ERROR = "Auth error" +DEEP_FAILURE = "Deep failure" +SERVICE_UNAVAILABLE = "Service unavailable" + + +class TestHealth: + """Test cases for the Health model and HealthStatus enum.""" + + @pytest.mark.unit + def test_degraded_status_requires_reason(self) -> None: + """Test that DEGRADED status requires a reason.""" + health = Health(status=HealthStatus.DEGRADED, reason=PARTIAL_OUTAGE) + assert health.status == HealthStatus.DEGRADED + assert health.reason == PARTIAL_OUTAGE + + with pytest.raises(ValueError, match="Health DEGRADED must have a reason"): + Health(status=HealthStatus.DEGRADED) + + @pytest.mark.unit + def test_down_status_requires_reason(self) -> None: + """Test that DOWN status requires a reason.""" + health = Health(status=HealthStatus.DOWN, reason="Database connection failed") + assert health.status == HealthStatus.DOWN + assert health.reason == "Database connection failed" + + with pytest.raises(ValueError, match="Health DOWN must have a reason"): + Health(status=HealthStatus.DOWN) + + @pytest.mark.unit + def test_up_status_must_not_have_reason(self) -> None: + """Test that UP status cannot have a reason.""" + with pytest.raises(ValueError, match="Health UP must not have reason"): + Health(status=HealthStatus.UP, reason="This should not be allowed") + + @pytest.mark.unit + def test_str_up(self) -> None: + """Test string representation of UP health status.""" + health = Health(status=HealthStatus.UP) + assert str(health) == "UP" + + @pytest.mark.unit + def test_str_degraded(self) -> None: + """Test string representation of DEGRADED health status.""" + health = Health(status=HealthStatus.DEGRADED, reason="Some issues") + assert str(health) == "DEGRADED: Some issues" + + @pytest.mark.unit + def test_str_down(self) -> None: + """Test string representation of DOWN health status.""" + health = Health(status=HealthStatus.DOWN, reason=SERVICE_UNAVAILABLE) + assert str(health) == f"DOWN: {SERVICE_UNAVAILABLE}" + + @pytest.mark.unit + def test_bool_up_is_true(self) -> None: + """Test that UP health status evaluates to True.""" + health = Health(status=HealthStatus.UP) + assert bool(health) is True + + @pytest.mark.unit + def test_bool_down_is_false(self) -> None: + """Test that DOWN health status evaluates to False.""" + health = Health(status=HealthStatus.DOWN, reason=SERVICE_UNAVAILABLE) + assert bool(health) is False + + @pytest.mark.unit + def test_compute_no_components_unchanged(self) -> None: + """Test that health status is unchanged when there are no components.""" + health = Health(status=HealthStatus.UP) + result = health.compute_health_from_components() + + assert result.status == HealthStatus.UP + assert result.reason is None + assert result is health + + @pytest.mark.unit + def test_compute_single_degraded_component(self) -> None: + """Test that health becomes DEGRADED when a single component is DEGRADED.""" + health = Health(status=HealthStatus.UP) + health.components = { + "database": Health(status=HealthStatus.DEGRADED, reason=SLOW_QUERIES), + "cache": Health(status=HealthStatus.UP), + } + + result = health.compute_health_from_components() + + assert result.status == HealthStatus.DEGRADED + assert result.reason == f"Component 'database' is DEGRADED ({SLOW_QUERIES})" + assert result is health + + @pytest.mark.unit + def test_compute_multiple_degraded_components(self) -> None: + """Test that health becomes DEGRADED with correct reason when multiple components are DEGRADED.""" + health = Health(status=HealthStatus.UP) + health.components = { + "database": Health(status=HealthStatus.DEGRADED, reason=SLOW_QUERIES), + "cache": Health(status=HealthStatus.DEGRADED, reason="Eviction lag"), + "api": Health(status=HealthStatus.UP), + } + + result = health.compute_health_from_components() + + assert result.status == HealthStatus.DEGRADED + assert result.reason is not None + assert "Components 'database' (Slow queries), 'cache' (Eviction lag) are DEGRADED" in result.reason + assert result is health + + @pytest.mark.unit + def test_compute_single_down_component(self) -> None: + """Test that health becomes DOWN when a single component is DOWN.""" + health = Health(status=HealthStatus.UP) + health.components = { + "database": Health(status=HealthStatus.DOWN, reason=DB_FAILURE), + "cache": Health(status=HealthStatus.UP), + } + + result = health.compute_health_from_components() + + assert result.status == HealthStatus.DOWN + assert result.reason == f"Component 'database' is DOWN ({DB_FAILURE})" + assert result is health + + @pytest.mark.unit + def test_compute_multiple_down_components(self) -> None: + """Test that health becomes DOWN with correct reason when multiple components are DOWN.""" + health = Health(status=HealthStatus.UP) + health.components = { + "database": Health(status=HealthStatus.DOWN, reason=DB_FAILURE), + "cache": Health(status=HealthStatus.DOWN, reason=CACHE_FAILURE), + "api": Health(status=HealthStatus.UP), + } + + result = health.compute_health_from_components() + + assert result.status == HealthStatus.DOWN + assert result.reason is not None + assert "Components '" in result.reason + assert "database" in result.reason + assert "cache" in result.reason + assert "are DOWN" in result.reason + assert result is health + + @pytest.mark.unit + def test_compute_down_trumps_degraded(self) -> None: + """Test that DOWN status takes precedence over DEGRADED in health aggregation.""" + health = Health(status=HealthStatus.UP) + health.components = { + "database": Health(status=HealthStatus.DEGRADED, reason=SLOW_QUERIES), + "cache": Health(status=HealthStatus.DOWN, reason=CACHE_FAILURE), + "api": Health(status=HealthStatus.UP), + } + + result = health.compute_health_from_components() + + assert result.status == HealthStatus.DOWN + assert result.reason is not None + assert "Component 'cache' is DOWN" in result.reason + assert result is health + + @pytest.mark.unit + def test_compute_already_down_preserved(self) -> None: + """Test that pre-existing DOWN reason is not overwritten when already DOWN.""" + health = Health(status=HealthStatus.DOWN, reason=ORIGINAL_FAILURE) + health.components = { + "database": Health(status=HealthStatus.DOWN, reason=DB_FAILURE), + "cache": Health(status=HealthStatus.UP), + } + + result = health.compute_health_from_components() + + assert result.status == HealthStatus.DOWN + assert result.reason == ORIGINAL_FAILURE + assert result is health + + @pytest.mark.unit + def test_compute_recursive(self) -> None: + """Test that DOWN propagates recursively through a multi-level component tree.""" + deep_component = Health(status=HealthStatus.DOWN, reason=DEEP_FAILURE) + mid_component = Health( + status=HealthStatus.UP, + components={"deep": deep_component}, + ) + health = Health( + status=HealthStatus.UP, + components={"mid": mid_component, "other": Health(status=HealthStatus.UP)}, + ) + + result = health.compute_health_from_components() + + assert result.status == HealthStatus.DOWN + assert result.reason is not None + assert "Component 'mid' is DOWN" in result.reason + assert health.components["mid"].status == HealthStatus.DOWN + assert health.components["mid"].reason is not None + assert "Component 'deep' is DOWN" in health.components["mid"].reason + assert health.components["other"].status == HealthStatus.UP + + @pytest.mark.unit + def test_validate_integration(self) -> None: + """Test that DOWN propagates automatically through a complex tree on construction.""" + health = Health( + status=HealthStatus.UP, + components={ + "database": Health(status=HealthStatus.UP), + "services": Health( + status=HealthStatus.UP, + components={ + "auth": Health(status=HealthStatus.DOWN, reason=AUTH_ERROR), + "storage": Health(status=HealthStatus.UP), + }, + ), + "monitoring": Health(status=HealthStatus.UP), + }, + ) + + assert health.status == HealthStatus.DOWN + assert health.reason is not None + assert "Component 'services' is DOWN" in health.reason + + assert health.components["services"].status == HealthStatus.DOWN + assert health.components["services"].reason is not None + assert "Component 'auth' is DOWN" in health.components["services"].reason + + assert health.components["database"].status == HealthStatus.UP + assert health.components["monitoring"].status == HealthStatus.UP + + @pytest.mark.unit + def test_component_without_reason_raises(self) -> None: + """Test that constructing a DOWN or DEGRADED component without a reason raises ValueError.""" + with pytest.raises(ValueError, match="Health DOWN must have a reason"): + Health(status=HealthStatus.DOWN) + + with pytest.raises(ValueError, match="Health DEGRADED must have a reason"): + Health(status=HealthStatus.DEGRADED) diff --git a/uv.lock b/uv.lock index 1516969..b32734a 100644 --- a/uv.lock +++ b/uv.lock @@ -9,6 +9,9 @@ overrides = [{ name = "pytest", specifier = ">=9.0.2" }] name = "aignostics-foundry-core" version = "0.0.0" source = { editable = "." } +dependencies = [ + { name = "pydantic" }, +] [package.dev-dependencies] dev = [ @@ -40,6 +43,7 @@ dev = [ ] [package.metadata] +requires-dist = [{ name = "pydantic", specifier = ">=2,<3" }] [package.metadata.requires-dev] dev = [ @@ -70,6 +74,15 @@ dev = [ { name = "watchdog", specifier = ">=6.0.0" }, ] +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + [[package]] name = "argcomplete" version = "3.6.3" @@ -1168,6 +1181,118 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304", size = 23045, upload-time = "2025-07-21T09:56:46.848Z" }, ] +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -1877,6 +2002,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + [[package]] name = "tzdata" version = "2025.3"