diff --git a/.codacy.yml b/.codacy.yml index da9235fdf..d83fecdd1 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -1,4 +1,7 @@ --- +exclude_paths: + - "tck/**" + engines: bandit: enabled: true diff --git a/.github/ISSUE_TEMPLATE/04_intermediate_issue.yml b/.github/ISSUE_TEMPLATE/04_intermediate_issue.yml index 44e89fcd9..eef1a8fe8 100644 --- a/.github/ISSUE_TEMPLATE/04_intermediate_issue.yml +++ b/.github/ISSUE_TEMPLATE/04_intermediate_issue.yml @@ -1,91 +1,104 @@ name: Intermediate Issue Template -description: Create a well-documented issue for contributors with some familiarity with the codebase +description: Create a well-documented issue for contributors ready to own larger tasks independently title: "[Intermediate]: " labels: ["intermediate"] assignees: [] - body: - type: markdown attributes: value: | --- - ## **Thanks for contributing!** ๐Ÿ˜Š + **For issue creators:** Intermediate issues sit between Beginner Issues and Advanced Issues. + The contributor should be able to research, implement, and test independently โ€” + but should not need to make architectural decisions or redesign core abstractions. + + If the task can be solved by following examples alone, it belongs in a Beginner Issue. + If it requires cross-module architectural reasoning, it belongs in Advanced. - We truly appreciate your time and effort. - This template is designed to help you create an Intermediate issue. + **All links in this template must be absolute URLs** (starting with `https://`). + Relative links break when rendered in GitHub issues. - The goal is to create an issue for users that have: - - basic familiarity with the Hiero Python SDK codebase - - experience following our contribution workflow - - confidence navigating existing source code and examples --- - type: textarea - id: intro + id: before_you_start attributes: label: ๐Ÿงฉ Intermediate Contributors - description: Who is this issue for? - value: | - This issue is intended for contributors who already have some familiarity with the - [Hiero Python SDK](https://hiero.org) codebase and contribution workflow. - - You should feel comfortable: - - navigating existing source code and examples - - understanding SDK concepts without step-by-step guidance - - following the standard PR workflow without additional onboarding - - If this is your very first contribution to the project, we recommend starting with a few - **Good First Issues** before working on this one. - validations: - required: false - - - type: markdown - attributes: + description: | + THIS SECTION IS PRE-FILLED. LEAVE AS-IS UNLESS YOU NEED TO ADD ISSUE-SPECIFIC PREREQUISITES. value: | + Welcome! This Intermediate Issue is designed for contributors who have completed beginner-level work and are ready to take on more ownership. + + It nudges you to: + + - Research across multiple files and modules + - Own implementation decisions โ€” not just follow a recipe + - Write thorough, meaningful tests + - Deliver a clean, review-ready pull request + + **Prerequisites:** + > [!IMPORTANT] - > ### ๐Ÿงญ What we consider an *Intermediate Issue* - > - > This issue generally: - > - > - Requires **some knowledge of the existing codebase**, but not deep architectural knowledge - > - Is **narrowly scoped** and well-contained - > - Is **low to medium risk**, not touching highly sensitive or critical DLT logic - > - May involve **refactors or small feature additions** - > - Provides **enough documentation or examples** to reason about a solution - > - Often has **similar patterns elsewhere in the codebase** - > - > **What this issue is NOT:** - > - A beginner-friendly onboarding task - > - A large architectural redesign - > - A change requiring extensive cross-module refactors - > - A breaking API change + > **Before claiming this issue, you must have:** + > - Completed at least 1 Good First Issue **and** 1 Beginner Issue + > - The contribution workflow nailed: signing, rebasing, CHANGELOG, linking issues + > - The ability to operate independently โ€” you may ask high-level architecture questions, but should not need step-by-step guidance + + > [!TIP] + > **You should be comfortable with:** + > - Navigating multiple modules across `src/`, `tests/`, and `examples/` + > - Reading and following [protobuf definitions](https://github.com/hashgraph/hedera-protobufs/tree/main/services) for correct naming, types, and field ordering + > - Writing meaningful tests โ€” not just AI-generated tests of AI-generated code + > - Identifying and avoiding breaking changes to public APIs + > - Self-reviewing your own PR before submitting + + If this feels like too big a step, that is completely fine โ€” try a + [Beginner Issue](https://github.com/hiero-ledger/hiero-sdk-python/issues?q=is%3Aissue+state%3Aopen+label%3Abeginner+no%3Aassignee) + first. You can always come back when you are ready. + + **Support:** A maintainer is available for high-level guidance โ€” implementation approach, test strategy, or design questions. Don't hesitate to ask. + + โฑ๏ธ Typical time to complete: 1 week part-time / 3 days full-time + ๐Ÿงฉ Difficulty: Requires investigation, testing, and polish โ€” most time should go to research and refinement, not coding + ๐ŸŽ“ Best for: Contributors who've completed beginner issues + + --- + + ๐Ÿ **When this issue is complete, you will have:** + + โœ… Researched across multiple files and modules + โœ… Owned implementation decisions + โœ… Written thorough, meaningful tests + โœ… Delivered a clean, review-ready pull request + โœ… Confidence to take on advanced issues + validations: + required: false - type: textarea id: problem attributes: label: ๐Ÿž Problem Description description: | - Describe the problem clearly and precisely. - - You may assume the reader: - - understands the SDK structure - - can navigate `src/`, `examples/`, and `tests/` - - is comfortable reading existing implementations - - Still, explain: - - what is wrong or missing - - where it lives in the codebase - - why it matters + DESCRIBE THE PROBLEM CLEARLY. + YOU MAY ASSUME THE READER CAN NAVIGATE src/, tests/, AND examples/. + EXPLAIN: WHAT IS WRONG OR MISSING, WHERE IT LIVES, AND WHY IT MATTERS. + INCLUDE RELEVANT FILE PATHS AND LINKS. + CONSIDER ADDING: EXPECTED TIME (e.g. 1-2 WEEKS), RELEVANT LABELS, AND A NOTE TO CHECK THE CODERABBIT AI PLAN. value: | - Describe the problem here. + _Replace this with the problem description._ + + **Before claiming:** + - Check the issue labels to see what area this touches + - Read the CodeRabbit AI plan (posted as a comment) for a rough implementation overview + - If this feels like too big a step, try a beginner issue first validations: required: true - type: markdown attributes: value: | - - ## ๐Ÿž Problem โ€“ Example + + ## Example โ€” Problem Description The `TransactionGetReceiptQuery` currently exposes the `get_children()` method, but the behavior is inconsistent with how child receipts are returned by the Mirror Node. @@ -107,23 +120,19 @@ body: attributes: label: ๐Ÿ’ก Expected Solution description: | - Describe the intended outcome. - - This does NOT need to be a full implementation plan, - but should explain: - - what should change - - what should NOT change - - any constraints or boundaries + BRIEFLY STATE WHAT THE CONTRIBUTOR SHOULD BUILD OR FIX. + THIS SHOULD BE A SHORT SUMMARY OF THE EXPECTED OUTCOME โ€” NOT A DETAILED IMPLEMENTATION GUIDE. + EXAMPLE: "Add a __repr__ method to AccountId that shows shard, realm, and num." value: | - Describe the expected solution here. + _Replace this with a brief solution summary._ validations: required: true - type: markdown attributes: value: | - - ## ๐Ÿ’ก Expected Solution โ€“ Example + + ## Example โ€” Expected Solution Introduce an optional configuration flag on `TransactionGetReceiptQuery` that allows callers to explicitly request child receipts. @@ -145,67 +154,216 @@ body: ``` - type: textarea - id: implementation + id: research attributes: - label: ๐Ÿง  Implementation Notes + label: ๐Ÿ” Background Research description: | - Provide technical guidance to help implementation. - - Examples: - - files or modules likely involved - - patterns already used elsewhere - - things to be careful about - - known edge cases + POINT THE CONTRIBUTOR TOWARD RELEVANT FILES, SIMILAR CODE, PROTOBUF DEFINITIONS, OR PATTERNS TO STUDY. + THIS IS THE "INVESTIGATE BEFORE YOU CODE" STEP. value: | - Add implementation notes here. + _Replace this with research pointers._ validations: - required: false + required: true - type: markdown attributes: value: | - - ## ๐Ÿง  Implementation Notes โ€“ Example + + ## Example โ€” Background Research - Likely steps: + - Open `src/hiero_sdk_python/query/transaction_get_receipt_query.py` and read how the query is built + - Compare with `TransactionGetRecordQuery` which already supports similar flags + - Check the [protobuf definition](https://github.com/hashgraph/hedera-protobufs/blob/main/services/transaction_get_receipt.proto) for the response structure + - Look at how child receipts are structured in the proto response - - Add an optional boolean field (e.g. `_include_children`) to - `TransactionGetReceiptQuery` + - type: textarea + id: implementation + attributes: + label: ๐Ÿ› ๏ธ Implementation + description: | + DESCRIBE WHAT THE CONTRIBUTOR SHOULD BUILD AFTER COMPLETING THEIR RESEARCH. + KEEP IT HIGH-LEVEL โ€” THEY SHOULD FIGURE OUT THE DETAILS FROM THEIR RESEARCH. + value: | + _Replace this with implementation guidance._ + validations: + required: true + + - type: markdown + attributes: + value: | + + ## Example โ€” Implementation + + - Add an optional boolean field (e.g. `_include_children`) to `TransactionGetReceiptQuery` - Ensure the flag is passed to the mirror node request - Update response parsing to include child receipts when present - - Extend the existing example in - `examples/query/transaction_get_receipt_query.py` - to demonstrate the new behavior + - Keep default behavior unchanged (backwards compatible) + - Extend the existing example to demonstrate the new behavior - Similar patterns can be found in other query classes that support - optional response extensions. + - type: textarea + id: testing + attributes: + label: ๐Ÿงช Testing Requirements + description: | + EDIT THIS SECTION TO MATCH THE TYPE OF CHANGE. DELETE CATEGORIES THAT DO NOT APPLY. + TESTING IS A MAJOR COMPONENT OF INTERMEDIATE ISSUES โ€” SET CLEAR EXPECTATIONS. + value: | + How you test depends on the type of change. + See the [testing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/testing.md) for full details. + + > [!IMPORTANT] + > At the intermediate level, **testing is a major component** โ€” not an afterthought. + > Each method you implement should be verified. Tests should cover happy paths, + > edge cases, and error handling. Tests must be understandable and reviewable, + > not just generated by AI. + + **Source code changes (e.g. Python files in `src/`):** + - Write unit tests covering happy path, edge cases, and error handling + - Run tests locally: `uv run pytest tests/unit/_test.py -v` + - Verify naming, types, and field ordering match [protobuf definitions](https://github.com/hashgraph/hedera-protobufs/tree/main/services) + - Check consistency with similar classes already in the SDK โ€” use the same patterns + - Integration tests will run automatically when you push + + **GitHub Actions / workflow changes (e.g. files in `.github/`):** + - Test by merging to your fork's `main` and simulating the scenario + - Create test issues or PRs as evidence and link them in your PR + - Include screenshots of workflow runs where applicable + + **Example script changes (e.g. files in `examples/`):** + - Run the example script and confirm output matches expected behavior + - Compare your example with similar existing examples for consistency + - You will need a [Hedera Portal](https://portal.hedera.com/) account for testnet credentials + + **In your PR, include links to test evidence:** screenshots, terminal output, test issue/PRs, or script results. + validations: + required: true - type: textarea - id: acceptance-criteria + id: quality_standards attributes: - label: โœ… Acceptance Criteria - description: Define what "done" means for this issue + label: ๐Ÿ›ก๏ธ Quality & Review Standards + description: | + PRE-FILLED GUIDANCE. LEAVE AS-IS. value: | - To merge this issue, the pull request must: + Intermediate PRs cover more code and touch more sensitive areas. This section helps you + deliver a PR that is ready to merge on first or second review. - - [ ] Fully address the problem described above - - [ ] Follow existing project conventions and patterns - - [ ] Include tests or example updates where appropriate - - [ ] Pass all CI checks - - [ ] Include a valid changelog entry - - [ ] be a DCO and GPG key signed as `git commit -S -s -m "chore: my change"` with a GPG key set up + **โš ๏ธ Breaking changes** + - Before changing any function signature, return type, or public API โ€” stop and check + - If a breaking change is unavoidable: add backwards compatibility and get explicit maintainer approval **before** implementing + - Run existing tests to verify nothing breaks: `uv run pytest tests/unit/ -v` + + **๐Ÿค– AI-assisted code** + - AI is a powerful tool โ€” but every suggestion must be verified, not just accepted + - Cross-reference AI output against [protobuf definitions](https://github.com/hashgraph/hedera-protobufs/tree/main/services) and similar SDK implementations + - Do not submit AI-generated tests that merely test AI-generated code โ€” tests should verify real behavior and edge cases + - If AI output and existing SDK patterns disagree, trust the SDK + + **๐Ÿ“ฆ PR packaging** + - Self-review your diff line by line before submitting + - Clean git history โ€” no rebase artifacts, merge commits, or unrelated files + - CHANGELOG entry under `[Unreleased]` + - Link test evidence (screenshots, terminal output, test PRs) in your PR description + - Ensure all CI checks pass before requesting review + - Double and triple check โ€” intermediate PRs are time-consuming to review + validations: + required: false + + - type: textarea + id: acceptance + attributes: + label: โœ… Done Checklist + description: | + EDIT OR ADD CRITERIA SPECIFIC TO THIS ISSUE. THE DEFAULTS COVER THE STANDARD REQUIREMENTS. + value: | + Before opening your PR, confirm: + + - [ ] My changes fully address the problem described above + - [ ] I verified naming, types, and patterns against protobuf definitions and similar SDK classes + - [ ] I tested my changes thoroughly (see testing section above) and linked evidence in my PR + - [ ] I checked for breaking changes โ€” no existing tests fail, no public APIs changed without approval + - [ ] I did not modify files unrelated to this issue + - [ ] My commits are signed: `git commit -S -s -m "chore: description"` โ€” [Signing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/signing.md) + - [ ] I added a CHANGELOG.md entry under `[Unreleased]` โ€” [Changelog guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/changelog_entry.md) + - [ ] All CI checks pass + - [ ] I self-reviewed my PR before requesting review validations: required: true - type: textarea - id: additional-info + id: contribution_steps attributes: - label: ๐Ÿ“š Additional Context or Resources + label: ๐Ÿ“‹ Workflow quick reference description: | - Add any links, references, or extra notes that may help. + PRE-FILLED REFERENCE TABLE. LEAVE AS-IS. value: | - - [SDK Developer Docs](https://github.com/hiero-ledger/hiero-sdk-python/tree/main/docs/sdk_developers) - - [SDK Developer Training](https://github.com/hiero-ledger/hiero-sdk-python/tree/main/docs/sdk_developers/training) + You know the workflow โ€” here are the links if you need them: + + | Step | Guide | + |------|-------| + | Claim this issue | Comment `/assign` below | + | Sync with main | [Rebasing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/rebasing.md) | + | Open a PR and link this issue | [PR guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/11_submit_pull_request.md) ยท [Linking guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/workflow/how_to_link_issues.md) | + | Resolve merge conflicts | [Merge conflicts guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/merge_conflicts.md) | + | Testing | [Testing guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/testing.md) | + validations: + required: false + + - type: textarea + id: ai_tips + attributes: + label: ๐Ÿค– Tips for using AI tools + description: | + PRE-FILLED GUIDANCE. LEAVE AS-IS. + value: | + Here is how to get the most out of AI tools at the intermediate level: + + - Use [Pylance](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/pylance.md) in VS Code to catch type errors and hallucinated methods in real time + - Always verify AI suggestions against [protobuf definitions](https://github.com/hashgraph/hedera-protobufs/tree/main/services) โ€” AI often gets field names, types, or ordering wrong + - Build your solution in small steps โ€” add print statements or logs to confirm each piece works before moving on + - When AI suggests code, check it against similar classes in the SDK โ€” that is the best source of Python SDK-specific patterns + - If AI output and SDK patterns disagree, trust the SDK + - Write tests that verify real behavior, not tests that test AI-generated code + validations: + required: false + + - type: textarea + id: stuck + attributes: + label: ๐Ÿ†˜ Stuck? + description: | + PRE-FILLED GUIDANCE. LEAVE AS-IS. + value: | + Intermediate issues can be challenging โ€” it is completely normal to get blocked. Here is what to do: + + - **Comment on this issue** and describe what you have tried. A maintainer will respond. + - **Ask on [Discord](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/discord.md)** in #hiero-python-sdk for quick questions. + - **Join [Office Hours](https://zoom-lfx.platform.linuxfoundation.org/meeting/99912667426?password=5b584a0e-1ed7-49d3-b2fc-dc5ddc888338)** (Wednesdays, 2pm UTC) for live, hands-on help with a maintainer โ€” screen sharing welcome. + + At this level, you are expected to research independently โ€” but asking good, high-level questions is a strength, not a weakness. Don't spend more than an hour blocked without asking. + validations: + required: false + + - type: textarea + id: resources + attributes: + label: ๐Ÿ“š Resources + description: | + ADD ANY TASK-SPECIFIC RESOURCES BELOW โ€” RELEVANT PROTOBUF DEFINITIONS, PYTHON DOCS, OR SPECIFIC FILES IN THE CODEBASE. + value: | + **Project references:** + - [Project structure](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/setup/project_structure.md) + - [CONTRIBUTING.md](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/CONTRIBUTING.md) + - [Browse closed intermediate PRs](https://github.com/hiero-ledger/hiero-sdk-python/pulls?q=is%3Apr+is%3Amerged+label%3Aintermediate) โ€” see how others did it + + **Protobuf references:** + - [Hedera Protobufs](https://github.com/hashgraph/hedera-protobufs/tree/main/services) โ€” source of truth for field names, types, and ordering + - [Protobuf language guide](https://protobuf.dev/programming-guides/proto3/) + **Python references:** + - [Python official docs](https://docs.python.org/3/) + - [Data model (dunder methods)](https://docs.python.org/3/reference/datamodel.html) + - [Type hints](https://docs.python.org/3/library/typing.html) + - [unittest.mock](https://docs.python.org/3/library/unittest.mock.html) โ€” for mocking in tests validations: required: false diff --git a/.github/ISSUE_TEMPLATE/05_advanced_issue.yml b/.github/ISSUE_TEMPLATE/05_advanced_issue.yml index d423d31ff..b91b3805b 100644 --- a/.github/ISSUE_TEMPLATE/05_advanced_issue.yml +++ b/.github/ISSUE_TEMPLATE/05_advanced_issue.yml @@ -1,226 +1,104 @@ name: Advanced Issue Template -description: Create a high-impact issue for experienced contributors with deep familiarity with the codebase +description: For expert contributors tackling architectural, multi-module, or core-logic changes. title: "[Advanced]: " labels: ["advanced"] assignees: [] body: - - type: markdown + - type: textarea + id: advanced_contributors attributes: + label: ๐Ÿง  Advanced Contributors โ€” Prerequisites & Expectations + description: | + REQUIRED: Contributors must meet these thresholds before claiming. value: | - --- - ## **Thanks for contributing!** ๐Ÿš€ - - We truly appreciate your interest in tackling an **Advanced issue**. - - This template is designed for work that requires **deep familiarity with the Hiero Python SDK** - and confidence making changes that may span multiple modules or affect core behavior. + > [!CAUTION] + > **Advanced issues are the highest-risk work in this project. We will reject PRs that do not meet these standards.** - The goal is to create issues for contributors who: - - have strong familiarity with the Hiero Python SDK internals - - are comfortable reasoning about trade-offs and design decisions - - can work independently with minimal guidance - --- + ### ๐Ÿ Concrete Prerequisites + - **Proven History:** Successfully completed **โ‰ฅ 10 non-trivial intermediate issues** in this repo. + - **Consistency:** **โ‰ฅ 3โ€“4 months** of active, human-led contributions to this SDK. + - **Expertise:** Deep architectural understanding of `_Executable`, `Transaction`, and `Query` base classes. + - **Advanced Python:** Demonstrated proficiency with complex patterns (e.g., async concurrency, decorators, or state management) used in the core SDK. - - type: textarea - id: intro - attributes: - label: ๐Ÿง  Advanced Contributors - description: Who is this issue intended for? - value: | - This issue is intended for contributors who are already very familiar with the - [Hiero Python SDK](https://hiero.org) codebase and its architectural patterns. + > [!NOTE] + > **Workflow Exception:** For issues focused on **GitHub Actions / Workflows**, the Python-specific thresholds above may be waived if the contributor demonstrates expert-level proficiency in CI/CD security and automation. - You should feel comfortable: - - navigating multiple modules across `src/` - - understanding and modifying core SDK abstractions - - reasoning about API design and backwards compatibility - - updating or extending tests, examples, and documentation as needed - - making changes that may affect public-facing behavior + ### โš ๏ธ AI Usage Policy + Using AI to generate code for Advanced issues is **strictly forbidden**. AI may be used only to help explain file relationships. Submitting AI-mined code or unvalidated generated output is grounds for **immediate rejection**. - New developers should start with - **Good First Issues** or **Intermediate Issues** first. + ### โฑ๏ธ Timeline & Workflow + - **Typical time:** ~1 month / ~50 hours. + - ๐Ÿ”ด Completing an advanced issue in 1โ€“3 days is a **red flag** and will likely be rejected. + - **Mandatory:** Post your proposed architectural approach as a comment and wait for explicit maintainer approval **before writing any code.** validations: required: false - - type: markdown - attributes: - value: | - > [!WARNING] - > ### ๐Ÿงญ What we consider an *Advanced Issue* - > - > This issue typically: - > - > - Requires **deep understanding of existing SDK design and behavior** - > - May touch **core abstractions**, shared utilities, or cross-cutting concerns - > - May involve **non-trivial refactors**, design changes, or behavior extensions - > - Has **medium to high risk** if implemented incorrectly - > - Requires careful consideration of **backwards compatibility** - > - May require updating **tests, examples, and documentation together** - > - > **What this issue is NOT:** - > - A simple bug fix - > - A narrowly scoped refactor - > - A task solvable by following existing patterns alone - > - > ๐Ÿ“– Helpful references: - > - `docs/sdk_developers/training` - > - `docs/sdk_developers/training/setup/project_structure.md` - > - `docs/sdk_developers/rebasing.md` - - type: textarea id: problem attributes: label: ๐Ÿž Problem Description - description: | - Describe the problem in depth. - - You may assume the reader: - - understands the overall SDK architecture - - can navigate and reason about multiple modules - - is comfortable reading and modifying core logic - - Clearly explain: - - what the current behavior is - - why it is insufficient or incorrect - - which components or layers are involved - - any relevant historical or design context - value: | - Describe the problem here. - validations: - required: true - - - type: markdown - attributes: - value: | - - ## ๐Ÿž Problem โ€“ Example - - The current transaction execution pipeline tightly couples - receipt retrieval, record retrieval, and retry logic into a single - execution flow. - - This coupling makes it difficult to: - - customize retry behavior - - extend execution semantics for scheduled or mirror-node-backed workflows - - test individual stages of transaction execution in isolation - - Several downstream SDK features would benefit from a clearer separation - of concerns in this area. - - Relevant areas: - - `src/hiero_sdk_python/transaction/` - - `src/hiero_sdk_python/execution/` - - `src/hiero_sdk_python/client/` - - - type: textarea - id: solution - attributes: - label: ๐Ÿ’ก Proposed / Expected Solution - description: | - Describe the intended direction or design. - - This should include: - - the high-level approach - - any new abstractions or changes to existing ones - - constraints (e.g. backwards compatibility, performance, API stability) - - known alternatives and why they were rejected (if applicable) - - A full design document is not required, but reasoning and intent should be clear. - value: | - Describe the proposed solution here. + placeholder: "Describe the current limitation or architectural risk here..." validations: required: true - - type: markdown - attributes: - value: | - - ## ๐Ÿ’ก Proposed Solution โ€“ Example - - Introduce a dedicated execution pipeline abstraction that separates: - - transaction submission - - receipt polling - - record retrieval - - retry and timeout logic - - The new design should: - - preserve existing public APIs - - allow advanced users to override or extend execution behavior - - make individual stages independently testable - - Existing transaction execution should be reimplemented - using the new pipeline internally. - - type: textarea id: implementation attributes: - label: ๐Ÿง  Implementation & Design Notes - description: | - Provide detailed technical guidance. - - This section is especially important for Advanced issues. - - Consider including: - - specific modules or classes involved - - suggested refactoring strategy - - migration or deprecation concerns - - testing strategy - - performance or security considerations + label: ๐Ÿ› ๏ธ Implementation Notes value: | - Add detailed implementation notes here. + ### Technical domains involved in this issue: + - [ ] **API Client Architecture** (request โ†’ serialization โ†’ execution โ†’ response mapping) + - [ ] **Backward Compatibility** (preserving method signatures, defaults, and return types) + - [ ] **Protobuf Alignment** (reading `.proto` files, `_to_proto()` / `_from_proto()` correctness) + - [ ] **State & Immutability** (correct usage of guards like `_require_not_frozen`) + - [ ] **Execution Boundaries** (retry logic, backoff, node selection, gRPC deadlines) + + _Replace this with specific implementation notes._ validations: required: false - - type: markdown + - type: textarea + id: quality_standards attributes: + label: ๐Ÿ›ก๏ธ Quality & Review Standards value: | - - ## ๐Ÿง  Implementation Notes โ€“ Example - - Suggested approach: + The bar for advanced PRs is **"safe, maintainable, architecturally sound, and production-ready."** - - Introduce a new `ExecutionPipeline` abstraction under - `src/hiero_sdk_python/execution/` - - Refactor existing transaction execution logic to delegate - to this pipeline - - Ensure existing public APIs remain unchanged - - Add focused unit tests for each pipeline stage - - Update at least one example to demonstrate extensibility - - Care should be taken to avoid breaking timeout semantics - relied upon by existing users. + ### ๐Ÿš€ Defining "Production Ready" + 1. **Architectural Fit:** The solution must fit naturally into the existing SDK abstractions. Avoid "hacks" or isolated logic that bypasses the core execution model. + 2. **Security & Correctness:** Evaluate all logic for injection risks, state corruption, or thread-safety issues. Every line of code must be manually verified. + 3. **Maintainability:** Code must be clear enough for any other maintainer to debug without your assistance. Prefer standard patterns over "clever" one-liners. + 4. **Backward Compatibility:** Public API signatures must be preserved. If a breaking change is required, it must be explicitly managed through a deprecation cycle. + validations: + required: false - type: textarea - id: acceptance-criteria + id: acceptance attributes: - label: โœ… Acceptance Criteria - description: Define what "done" means for this issue + label: โœ… PR Quality Checklist + description: | + PRE-FILLED. These are the standards the contributor must meet before opening a PR. value: | - To merge this issue, the pull request must: - - - [ ] Fully address the problem and design goals described above - - [ ] Maintain backwards compatibility unless explicitly approved otherwise - - [ ] Follow existing architectural and coding conventions - - [ ] Include comprehensive tests covering new and existing behavior - - [ ] Update relevant examples and documentation - - [ ] Pass all CI checks - - [ ] Include a valid changelog entry - - [ ] be a DCO and GPG key signed as `git commit -S -s -m "chore: my change"` with a GPG key set up - validations: - required: true + Before opening your PR, the contributor must confirm: + - [ ] I understand the system-wide impact of these changes on affected modules and performance. + - [ ] The system design fits with current Hiero SDK architectural approaches. + - [ ] I have tested my changes extensively against both local and network environments. + - [ ] I have verified naming, types, and field ordering against pinned Protobufs (v0.72.0-rc.2). + - [ ] Every line of code is personally understood and explainable (no unvalidated AI code). - type: textarea - id: additional-info + id: resources attributes: - label: ๐Ÿ“š Additional Context, Links, or Prior Art - description: | - Add any references that may help: - - design docs - - prior discussions - - related issues or PRs - - external references + label: ๐Ÿ“š Resources & Support value: | - Optional. + **Project References:** + - [SDK Project Structure](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/setup/project_structure.md) + - [Transaction Lifecycle](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/transaction_lifecycle.md) + - [Hedera Protobufs (v0.72.0-rc.2)](https://github.com/hashgraph/hedera-protobufs/tree/v0.72.0-rc.2/services) + + **๐Ÿ†˜ Stuck?** + - [Office Hours](https://zoom-lfx.platform.linuxfoundation.org/meeting/99912667426?password=5b584a0e-1ed7-49d3-b2fc-dc5ddc888338) (Wednesdays, 2pm UTC) + - [Discord #hiero-python-sdk](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/discord.md) validations: - required: false + required: false \ No newline at end of file diff --git a/.github/coderabbit/release-pr-prompt.md b/.github/coderabbit/release-pr-prompt.md new file mode 100644 index 000000000..65b478e91 --- /dev/null +++ b/.github/coderabbit/release-pr-prompt.md @@ -0,0 +1,34 @@ +## ๐Ÿ•ต๏ธ Release-Gate Audit: Breaking Changes & Architecture + +You are performing a **Senior Release Engineer** audit on this diff. Your goal is to identify risks that could impact downstream users or system stability. + +### ๐Ÿšจ Critical Instructions +- **Ignore** linting, variable naming, or stylistic preferences. +- **Focus** on public API surface, data schemas, and logic flow. +- **Be Concise**: Use bullet points. If no risks are found, state "No breaking changes identified." + +--- + +### 1. ๐Ÿ› ๏ธ Public API & Contract Changes +Identify any modifications to the public-facing interface. +- **Removals/Renames**: Are any classes, methods, or variables renamed or removed? +- **Signature Changes**: Have parameter types or return types changed in a way that breaks existing calls? +- **Defaults**: Have default values for arguments changed? + +### 2. ๐Ÿ—๏ธ Architectural Integrity +- **Dependency Changes**: Highlight any new external libraries or significant version bumps. +- **Side Effects**: Are there new global states, singletons, or changes to how the application initializes? +- **Resource Usage**: Does this diff introduce patterns that might impact memory or CPU (e.g., new loops, heavy recursive calls)? + +### 3. ๐Ÿ“‰ Backward Compatibility & Data +- **Persistence**: If applicable, do database schema changes or file format changes require a migration? +- **Protocol**: If this communicates via API/WebSockets, is the payload structure still compatible with the previous version? + +### 4. ๐Ÿงช Testing & Validation +- **Coverage Gap**: Are there major logic additions that lack corresponding test files in the diff? +- **Edge Cases**: Identify at least one "what-if" scenario that might cause a failure (e.g., "What if the network is down during this new initialization step?"). + +--- + +### ๐Ÿ Summary Verdict +Provide a 1-sentence summary of the "Safety" of this release candidate. \ No newline at end of file diff --git a/.github/scripts/release-pr-coderabbit-gate.js b/.github/scripts/release-pr-coderabbit-gate.js new file mode 100644 index 000000000..72fe00ca6 --- /dev/null +++ b/.github/scripts/release-pr-coderabbit-gate.js @@ -0,0 +1,143 @@ +/** + * Posts a single "@coderabbit review" comment on release PRs, embedding the + * release review prompt. Designed to run with: + * - permissions: contents: read, pull-requests: write + * + * Safety: + * - Only runs for maintainer-authored PRs (MEMBER/OWNER) + * - Dedupe via hidden marker comment + */ + +const fs = require("fs"); +const path = require("path"); + +const MARKER = ""; + + +function loadPrompt() { + const promptPath = path.join( + process.env.GITHUB_WORKSPACE || ".", + ".github/coderabbit/release-pr-prompt.md" + ); + try { + const content = fs.readFileSync(promptPath, "utf8").trim(); + if (!content) { + throw new Error("Release prompt file is empty"); + } + return content; + } catch (error) { + throw new Error(`Failed to load release prompt from ${promptPath}: ${error.message}`); + } +} + + +async function commentAlreadyExists({ github, owner, repo, issue_number }) { + try { + const data = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number, + per_page: 100, + }); + data.forEach(c => { + if (c.body && c.body.includes(MARKER)) { + console.log(`FOUND MATCH in comment by ${c.user.login}: ${c.html_url}`); + } + }); + return data.some((c) => typeof c.body === "string" && c.body.includes(MARKER)); + } + catch (error) { + console.error(`Error checking for existing comments: ${error.message}`); + return true; // Fail closed to avoid duplicates if we can't verify + } +} + + +function buildBody({ prompt, headRef }) { + return [ + "@coderabbit review", + "", + MARKER, + "", + "## ๐Ÿš€ Release Gate: Cumulative Audit", + "> **Notice to Reviewer**: This is a checkpoint PR for a new release. While the diff here primarily updates versioning and the Changelog, your audit must cover the **entire cumulative scope** of changes since the last version tag.", + "", + "### ๐ŸŽฏ Audit Objectives", + "- Analyze all features merged into `main` since the previous release.", + "- Identify breaking changes in the public API or protocol.", + "- Verify architectural integrity across the Hiero SDK Python modules.", + "", + "
", + "View Senior Audit Constraints", + "", + prompt, + "", + "
", + "", + "---", + `*Auditing Branch: \`${headRef}\` against the project history.*`, + ].join("\n"); +} + +function getSkipReason(pr) { + if (!pr) { + return "No pull_request payload; exiting."; + } + + if (!["MEMBER", "OWNER"].includes(pr.author_association)) { + return `author_association=${pr.author_association}; skipping.`; + } + + const title = (pr.title || "").toLowerCase(); + const isReleaseTitle = + title.startsWith("chore: release v") || title.startsWith("release v"); + + if (!isReleaseTitle) { + return "Not a release PR title; skipping."; + } + + return null; +} + +module.exports = async ({ github, context }) => { + let issue_number = "unknown"; + let headRef = "?"; + let baseRef = "?"; + + try { + const owner = context.repo.owner; + const repo = context.repo.repo; + const pr = context.payload.pull_request; + + const skipReason = getSkipReason(pr); + if (skipReason) { + console.log(skipReason); + return; + } + + issue_number = pr.number; + if (await commentAlreadyExists({ github, owner, repo, issue_number })) { + console.log("Release gate comment already exists. Skipping."); + return; + } + + headRef = pr.head?.ref || "unknown"; + baseRef = pr.base?.ref || "unknown"; + + const prompt = loadPrompt(); + const body = buildBody({ prompt, headRef }); + + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body, + }); + + console.log("Posted CodeRabbit release-gate comment."); + console.log(`PR #${issue_number} (${headRef} โ†’ ${baseRef})`); + } catch (error) { + console.error(`Error in release PR coderabbit gate: ${error.message}`); + console.log(`PR #${issue_number || 'unknown'} (${headRef || '?'} โ†’ ${baseRef || '?'})`); + } +}; \ No newline at end of file diff --git a/.github/spam-list.txt b/.github/spam-list.txt index 22c767db4..ae5f18a3b 100644 --- a/.github/spam-list.txt +++ b/.github/spam-list.txt @@ -1,9 +1,12 @@ -KubanjaElijahEldred -by22Jy -CODEAbhinav-art -zhanglinqian -Halbot100 -roberthallers -SergioChan -ndpvt-web -OnlyTerp \ No newline at end of file +KubanjaElijahEldred +by22Jy +CODEAbhinav-art +zhanglinqian +Halbot100 +roberthallers +SergioChan +ndpvt-web +OnlyTerp +shixian-HFUT +Yuki9814 +ashrafzunaira18 \ No newline at end of file diff --git a/.github/workflows/archive/bot-gfi-notify-team.yml b/.github/workflows/archive/bot-gfi-notify-team.yml index 4f06782a0..472cdb869 100644 --- a/.github/workflows/archive/bot-gfi-notify-team.yml +++ b/.github/workflows/archive/bot-gfi-notify-team.yml @@ -11,7 +11,7 @@ permissions: jobs: gfi_notify_team: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md if: > github.event.issue.labels && contains( diff --git a/.github/workflows/archive/bot-mentor-assignment.yml b/.github/workflows/archive/bot-mentor-assignment.yml index cf45b81f3..c495c45ca 100644 --- a/.github/workflows/archive/bot-mentor-assignment.yml +++ b/.github/workflows/archive/bot-mentor-assignment.yml @@ -13,7 +13,7 @@ permissions: jobs: assign-mentor: if: contains(github.event.issue.labels.*.name, 'Good First Issue') - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 diff --git a/.github/workflows/archive/bot-merge-conflict.yml b/.github/workflows/archive/bot-merge-conflict.yml index 32be65009..5452a8ccd 100644 --- a/.github/workflows/archive/bot-merge-conflict.yml +++ b/.github/workflows/archive/bot-merge-conflict.yml @@ -9,7 +9,7 @@ on: workflow_dispatch: inputs: dry_run: - description: 'Run in dry-run mode (no comments or status updates)' + description: "Run in dry-run mode (no comments or status updates)" type: boolean default: true @@ -25,7 +25,7 @@ concurrency: jobs: check-conflicts: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - name: Checkout code uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 @@ -47,4 +47,4 @@ jobs: const scriptPath = path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/bot-merge-conflict.js') console.log(`Loading script from: ${scriptPath}`) const script = require(scriptPath) - await script({github, context, core}) \ No newline at end of file + await script({github, context, core}) diff --git a/.github/workflows/archive/bot-next-issue-recommendation.yml b/.github/workflows/archive/bot-next-issue-recommendation.yml index d8ae657e2..bf509e820 100644 --- a/.github/workflows/archive/bot-next-issue-recommendation.yml +++ b/.github/workflows/archive/bot-next-issue-recommendation.yml @@ -15,9 +15,9 @@ concurrency: jobs: recommend-next-issue: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md if: github.event.pull_request.merged == true - + steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 @@ -26,7 +26,7 @@ jobs: - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - + - name: Recommend next issue uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # 8.0.0 with: diff --git a/.github/workflows/archive/bot-pr-draft-ready-reminder.yaml b/.github/workflows/archive/bot-pr-draft-ready-reminder.yaml index f3640de6b..a965907a0 100644 --- a/.github/workflows/archive/bot-pr-draft-ready-reminder.yaml +++ b/.github/workflows/archive/bot-pr-draft-ready-reminder.yaml @@ -12,7 +12,7 @@ permissions: jobs: draft-ready-reminder: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md concurrency: group: draft-ready-reminder-${{ github.event.pull_request.number }} cancel-in-progress: true @@ -32,5 +32,5 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const script = require('./.github/scripts/bot-pr-draft-ready-reminder.js'); - await script({ github, context }); \ No newline at end of file + const script = require('./.github/scripts/bot-pr-draft-ready-reminder.js'); + await script({ github, context }); diff --git a/.github/workflows/archive/bot-pr-linked-issue-not-assigned.yml b/.github/workflows/archive/bot-pr-linked-issue-not-assigned.yml index 37af8d0a2..f1d960429 100644 --- a/.github/workflows/archive/bot-pr-linked-issue-not-assigned.yml +++ b/.github/workflows/archive/bot-pr-linked-issue-not-assigned.yml @@ -9,11 +9,11 @@ on: workflow_dispatch: inputs: pr_number: - description: 'PR number to check' + description: "PR number to check" required: true type: number dry_run: - description: 'Dry run (only log, no comments)' + description: "Dry run (only log, no comments)" required: false type: boolean default: true @@ -25,7 +25,7 @@ permissions: jobs: check-linked-issue-assignment: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md concurrency: group: bot-pr-linked-issue-not-assigned-${{ github.event.pull_request.number || github.event.inputs.pr_number }} diff --git a/.github/workflows/archive/bot-pr-missing-linked-issue.yml b/.github/workflows/archive/bot-pr-missing-linked-issue.yml index 9bb5c32dc..f6bfc01b0 100644 --- a/.github/workflows/archive/bot-pr-missing-linked-issue.yml +++ b/.github/workflows/archive/bot-pr-missing-linked-issue.yml @@ -9,11 +9,11 @@ on: workflow_dispatch: inputs: pr_number: - description: 'PR number to check' + description: "PR number to check" required: true type: number dry_run: - description: 'Dry run (only log, no comments)' + description: "Dry run (only log, no comments)" required: false type: boolean default: true @@ -25,7 +25,7 @@ permissions: jobs: check-linked-issue: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md concurrency: group: bot-pr-missing-linked-issue-${{ github.event.pull_request.number || github.event.inputs.pr_number }} diff --git a/.github/workflows/archive/bot-verified-commits.yml b/.github/workflows/archive/bot-verified-commits.yml index 316a0e693..893196baa 100644 --- a/.github/workflows/archive/bot-verified-commits.yml +++ b/.github/workflows/archive/bot-verified-commits.yml @@ -34,7 +34,7 @@ concurrency: jobs: verify-commits: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md # ========================================================================= # CONFIGURATION - All customizable values are defined here as env vars diff --git a/.github/workflows/archive/pr-check-title.yml b/.github/workflows/archive/pr-check-title.yml index bbcf511cb..a20328746 100644 --- a/.github/workflows/archive/pr-check-title.yml +++ b/.github/workflows/archive/pr-check-title.yml @@ -1,4 +1,4 @@ -name: 'PR Formatting' +name: "PR Formatting" on: workflow_dispatch: pull_request_target: @@ -24,14 +24,14 @@ concurrency: jobs: title-check: name: Title Check - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md if: ${{ !github.event.pull_request.base.repo.fork }} permissions: checks: write statuses: write steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 with: egress-policy: audit diff --git a/.github/workflows/archived/bot-pr-auto-draft-on-changes.yml b/.github/workflows/archived/bot-pr-auto-draft-on-changes.yml index 619a3a5c3..2c806093d 100644 --- a/.github/workflows/archived/bot-pr-auto-draft-on-changes.yml +++ b/.github/workflows/archived/bot-pr-auto-draft-on-changes.yml @@ -15,7 +15,7 @@ concurrency: jobs: convert-to-draft: if: ${{ github.event.review.state == 'changes_requested' }} - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner (Audit all outbound calls) diff --git a/.github/workflows/bot-advanced-check.yml b/.github/workflows/bot-advanced-check.yml index 3feff4fd0..cd41a943b 100644 --- a/.github/workflows/bot-advanced-check.yml +++ b/.github/workflows/bot-advanced-check.yml @@ -25,10 +25,10 @@ jobs: ####################################### check-advanced-qualification: # steps are skipped, job completes successfully - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/bot-assignment-check.yml b/.github/workflows/bot-assignment-check.yml index c733911e9..13db23e17 100644 --- a/.github/workflows/bot-assignment-check.yml +++ b/.github/workflows/bot-assignment-check.yml @@ -12,7 +12,7 @@ jobs: runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/bot-beginner-assign-on-comment.yml b/.github/workflows/bot-beginner-assign-on-comment.yml index ceeacf9e5..2ea2ef679 100644 --- a/.github/workflows/bot-beginner-assign-on-comment.yml +++ b/.github/workflows/bot-beginner-assign-on-comment.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Harden runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/bot-coderabbit-plan-trigger.yml b/.github/workflows/bot-coderabbit-plan-trigger.yml index de7e59c8e..bf88d0b39 100644 --- a/.github/workflows/bot-coderabbit-plan-trigger.yml +++ b/.github/workflows/bot-coderabbit-plan-trigger.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/bot-community-calls.yml b/.github/workflows/bot-community-calls.yml index 4735252fc..831f4f1f3 100644 --- a/.github/workflows/bot-community-calls.yml +++ b/.github/workflows/bot-community-calls.yml @@ -27,7 +27,7 @@ jobs: runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc #2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d #2.16.1 with: egress-policy: audit diff --git a/.github/workflows/bot-gfi-assign-on-comment.yml b/.github/workflows/bot-gfi-assign-on-comment.yml index 5ecdb46bf..8342676ba 100644 --- a/.github/workflows/bot-gfi-assign-on-comment.yml +++ b/.github/workflows/bot-gfi-assign-on-comment.yml @@ -14,7 +14,7 @@ jobs: # Only run on issue comments (not PR comments) if: github.event.issue.pull_request == null - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md # Prevent assignment-limit races across different issues by processing # one GFI assignment workflow at a time for this repository. @@ -24,7 +24,7 @@ jobs: steps: - name: Harden runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/bot-gfi-candidate-notification.yaml b/.github/workflows/bot-gfi-candidate-notification.yaml index 68e272df4..f393f1a0a 100644 --- a/.github/workflows/bot-gfi-candidate-notification.yaml +++ b/.github/workflows/bot-gfi-candidate-notification.yaml @@ -21,7 +21,7 @@ jobs: steps: - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d with: egress-policy: audit diff --git a/.github/workflows/bot-inactivity-unassign.yml b/.github/workflows/bot-inactivity-unassign.yml index 4456f47df..88a122332 100644 --- a/.github/workflows/bot-inactivity-unassign.yml +++ b/.github/workflows/bot-inactivity-unassign.yml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d with: egress-policy: audit diff --git a/.github/workflows/bot-intermediate-assignment.yml b/.github/workflows/bot-intermediate-assignment.yml index 43ce00086..339c6054a 100644 --- a/.github/workflows/bot-intermediate-assignment.yml +++ b/.github/workflows/bot-intermediate-assignment.yml @@ -23,10 +23,10 @@ jobs: group: intermediate-guard-${{ github.event.issue.number || github.run_id }} cancel-in-progress: false if: contains(github.event.issue.labels.*.name, 'intermediate') - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/bot-issue-reminder-no-pr.yml b/.github/workflows/bot-issue-reminder-no-pr.yml index 939f72b40..c9e32b45d 100644 --- a/.github/workflows/bot-issue-reminder-no-pr.yml +++ b/.github/workflows/bot-issue-reminder-no-pr.yml @@ -18,11 +18,11 @@ permissions: jobs: reminder: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d with: egress-policy: audit diff --git a/.github/workflows/bot-linked-issue-enforcer.yml b/.github/workflows/bot-linked-issue-enforcer.yml index 33f8b8b72..fbac3a03e 100644 --- a/.github/workflows/bot-linked-issue-enforcer.yml +++ b/.github/workflows/bot-linked-issue-enforcer.yml @@ -18,13 +18,13 @@ permissions: jobs: pr-linked-issue-checker: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md env: DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }} steps: - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit - name: Checkout repository diff --git a/.github/workflows/bot-office-hours.yml b/.github/workflows/bot-office-hours.yml index 9724cd5bc..3c5f621aa 100644 --- a/.github/workflows/bot-office-hours.yml +++ b/.github/workflows/bot-office-hours.yml @@ -20,13 +20,13 @@ permissions: jobs: office-hour-reminder: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md concurrency: group: office-hour-reminder cancel-in-progress: false steps: - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc #2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d #2.16.1 with: egress-policy: audit diff --git a/.github/workflows/bot-p0-issues-notify-team.yml b/.github/workflows/bot-p0-issues-notify-team.yml index a4b202f76..4a753cc20 100644 --- a/.github/workflows/bot-p0-issues-notify-team.yml +++ b/.github/workflows/bot-p0-issues-notify-team.yml @@ -1,18 +1,17 @@ # This workflow warns the team about P0 issues immediately when they are created. name: P0 Issue Alert on: - issues: - types: + types: - labeled permissions: issues: write contents: read -jobs: +jobs: p0_notify_team: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md # Only run for issues labeled with 'p0' (case-insensitive) if: > (github.event_name == 'issues' && ( @@ -20,8 +19,8 @@ jobs: ) steps: - - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + - name: Harden the runner + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit @@ -29,10 +28,10 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1 - name: Notify team of P0 issues - env: + env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0 with: script: | const script = require('./.github/scripts/bot-p0-issues-notify-team.js'); - await script({ github, context}); \ No newline at end of file + await script({ github, context}); diff --git a/.github/workflows/bot-pr-draft-explainer.yaml b/.github/workflows/bot-pr-draft-explainer.yaml index 705ac1cc4..57bbc8a46 100644 --- a/.github/workflows/bot-pr-draft-explainer.yaml +++ b/.github/workflows/bot-pr-draft-explainer.yaml @@ -23,13 +23,13 @@ permissions: jobs: pr-draft-explainer: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md concurrency: group: pr-draft-explainer-${{ github.event.pull_request.number || github.event.inputs.pr_number }} cancel-in-progress: true steps: - name: Harden the runner - uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit @@ -46,5 +46,5 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0 with: script: | - const script = require('./.github/scripts/bot-pr-draft-explainer.js'); - await script({ github, context }); \ No newline at end of file + const script = require('./.github/scripts/bot-pr-draft-explainer.js'); + await script({ github, context }); diff --git a/.github/workflows/bot-pr-inactivity-reminder.yml b/.github/workflows/bot-pr-inactivity-reminder.yml index 79dde45fe..11f288b72 100644 --- a/.github/workflows/bot-pr-inactivity-reminder.yml +++ b/.github/workflows/bot-pr-inactivity-reminder.yml @@ -4,33 +4,32 @@ name: PR Inactivity Reminder Bot on: schedule: - - cron: '0 11 * * *' + - cron: "0 11 * * *" workflow_dispatch: inputs: dry_run: description: 'If true, do not post comments (dry run). Accepts "true" or "false". Default true for manual runs.' required: false - default: 'true' + default: "true" permissions: pull-requests: write issues: write contents: read - jobs: remind_inactive_prs: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md env: DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }} steps: - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - name: Remind authors of inactive PRs env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -39,4 +38,4 @@ jobs: with: script: | const script = require('./.github/scripts/pr_inactivity_reminder.js') - await script({ github, context }); \ No newline at end of file + await script({ github, context }); diff --git a/.github/workflows/bot-workflows.yml b/.github/workflows/bot-workflows.yml index 48eee26bf..2de71473a 100644 --- a/.github/workflows/bot-workflows.yml +++ b/.github/workflows/bot-workflows.yml @@ -1,5 +1,5 @@ name: PythonBot - Workflow Failure Notifier -on: +on: workflow_dispatch: workflow_run: workflows: @@ -18,10 +18,10 @@ concurrency: jobs: notify-pr: if: ${{ github.event.workflow_run.conclusion == 'failure' }} - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit @@ -34,11 +34,11 @@ jobs: HEAD_BRANCH=$(gh run view ${{ github.event.workflow_run.id }} \ --repo ${{ github.repository }} \ --json headBranch --jq '.headBranch') - + # Find the PR number for this branch (only open PRs) PR_NUMBER=$(gh pr list --repo ${{ github.repository }} --state open --head "$HEAD_BRANCH" --json number --jq '.[0].number') echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_ENV - + - name: Comment on PR if: env.PR_NUMBER != '' env: diff --git a/.github/workflows/cron-check-broken-links.yml b/.github/workflows/cron-check-broken-links.yml index 321d1ca11..95977a5ea 100644 --- a/.github/workflows/cron-check-broken-links.yml +++ b/.github/workflows/cron-check-broken-links.yml @@ -2,11 +2,11 @@ name: Cron โ€“ Check Broken Markdown Links on: schedule: - - cron: '0 0 1 * *' + - cron: "0 0 1 * *" workflow_dispatch: inputs: dry_run: - description: 'Run without creating issues? (true/false)' + description: "Run without creating issues? (true/false)" required: true default: true type: boolean @@ -17,10 +17,10 @@ permissions: jobs: cron-check-broken-links: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - name: Harden runner (audit outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit @@ -30,7 +30,7 @@ jobs: - name: Check Markdown links (Lychee) id: lychee uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2.8.0 - continue-on-error: true + continue-on-error: true with: args: --verbose --no-progress './**/*.md' fail: true diff --git a/.github/workflows/cron-update-spam-list.yml b/.github/workflows/cron-update-spam-list.yml index 638b2197e..6e2e89490 100644 --- a/.github/workflows/cron-update-spam-list.yml +++ b/.github/workflows/cron-update-spam-list.yml @@ -20,10 +20,10 @@ env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} jobs: update-spam-list: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - name: Harden runner (audit outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.github/workflows/deps-check.yml b/.github/workflows/deps-check.yml index 5a9b0af66..aa90413b5 100644 --- a/.github/workflows/deps-check.yml +++ b/.github/workflows/deps-check.yml @@ -14,7 +14,7 @@ permissions: jobs: min-deps: - runs-on: ubuntu-latest + runs-on: ${{ (github.repository_owner != 'hiero-ledger' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork)) && 'ubuntu-latest' || 'hl-sdk-py-lin-md' }} strategy: fail-fast: false @@ -23,7 +23,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit @@ -37,7 +37,7 @@ jobs: cache: "pip" - name: Install uv - uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - name: Create virtual environment run: uv venv diff --git a/.github/workflows/pr-check-broken-links.yml b/.github/workflows/pr-check-broken-links.yml index 173ef74e2..33b6f1241 100644 --- a/.github/workflows/pr-check-broken-links.yml +++ b/.github/workflows/pr-check-broken-links.yml @@ -3,18 +3,18 @@ name: PR Check โ€“ Broken Markdown Links on: pull_request: paths: - - '**/*.md' + - "**/*.md" permissions: contents: read jobs: pr-check-broken-links: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - name: Harden runner (audit outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit @@ -24,6 +24,6 @@ jobs: - name: Check Markdown links uses: tcort/github-action-markdown-link-check@e7c7a18363c842693fadde5d41a3bd3573a7a225 # v1.1.2 with: - use-quiet-mode: 'yes' - check-modified-files-only: 'yes' - base-branch: 'main' + use-quiet-mode: "yes" + check-modified-files-only: "yes" + base-branch: "main" diff --git a/.github/workflows/pr-check-changelog.yml b/.github/workflows/pr-check-changelog.yml index 83177726a..10ea4d79f 100644 --- a/.github/workflows/pr-check-changelog.yml +++ b/.github/workflows/pr-check-changelog.yml @@ -1,4 +1,4 @@ -name: 'PR Changelog Check' +name: "PR Changelog Check" on: pull_request: @@ -9,20 +9,20 @@ permissions: jobs: changelog-check: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 - name: Harden the runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit - name: Run local changelog check env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | chmod +x .github/scripts/pr-check-changelog.sh - bash .github/scripts/pr-check-changelog.sh \ No newline at end of file + bash .github/scripts/pr-check-changelog.sh diff --git a/.github/workflows/pr-check-codecov.yml b/.github/workflows/pr-check-codecov.yml index 767a1ee5d..e9375a53e 100644 --- a/.github/workflows/pr-check-codecov.yml +++ b/.github/workflows/pr-check-codecov.yml @@ -1,54 +1,54 @@ name: Code Coverage on: - push: - branches: [main] - pull_request: + push: + branches: [main] + pull_request: permissions: contents: read jobs: - coverage: - runs-on: ubuntu-latest - - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 - with: - egress-policy: audit - - - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - with: - fetch-depth: 2 - - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: "3.11" - - - name: Install uv - uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0 - - - name: Install dependencies - run: | - uv sync --all-extras --dev - - - name: Generate Proto Files - run: uv run python generate_proto.py - - - name: Run unit tests and generate coverage report - run: | - uv run pytest tests/unit \ - --cov=src \ - --cov-report=xml \ - --cov-report=term-missing - - - name: Upload coverage to Codecov - if: github.repository_owner == 'hiero-ledger' - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: coverage.xml - fail_ci_if_error: true + coverage: + runs-on: hl-sdk-py-lin-md + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 2 + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.11" + + - name: Install uv + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + + - name: Install dependencies + run: | + uv sync --all-extras --dev --frozen + + - name: Generate Proto Files + run: uv run python generate_proto.py + + - name: Run unit tests and generate coverage report + run: | + uv run pytest tests/unit \ + --cov=src \ + --cov-report=xml \ + --cov-report=term-missing + + - name: Upload coverage to Codecov + if: github.repository_owner == 'hiero-ledger' + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage.xml + fail_ci_if_error: true diff --git a/.github/workflows/pr-check-examples.yml b/.github/workflows/pr-check-examples.yml index a043c6187..867982f8e 100644 --- a/.github/workflows/pr-check-examples.yml +++ b/.github/workflows/pr-check-examples.yml @@ -11,10 +11,10 @@ on: jobs: run-examples: - runs-on: ubuntu-latest + runs-on: ${{ (github.repository_owner != 'hiero-ledger' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork)) && 'ubuntu-latest' || 'hl-sdk-py-lin-md' }} steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit @@ -24,10 +24,10 @@ jobs: fetch-depth: 0 - name: Install uv - uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 - name: Install dependencies - run: uv sync --all-extras + run: uv sync --all-extras --frozen - name: Generate Proto Files run: uv run python generate_proto.py diff --git a/.github/workflows/pr-check-test.yml b/.github/workflows/pr-check-secondary-unit-integration-test.yml similarity index 80% rename from .github/workflows/pr-check-test.yml rename to .github/workflows/pr-check-secondary-unit-integration-test.yml index 9a4b69a97..77a73e253 100644 --- a/.github/workflows/pr-check-test.yml +++ b/.github/workflows/pr-check-secondary-unit-integration-test.yml @@ -1,5 +1,4 @@ -name: Hiero Solo Integration & Unit Tests - +name: Secondary PR Check - Hiero Solo Integration & Unit Tests on: push: branches: @@ -9,25 +8,31 @@ on: - "docs/**" - "examples/**" - "tck/**" + - "tests/fuzz/**" - ".github/**" - - "!.github/workflows/pr-check-test.yml" + - "!.github/workflows/pr-check-secondary-unit-integration-test.yml" pull_request: paths-ignore: - "**/*.md" - "docs/**" - "examples/**" - "tck/**" + - "tests/fuzz/**" - ".github/**" - - "!.github/workflows/pr-check-test.yml" + - "!.github/workflows/pr-check-secondary-unit-integration-test.yml" workflow_dispatch: {} permissions: contents: read +concurrency: + group: pr-check-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: unit-tests: name: Unit Tests (Python ${{ matrix.python-version }}) - runs-on: ubuntu-latest + runs-on: ${{ (github.repository_owner != 'hiero-ledger' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork)) && 'ubuntu-latest' || 'hl-sdk-py-lin-md' }} timeout-minutes: 10 strategy: fail-fast: false @@ -36,7 +41,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit @@ -49,12 +54,12 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install uv - uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true - name: Install dependencies - run: uv sync --all-extras --dev + run: uv sync --all-extras --dev --frozen - name: Generate Proto Files run: uv run generate_proto.py @@ -87,7 +92,7 @@ jobs: integration-tests: name: Integration Tests (Python ${{ matrix.python-version }}) - runs-on: ubuntu-latest + runs-on: ${{ (github.repository_owner != 'hiero-ledger' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork)) && 'ubuntu-latest' || 'hl-sdk-py-lin-md' }} timeout-minutes: 20 needs: - unit-tests @@ -99,7 +104,7 @@ jobs: steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit @@ -112,12 +117,12 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install uv - uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 with: enable-cache: true - name: Install dependencies - run: uv sync --all-extras --dev + run: uv sync --all-extras --dev --frozen - name: Generate Proto Files run: uv run generate_proto.py @@ -162,7 +167,7 @@ jobs: test-summary: name: Test Results Summary - runs-on: ubuntu-latest + runs-on: ${{ (github.repository_owner != 'hiero-ledger' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork)) && 'ubuntu-latest' || 'hl-sdk-py-lin-md' }} needs: - unit-tests - integration-tests diff --git a/.github/workflows/pr-check-test-files.yml b/.github/workflows/pr-check-test-files.yml index d90dc06ce..c8de51b8d 100644 --- a/.github/workflows/pr-check-test-files.yml +++ b/.github/workflows/pr-check-test-files.yml @@ -14,17 +14,18 @@ concurrency: jobs: check-test-files: - runs-on: ubuntu-latest + # No fork check needed: push-only workflow; fork pushes have different repository_owner + runs-on: ${{ (github.repository_owner != 'hiero-ledger') && 'ubuntu-latest' || 'hl-sdk-py-lin-md' }} steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit - name: Checkout repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - + - name: Set up Hiero SDK Upstream run: | git remote set-url origin https://github.com/hiero-ledger/hiero-sdk-python.git diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6c24aa547..2ebfb84d1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,14 +3,14 @@ name: Publish to PyPI on: push: tags: - - 'v*.*.*' + - "v*.*.*" permissions: contents: read jobs: build-and-publish: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md environment: name: pypi url: https://pypi.org/p/hiero-sdk-python @@ -18,7 +18,7 @@ jobs: id-token: write steps: - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit @@ -27,12 +27,16 @@ jobs: - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.14' - name: Upgrade pip - run: pip install --upgrade pip + run: pip install "pip==26.0.1" + # Dependency versions are pinned to ensure reproducible builds. + # These should be periodically reviewed and updated for ensuring smooth execution. - name: Install build, pdm-backend, and grpcio-tools - run: pip install build pdm-backend "grpcio-tools==1.68.1" + run: pip install build==1.4.2 pdm-backend==2.4.8 grpcio-tools==1.76.0 - name: Generate Protobuf run: python generate_proto.py diff --git a/.github/workflows/release-pr-coderabbit-gate.yml b/.github/workflows/release-pr-coderabbit-gate.yml new file mode 100644 index 000000000..eb096a0d2 --- /dev/null +++ b/.github/workflows/release-pr-coderabbit-gate.yml @@ -0,0 +1,42 @@ +name: CodeRabbit Release Gate Comment + +on: + pull_request: + types: [opened, reopened, synchronize, edited] + +permissions: + contents: read + pull-requests: write + +concurrency: + group: coderabbit-release-gate-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + coderabbit-release-gate: + runs-on: ubuntu-latest + # Only run for release PRs /title check as initial filter + if: | + github.event.pull_request && + (github.event.pull_request.author_association == 'MEMBER' || + github.event.pull_request.author_association == 'OWNER') && + (startsWith(github.event.pull_request.title, 'chore: release v') || + startsWith(github.event.pull_request.title, 'release v')) + + steps: + - name: Harden the runner + uses: step-security/harden-runner@e3f713f2d8f53843e71c69a996d56f51aa9adfb9 # v2.14.1 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1 + + - name: Post CodeRabbit release-gate prompt comment + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd #v8.0.0 + with: + script: | + const script = require('./.github/scripts/release-pr-coderabbit-gate.js'); + await script({ github, context}); \ No newline at end of file diff --git a/.github/workflows/sync-issue-labels-add.yml b/.github/workflows/sync-issue-labels-add.yml new file mode 100644 index 000000000..fc345431b --- /dev/null +++ b/.github/workflows/sync-issue-labels-add.yml @@ -0,0 +1,128 @@ +name: Add Linked Issue Labels to PR + +on: + workflow_dispatch: + inputs: + upstream_run_id: + description: "Upstream compute workflow run ID" + required: true + type: string + pr_number: + description: "Pull request number" + required: true + type: string + dry_run: + description: "Dry run flag" + required: false + type: string + default: "true" + is_fork_pr: + description: "Fork PR flag" + required: false + type: string + default: "false" +defaults: + run: + shell: bash +permissions: + actions: read + issues: write + +jobs: + add-labels: + concurrency: + group: sync-issue-labels-pr-${{ github.event.inputs.pr_number }} + cancel-in-progress: true + runs-on: ubuntu-latest + steps: + - name: Harden the runner + uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 + with: + egress-policy: audit + + - name: Download labels artifact + id: download + continue-on-error: true + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + with: + name: pr-labels-${{ github.event.inputs.pr_number }} + path: artifacts + run-id: ${{ github.event.inputs.upstream_run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Read labels payload + id: read + env: + INPUT_PR_NUMBER: ${{ github.event.inputs.pr_number }} + INPUT_IS_FORK_PR: ${{ github.event.inputs.is_fork_pr }} + INPUT_DRY_RUN: ${{ github.event.inputs.dry_run }} + run: | + labels_file="artifacts/labels.json" + if [ ! -f "$labels_file" ]; then + echo "::error::Labels artifact not found. Cross-workflow handoff is broken." + echo "labels=[]" >> "$GITHUB_OUTPUT" + echo "labels_count=0" >> "$GITHUB_OUTPUT" + echo "labels_multiline=" >> "$GITHUB_OUTPUT" + echo "pr_number=$INPUT_PR_NUMBER" >> "$GITHUB_OUTPUT" + echo "is_fork_pr=$INPUT_IS_FORK_PR" >> "$GITHUB_OUTPUT" + echo "dry_run=$INPUT_DRY_RUN" >> "$GITHUB_OUTPUT" + echo "source_event=workflow_dispatch" >> "$GITHUB_OUTPUT" + exit 1 + fi + labels=$(jq -c '.labels // []' "$labels_file") + pr_number=$(jq -r '.pr_number // 0' "$labels_file") + is_fork_pr=$(jq -r '.is_fork_pr // false' "$labels_file") + dry_run=$(jq -r '.dry_run // "true"' "$labels_file") + source_event=$(jq -r '.source_event // ""' "$labels_file") + labels_multiline=$(jq -r '.labels // [] | .[]' "$labels_file") + labels_count=$(echo "$labels" | jq 'length') + echo "labels=$labels" >> "$GITHUB_OUTPUT" + echo "labels_count=$labels_count" >> "$GITHUB_OUTPUT" + { + echo "labels_multiline<> "$GITHUB_OUTPUT" + echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT" + echo "is_fork_pr=$is_fork_pr" >> "$GITHUB_OUTPUT" + echo "dry_run=$dry_run" >> "$GITHUB_OUTPUT" + echo "source_event=$source_event" >> "$GITHUB_OUTPUT" + + - name: Validate labels payload + id: validate + run: | + if [ "$PR_NUMBER" = "0" ] || [ "$(echo "$LABELS" | jq -r '. | length')" = "0" ]; then + echo "Invalid payload: pr_number=$PR_NUMBER or labels empty. Skipping label addition." + echo "valid_payload=false" >> "$GITHUB_OUTPUT" + else + echo "valid_payload=true" >> "$GITHUB_OUTPUT" + fi + env: + PR_NUMBER: ${{ steps.read.outputs.pr_number }} + LABELS: ${{ steps.read.outputs.labels }} + + - name: Determine if labels should be applied + id: should_apply + run: | + if [ "${{ steps.read.outputs.is_fork_pr }}" = "true" ]; then + echo "apply=false" >> "$GITHUB_OUTPUT" + echo "reason=fork PR" >> "$GITHUB_OUTPUT" + elif [ "${{ steps.validate.outputs.valid_payload }}" != "true" ]; then + echo "apply=false" >> "$GITHUB_OUTPUT" + echo "reason=invalid payload" >> "$GITHUB_OUTPUT" + elif [ "${{ steps.read.outputs.source_event }}" = "workflow_dispatch" ] && [ "${{ steps.read.outputs.dry_run }}" = "true" ]; then + echo "apply=false" >> "$GITHUB_OUTPUT" + echo "reason=dry run" >> "$GITHUB_OUTPUT" + else + echo "apply=true" >> "$GITHUB_OUTPUT" + echo "reason=" >> "$GITHUB_OUTPUT" + fi + + - name: Add labels to PR + if: ${{ steps.should_apply.outputs.apply == 'true' }} + uses: actions-ecosystem/action-add-labels@1a9c3715c0037e96b97bb38cb4c4b56a1f1d4871 # main + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + labels: ${{ steps.read.outputs.labels_multiline }} + number: ${{ steps.read.outputs.pr_number }} + diff --git a/.github/workflows/sync-issue-labels-compute.yml b/.github/workflows/sync-issue-labels-compute.yml new file mode 100644 index 000000000..e3db19955 --- /dev/null +++ b/.github/workflows/sync-issue-labels-compute.yml @@ -0,0 +1,207 @@ +name: Compute Linked Issue Labels + +on: + pull_request: + types: [opened, edited, reopened, synchronize, ready_for_review] + workflow_dispatch: + inputs: + pr_number: + description: "PR number to sync labels for" + required: true + type: number + dry-run-enabled: + description: "Dry run (log only, do not apply labels)" + required: false + type: boolean + default: true + +permissions: + actions: write + pull-requests: read + issues: read + contents: read + +jobs: + compute-labels: + concurrency: + group: sync-issue-labels-compute-pr-${{ github.event.pull_request.number || github.event.inputs.pr_number }} + cancel-in-progress: true + runs-on: ubuntu-latest + outputs: + pr_number: ${{ steps.compute.outputs.pr_number }} + dry_run: ${{ steps.compute.outputs.dry_run }} + is_fork_pr: ${{ steps.compute.outputs.is_fork_pr }} + steps: + - name: Harden the runner + uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 + with: + egress-policy: audit + + - name: Compute linked issue labels + id: compute + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} + DRY_RUN: 'true' + REQUESTED_DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && format('{0}', github.event.inputs['dry-run-enabled']) || 'true' }} + IS_FORK_PR: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork || 'false' }} + MAX_LINKED_ISSUES: '20' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + result-encoding: json + script: | + const MAX_LINKED_ISSUES = Number(process.env.MAX_LINKED_ISSUES || "20"); + + function extractLabels(labelData) { + const result = []; + for (const item of labelData) { + const name = typeof item === "string" ? item : item && item.name; + if (name && name.trim()) result.push(name.trim()); + } + return result; + } + + function extractLinkedIssueNumbers(prBody, owner, repo) { + const numbers = new Set(); + const closingRefRegex = /(?:fix|fixes|fixed|close|closes|closed|resolve|resolves|resolved)\s+(?:([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+))?#(\d+)\b/gi; + const lines = String(prBody || "").split(/\r?\n/); + for (const line of lines) { + let m; + while ((m = closingRefRegex.exec(line)) !== null) { + const refOwner = (m[1] || "").toLowerCase(); + const refRepo = (m[2] || "").toLowerCase(); + if (refOwner && refRepo && (refOwner !== owner.toLowerCase() || refRepo !== repo.toLowerCase())) continue; + numbers.add(Number(m[3])); + } + } + const all = Array.from(numbers); + if (all.length > MAX_LINKED_ISSUES) { + console.log(`[sync] Limiting linked issue refs from ${all.length} to ${MAX_LINKED_ISSUES}.`); + } + return all.slice(0, MAX_LINKED_ISSUES); + } + + const prNumber = Number(process.env.PR_NUMBER); + if (!prNumber) { + core.setOutput('has_labels', 'false'); + core.setOutput('labels', '[]'); + core.setOutput('pr_number', ''); + core.setOutput('dry_run', 'true'); + core.setOutput('is_fork_pr', String(process.env.IS_FORK_PR || 'false')); + core.setOutput('source_event', context.eventName); + return; + } + + const { data: prData } = await github.rest.pulls.get({ + owner: context.repo.owner, repo: context.repo.repo, pull_number: prNumber + }); + + const prAuthor = (prData.user && prData.user.login) || ""; + if (/\[bot\]$/i.test(prAuthor) || /dependabot/i.test(prAuthor)) { + console.log(`[sync] Skipping bot-authored PR from ${prAuthor}.`); + core.setOutput('has_labels', 'false'); + core.setOutput('labels', '[]'); + core.setOutput('pr_number', String(prNumber)); + core.setOutput('dry_run', 'true'); + core.setOutput('is_fork_pr', String(process.env.IS_FORK_PR || 'false')); + core.setOutput('source_event', context.eventName); + return; + } + + const linkedIssues = extractLinkedIssueNumbers(prData.body || "", context.repo.owner, context.repo.repo); + if (!linkedIssues.length) { + console.log("[sync] No linked issue references found in PR body."); + core.setOutput('has_labels', 'false'); + core.setOutput('labels', '[]'); + core.setOutput('pr_number', String(prNumber)); + core.setOutput('dry_run', 'true'); + core.setOutput('is_fork_pr', String(process.env.IS_FORK_PR || 'false')); + core.setOutput('source_event', context.eventName); + return; + } + + console.log(`[sync] Linked issues: ${linkedIssues.map(n => '#' + n).join(', ')}`); + + const allLabels = []; + for (const num of linkedIssues) { + try { + const { data } = await github.rest.issues.get({ + owner: context.repo.owner, repo: context.repo.repo, issue_number: num + }); + if (data.pull_request) { console.log(`[sync] Skipping #${num}: is a PR reference.`); continue; } + const labels = extractLabels(data.labels || []); + console.log(`[sync] Issue #${num} labels: ${labels.length ? labels.join(', ') : '(none)'}`); + allLabels.push(...labels); + } catch (err) { + if (err && err.status === 404) { console.log(`[sync] Issue #${num} not found. Skipping.`); continue; } + throw err; + } + } + + const existing = extractLabels(prData.labels || []); + const existingSet = new Set(existing); + const deduped = Array.from(new Set(allLabels)); + const toAdd = deduped.filter(l => !existingSet.has(l)); + + console.log(`[sync] Existing: ${existing.length ? existing.join(', ') : '(none)'}`); + console.log(`[sync] To add: ${toAdd.length ? toAdd.join(', ') : '(none)'}`); + + const labels = toAdd; + const hasLabels = labels.length > 0; + core.setOutput('has_labels', String(hasLabels)); + core.setOutput('labels', JSON.stringify(labels)); + core.setOutput('pr_number', String(prNumber)); + core.setOutput('dry_run', String(process.env.REQUESTED_DRY_RUN || 'true')); + core.setOutput('is_fork_pr', String(process.env.IS_FORK_PR || 'false')); + core.setOutput('source_event', context.eventName); + return { has_labels: hasLabels, labels, pr_number: String(prNumber), dry_run: process.env.REQUESTED_DRY_RUN, is_fork_pr: process.env.IS_FORK_PR, source_event: context.eventName }; + + - name: Write labels artifact payload + env: + LABELS_JSON: ${{ steps.compute.outputs.labels }} + PR_NUMBER: ${{ steps.compute.outputs.pr_number }} + IS_FORK_PR: ${{ steps.compute.outputs.is_fork_pr }} + DRY_RUN: ${{ steps.compute.outputs.dry_run }} + SOURCE_EVENT: ${{ steps.compute.outputs.source_event }} + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const fs = require('fs'); + const parsed = JSON.parse(process.env.LABELS_JSON || '[]'); + const payload = { + pr_number: Number(process.env.PR_NUMBER || 0), + labels: Array.isArray(parsed) ? parsed : [], + is_fork_pr: /^true$/i.test(process.env.IS_FORK_PR || ''), + dry_run: /^true$/i.test(process.env.DRY_RUN || ''), + source_event: process.env.SOURCE_EVENT || '', + }; + fs.writeFileSync('labels.json', JSON.stringify(payload)); + console.log(`Wrote labels artifact payload for PR #${payload.pr_number}: ${payload.labels.length} labels`); + + - name: Upload labels artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: pr-labels-${{ steps.compute.outputs.pr_number }} + path: labels.json + retention-days: 1 + + dispatch-add: + needs: compute-labels + if: ${{ needs.compute-labels.outputs.is_fork_pr != 'true' }} + runs-on: ubuntu-latest + steps: + - name: Trigger add workflow + uses: step-security/workflow-dispatch@acca1a315af3bf7f33dd116d3cb405cb83f5cbdc # v1.2.8 + with: + workflow: .github/workflows/sync-issue-labels-add.yml + repo: ${{ github.repository }} + ref: main + token: ${{ secrets.GH_ACCESS_TOKEN }} + inputs: >- + { + "upstream_run_id":"${{ github.run_id }}", + "pr_number":"${{ needs.compute-labels.outputs.pr_number }}", + "dry_run":"${{ needs.compute-labels.outputs.dry_run }}", + "is_fork_pr":"${{ needs.compute-labels.outputs.is_fork_pr }}" + } + diff --git a/.github/workflows/tck-test.yml b/.github/workflows/tck-test.yml new file mode 100644 index 000000000..7e9a627a6 --- /dev/null +++ b/.github/workflows/tck-test.yml @@ -0,0 +1,52 @@ +name: TCK Unit Test + +on: + push: + branches: + - "main" + paths: + - "tck/**" + - "tests/tck/**" + pull_request: + paths: + - "tck/**" + - "tests/tck/**" + workflow_dispatch: {} + +permissions: + contents: read + + +jobs: + tck-unit-test: + name: Tck Unit Test + runs-on: hl-sdk-py-lin-md + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.14" + + - name: Install uv + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --all-extras --dev --frozen + + - name: Generate Proto Files + run: uv run generate_proto.py + + - name: Run unit tests + run: | + uv run pytest tests/tck -v diff --git a/.github/workflows/unassign-on-comment.yml b/.github/workflows/unassign-on-comment.yml index a8bceb18c..f99761e90 100644 --- a/.github/workflows/unassign-on-comment.yml +++ b/.github/workflows/unassign-on-comment.yml @@ -14,7 +14,7 @@ jobs: # Only run on issue comments (not PR comments) if: github.event.issue.pull_request == null - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md concurrency: group: unassign-${{ github.event.issue.number }} @@ -22,7 +22,7 @@ jobs: steps: - name: Harden runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d with: egress-policy: audit diff --git a/.github/workflows/working-on-comment.yml b/.github/workflows/working-on-comment.yml index 27d88bf3a..85c121145 100644 --- a/.github/workflows/working-on-comment.yml +++ b/.github/workflows/working-on-comment.yml @@ -17,7 +17,7 @@ permissions: jobs: working-command: if: github.event_name == 'workflow_dispatch' || (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/working')) - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md permissions: issues: write contents: read @@ -26,7 +26,7 @@ jobs: cancel-in-progress: false steps: - name: Harden Runner - uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1 + uses: step-security/harden-runner@fe104658747b27e96e4f7e80cd0a94068e53901d # v2.16.1 with: egress-policy: audit diff --git a/.gitignore b/.gitignore index c85306d25..f1f06ce9d 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,9 @@ src/hiero_sdk_python/hapi # Pytest .pytest_cache +# Hypothesis state +.hypothesis/ + # Lock files -uv.lock pdm.lock -pubkey.asc \ No newline at end of file +pubkey.asc diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ca8fe607..6d1a2108e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,19 +7,62 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. ## [Unreleased] ### Src +- Exposed all missing `TransactionRecord` protobuf fields `consensusTimestamp`, `scheduleRef`, `assessed_custom_fees`, `automatic_token_associations`, `parent_consensus_timestamp`, `alias`, `ethereum_hash`, `paid_staking_rewards`, `evm_address`, `contractCreateResult` with proper `None` handling, PRNG oneof handling with unset values return `None` instead of default values 0 / b"" (#1636) +- Enforce reproducible and consistent builds using `uv.lock` and frozen installs across workflows. (#2057) + +### Tests +- Refactor `mock_server` setup for network level TLS handling and added thread safety + + +### Examples + +- Add the missing `setup_client()` docstring in `examples/tokens/token_dissociate_transaction.py` for consistency with the other example functions. (#2058) + +### Docs + + +### .github +- chore: pin pip packages to exact versions in publish.yml to improve supply chain security and reproducibility (#2056) +- chore: update GitHub Actions runners from ubuntu-latest to hl-sdk-py-lin-md (#2021) +- Refactored the Advanced Issue Template to V2 with stricter prerequisites and a focus on architectural design (#2016). +- Refactored the Advanced Issue Template to ensure PR-level quality checklists do not block maintainers during issue creation (#2036) +- chore: add concurrency to unit and integration tests (#2071) +- Add automated label sync workflow to propagate labels from linked issues to pull requests (#1716) +- chore: update spam list (#2035) +- Add CodeRabbit release gate workflow and prompt for PR audits `release-pr-prompt.md`, `release-pr-coderabbit-gate.yml` and `release-pr-coderabbit-gate.js` + +## [0.2.3] - 2026-03-26 + +### Added +- Add `__repr__` method to `TokenId` class for cleaner debugging output (#1653) + +### Src +- Updated `AccountUpdateTransaction.set_key()` to accept generic `Key` objects (including `KeyList` and threshold keys), rather than strictly requiring a `PublicKey`. +- Fix the TransactionGetReceiptQuery to raise ReceiptStatusError for the non-retryable and non success receipt status +- Refactor `AccountInfo` to use the existing `StakingInfo` wrapper class instead of flattened staking fields. Access is now via `info.staking_info.staked_account_id`, `info.staking_info.staked_node_id`, and `info.staking_info.decline_reward`. The old flat accessors (`info.staked_account_id`, `info.staked_node_id`, `info.decline_staking_reward`) are still available as deprecated properties and will emit a `DeprecationWarning`. (#1366) +- Added abstract `Key` supper class to handle various proto Keys. ### Examples -- edit ### Tests +- Added TCK endpoint for the createAccount method +- Renamed `delegate_contract_id.py` to `delegate_contract_id_test.py` (#2004) +- Fix Flaky tests for `mock_server` by enforcing non-tls port and adding a mock_tls certificate +- Implement basic fuzz testing [#1872](https://github.com/hiero-ledger/hiero-sdk-python/issues/1872) ### Docs +- Add Chocolatey as a prerequisite in the Windows setup guide (#1961) ### .github +- chore: update several ubuntu runners to hl-sdk-py-lin-md (#1480) +- Refactored intermediate issue template with quality standards, testing requirements, breaking change awareness, and protobuf verification guidance to reduce review burden and improve PR quality (#1892) - fix: prevent CodeRabbit from posting comments on closed issues(#1962) +- chore: update spam list #1988 +- chore: Update `bot-advanced-check.yml`, `bot-gfi-assign-on-comment.yml`, `bot-intermediate-assignment.yml`, `bot-linked-issue-enforcer.yml`, `unassign-on-comment.yml`, `working-on-comment.yml` workflow runner configuration +- Fix build failing in `publish.yml` @@ -37,7 +80,6 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - Updated `generated_proto.py` file to work with new proto version - fix: Ensure UTF-8 encoding when reading and writing proto files in `generate_proto.py` to prevent encoding issues on Windows (`#1963`) - ### Examples - Updated the `examples/consensus/topic_create_transaction_revenue_generating.py` example to use `Client.from_env()` for simpler client setup. (#1964) @@ -110,8 +152,6 @@ This changelog is based on [Keep a Changelog](https://keepachangelog.com/en/1.1. - docs: Clarify issues need to be assigned in template files. (#1884) - doc: Fix testnet link in README.md. (#1879) - - ### Tests - Format `tests/unit/endpoint_test.py` using black. (`#1792`) - Implement TCK JSON-RPC server with request handling and error management diff --git a/codecov.yml b/codecov.yml index 622d0b98b..34b3fa451 100644 --- a/codecov.yml +++ b/codecov.yml @@ -10,5 +10,8 @@ coverage: target: 92% threshold: 2% +ignore: +- "tck/**" + comment: layout: "reach, diff, flags" diff --git a/docs/github/04_workflow_documentation.md b/docs/github/04_workflow_documentation.md index 4687b7572..239137d99 100644 --- a/docs/github/04_workflow_documentation.md +++ b/docs/github/04_workflow_documentation.md @@ -63,7 +63,7 @@ permissions: jobs: gfi-assign: if: github.event.issue.pull_request == null - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md concurrency: group: gfi-assign-${{ github.event.issue.number }} @@ -100,7 +100,7 @@ permissions: jobs: changelog-check: - runs-on: ubuntu-latest + runs-on: hl-sdk-py-lin-md steps: - name: Checkout repository diff --git a/docs/sdk_developers/setup_windows.md b/docs/sdk_developers/setup_windows.md index c0f82273d..794fe854d 100644 --- a/docs/sdk_developers/setup_windows.md +++ b/docs/sdk_developers/setup_windows.md @@ -25,6 +25,7 @@ Before you begin, ensure you have the following installed on your system: 1. **Git for Windows**: [Download and install Git](https://gitforwindows.org/). 2. **Python 3.10+**: [Download and install Python](https://www.python.org/downloads/windows/). Ensure "Add Python to PATH" is checked during installation. 3. **GitHub Account**: You will need a GitHub account to fork the repository. +4. **Chocolatey**: For Windows 10 users, you must install Chocolatey. [Install Chocolatey](https://chocolatey.org/install). Make sure Chocolatey is added to PATH. --- diff --git a/examples/account/account_update_transaction.py b/examples/account/account_update_transaction.py index b68355767..57c06d113 100644 --- a/examples/account/account_update_transaction.py +++ b/examples/account/account_update_transaction.py @@ -8,34 +8,21 @@ python examples/account/account_update_transaction.py """ import datetime -import os import sys -from dotenv import load_dotenv - -from hiero_sdk_python import AccountId, Client, Duration, Hbar, Network, PrivateKey +from hiero_sdk_python import Client, Duration, Hbar, PrivateKey from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction from hiero_sdk_python.account.account_update_transaction import AccountUpdateTransaction from hiero_sdk_python.query.account_info_query import AccountInfoQuery from hiero_sdk_python.response_code import ResponseCode from hiero_sdk_python.timestamp import Timestamp -load_dotenv() - -network_name = os.getenv("NETWORK", "testnet").lower() - -def setup_client(): - """Initialize and set up the client with operator account.""" - network = Network(network_name) - print(f"Connecting to Hedera {network_name} network!") - client = Client(network) - - operator_id = AccountId.from_string(os.getenv("OPERATOR_ID", "")) - operator_key = PrivateKey.from_string(os.getenv("OPERATOR_KEY", "")) - client.set_operator(operator_id, operator_key) +def setup_client() -> Client: + """Setup Client.""" + client = Client.from_env() + print(f"Network: {client.network.network}") print(f"Client set up with operator id {client.operator_account_id}") - return client diff --git a/examples/account/account_update_transaction_with_keylist.py b/examples/account/account_update_transaction_with_keylist.py new file mode 100644 index 000000000..d63938214 --- /dev/null +++ b/examples/account/account_update_transaction_with_keylist.py @@ -0,0 +1,136 @@ +""" +Example demonstrating account update functionality with key lists (multi-signature threshold keys). + +run with: +uv run examples/account/account_update_transaction_with_keylist.py +python examples/account/account_update_transaction_with_keylist.py + +""" +import sys + +from hiero_sdk_python import Client, Hbar, PrivateKey +from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction +from hiero_sdk_python.account.account_update_transaction import AccountUpdateTransaction +from hiero_sdk_python.crypto.key_list import KeyList +from hiero_sdk_python.query.account_info_query import AccountInfoQuery +from hiero_sdk_python.response_code import ResponseCode + + +def setup_client() -> Client: + """Setup Client.""" + client = Client.from_env() + print(f"Network: {client.network.network}") + print(f"Client set up with operator id {client.operator_account_id}") + return client + + +def create_account(client): + """Create a test account.""" + account_private_key = PrivateKey.generate_ed25519() + account_public_key = account_private_key.public_key() + + receipt = ( + AccountCreateTransaction() + .set_key_without_alias(account_public_key) + .set_initial_balance(Hbar(1)) + .set_account_memo("Test account for update with keylist") + .freeze_with(client) + .sign(account_private_key) + .execute(client) + ) + + if receipt.status != ResponseCode.SUCCESS: + print( + f"Account creation failed with status: {ResponseCode(receipt.status).name}" + ) + sys.exit(1) + + account_id = receipt.account_id + print(f"\nAccount created with ID: {account_id}") + + return account_id, account_private_key + + +def query_account_info(client, account_id): + """Query and display account information.""" + info = AccountInfoQuery(account_id).execute(client) + + print(f"Account ID: {info.account_id}") + print(f"Account Balance: {info.balance}") + print(f"Account Memo: '{info.account_memo}'") + print(f"Public Key: {info.key}") + + +def account_update_with_keylist(): + """ + Demonstrates account update functionality using a threshold key list by: + + 1. Setting up client with operator account + 2. Creating a test account + 3. Updating the account to use a KeyList with threshold + 4. Proving that the new account key is active by signing an update + """ + client = setup_client() + + # Create a test account first + account_id, current_private_key = create_account(client) + + print("\nAccount info before update:") + # Query the account info + query_account_info(client, account_id) + + # Rotate from a single key to a threshold KeyList (2 of 2). + threshold_key_1 = PrivateKey.generate_ed25519() + threshold_key_2 = PrivateKey.generate_ed25519() + threshold_key = KeyList( + [threshold_key_1.public_key(), threshold_key_2.public_key()], threshold=2 + ) + + print("\nRotating account key to a 2-of-2 threshold KeyList...") + key_list_receipt = ( + AccountUpdateTransaction() + .set_account_id(account_id) + .set_key(threshold_key) + .freeze_with(client) + .sign(current_private_key) # Sign with current key + .sign(threshold_key_1) # First signature for threshold=2 + .sign(threshold_key_2) # Second signature for threshold=2 + .execute(client) + ) + + if key_list_receipt.status != ResponseCode.SUCCESS: + print( + f"KeyList rotation failed with status: {ResponseCode(key_list_receipt.status).name}" + ) + sys.exit(1) + + print("\nAccount info after KeyList update:") + query_account_info(client, account_id) + + # Prove the new account key is active by signing with both threshold keys. + print("\nProving the new account key by updating the memo using both threshold keys...") + memo_receipt = ( + AccountUpdateTransaction() + .set_account_id(account_id) + .set_account_memo("Updated account memo with threshold key") + .freeze_with(client) + .sign(threshold_key_1) + .sign(threshold_key_2) + .execute(client) + ) + + if memo_receipt.status != ResponseCode.SUCCESS: + print( + "Memo update with threshold key failed with status: " + f"{ResponseCode(memo_receipt.status).name}" + ) + sys.exit(1) + + print("\nAccount info after memo update:") + query_account_info(client, account_id) + + print("\nThreshold KeyList rotation and follow-up update succeeded.") + + +if __name__ == "__main__": + account_update_with_keylist() \ No newline at end of file diff --git a/examples/query/transaction_get_receipt_query_validate_status.py b/examples/query/transaction_get_receipt_query_validate_status.py new file mode 100644 index 000000000..5291bb43a --- /dev/null +++ b/examples/query/transaction_get_receipt_query_validate_status.py @@ -0,0 +1,88 @@ +""" +Example demonstrating the validate_status feature for Transaction Receipts. + +uv run examples/query/transaction_get_receipt_query_validate_status.py +python examples/query/transaction_get_receipt_query_validate_status.py +""" +import sys + +from hiero_sdk_python import ( + AccountDeleteTransaction, + AccountId, + Client, + ResponseCode, + TransactionGetReceiptQuery, + ReceiptStatusError, +) + +def setup_client(): + """Initialize the Hiero client from environment variables.""" + try: + client = Client.from_env() + print(f"Client set up for {client.network.network}...") + return client + except ValueError as e: + print(f"Error setting up client: {e}") + sys.exit(1) + +def submit_failing_transaction(client): + """Submit a transaction designed to fail to demonstrate receipt handling.""" + tx = ( + AccountDeleteTransaction() + .set_account_id(AccountId(0, 0, 9999999)) + .set_transfer_account_id(client.operator_account_id) + .freeze_with(client) + ) + + # wait_for_receipt=False allows us to query the receipt manually later + response = tx.execute(client, wait_for_receipt=False) + + print(f"Transaction submitted: {response.transaction_id}") + + return response.transaction_id + +def run_manual_validation(client, transaction_id): + """Demonstrate manual status checking (the default behavior).""" + print("\n--- Option A: Manual Validation (Default) ---") + query = ( + TransactionGetReceiptQuery() + .set_transaction_id(transaction_id) + .set_validate_status(False) + ) + + print("Executing query with validate_status=False") + receipt = query.execute(client) + status_name = ResponseCode(receipt.status).name + print(f"Query returned receipt with status: {status_name}") + + +def run_automatic_validation(client, transaction_id): + """Demonstrate automatic validation using ReceiptStatusError.""" + print("\n--- Option B: Automatic Validation ---") + query = ( + TransactionGetReceiptQuery() + .set_transaction_id(transaction_id) + .set_validate_status(True) + ) + + try: + print("Executing query with validate_status=True") + query.execute(client) + except ReceiptStatusError as e: + status_name = ResponseCode(e.status).name + print(f"Query raises expected exception: {status_name}") + +def main(): + client = setup_client() + + # Get a transaction ID to query + tx_id = submit_failing_transaction(client) + + # Default receipt query tx + run_manual_validation(client, tx_id) + + # Receipt query with validate_status + run_automatic_validation(client, tx_id) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/tokens/token_dissociate_transaction.py b/examples/tokens/token_dissociate_transaction.py index 871c8cffb..a08803d3a 100644 --- a/examples/tokens/token_dissociate_transaction.py +++ b/examples/tokens/token_dissociate_transaction.py @@ -19,6 +19,7 @@ def setup_client(): + """Initialize the Hiero client from environment variables and print connection info.""" client = Client.from_env() print(f"Network: {client.network.network}") print(f"Client set up with operator id {client.operator_account_id}") diff --git a/examples/transaction/transaction_record.py b/examples/transaction/transaction_record.py new file mode 100644 index 000000000..c98f790f5 --- /dev/null +++ b/examples/transaction/transaction_record.py @@ -0,0 +1,160 @@ +""" +Simple example: Exploring TransactionRecord fields. + +This example creates a mock TransactionRecord with sample data and prints all fields +in a readable format. + +No network or client needed โ€” just run the file! + +Run: + python examples/transaction/transaction_record.py +""" + +from collections import defaultdict + +from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.contract.contract_function_result import ContractFunctionResult +from hiero_sdk_python.contract.contract_id import ContractId +from hiero_sdk_python.hapi.services import transaction_receipt_pb2 +from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.schedule.schedule_id import ScheduleId +from hiero_sdk_python.timestamp import Timestamp +from hiero_sdk_python.tokens.assessed_custom_fee import AssessedCustomFee +from hiero_sdk_python.tokens.token_association import TokenAssociation +from hiero_sdk_python.tokens.token_id import TokenId +from hiero_sdk_python.transaction.transaction_id import TransactionId +from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt +from hiero_sdk_python.transaction.transaction_record import TransactionRecord + + +def create_mock_record(): + """Create a mock TransactionRecord with sample values for all fields.""" + # Basic setup + tx_id = TransactionId.from_string("0.0.1234@1698765432.000000000") + + receipt_proto = transaction_receipt_pb2.TransactionReceipt() + receipt_proto.status = ResponseCode.SUCCESS.value + + receipt = TransactionReceipt( + receipt_proto=receipt_proto, + transaction_id=tx_id + ) + + ts = Timestamp(seconds=1698765432, nanos=123456789) + sched = ScheduleId(0, 0, 9999) + + record = TransactionRecord( + transaction_id=tx_id, + transaction_hash=b'\x01\x02\x03\x04' * 12, + transaction_memo="Hello from example!", + transaction_fee=50000, + receipt=receipt, + token_transfers=defaultdict(lambda: defaultdict(int)), + nft_transfers=defaultdict(list), + transfers=defaultdict(int), + new_pending_airdrops=[], + prng_number=42, + prng_bytes=None, # mutually exclusive with prng_number + duplicates=[], + children=[], + consensus_timestamp=ts, + schedule_ref=sched, + assessed_custom_fees=[ + AssessedCustomFee( + amount=1000000, + fee_collector_account_id=AccountId(shard=0, realm=0, num=98), + effective_payer_account_ids=[AccountId(shard=0, realm=0, num=100)] + ) + ], + automatic_token_associations=[ + TokenAssociation( + token_id=TokenId(shard=0, realm=0, num=5678), + account_id=AccountId(shard=0, realm=0, num=1234) + ) + ], + parent_consensus_timestamp=ts, + alias=b'\x12\x34\x56\x78\x9a\xbc', + ethereum_hash=b'\xab' * 32, + paid_staking_rewards=[ + (AccountId(shard=0, realm=0, num=456), 500000), + (AccountId(shard=0, realm=0, num=789), 250000) + ], + evm_address=b'\xef' * 20, + contract_create_result=ContractFunctionResult( + contract_id=ContractId(shard=0, realm=0, contract=1000), + contract_call_result=b"Contract created successfully!" + ), + ) + + record.transfers[AccountId(shard=0, realm=0, num=100)] = -10000 + record.transfers[AccountId(shard=0, realm=0, num=200)] = 10000 + + return record + +def _print_basic_fields(record): + print("Basic:") + print(f" Transaction ID: {record.transaction_id}") + print(f" Memo: {record.transaction_memo}") + print(f" Fee: {record.transaction_fee} tinybars") + print(f" Hash: {record.transaction_hash.hex() if record.transaction_hash else 'None'}") + if record.receipt: + try: + status = ResponseCode(record.receipt.status).name + except ValueError: + status = str(record.receipt.status) + else: + status = "None" + print(f" Receipt Status: {status}") + print(f" PRNG Number: {record.prng_number}") + print(f" PRNG Bytes (hex): {record.prng_bytes.hex() if record.prng_bytes else 'None'}") + + +def _print_transfer_fields(record): + print(f" HBAR Transfers: {dict(record.transfers) if record.transfers else 'None'}") + print(f" Token Transfers: {dict(record.token_transfers) if record.token_transfers else 'None'}") + print(f" NFT Transfers: { {k: len(v) for k, v in record.nft_transfers.items()} if record.nft_transfers else 'None'}") + print(f" Pending Airdrops: {len(record.new_pending_airdrops)}") + + +def _print_new_fields(record): + print(f" Consensus Timestamp: {record.consensus_timestamp}") + print(f" Parent Consensus Timestamp: {record.parent_consensus_timestamp}") + print(f" Schedule Ref: {record.schedule_ref}") + print(f" Assessed Custom Fees ({len(record.assessed_custom_fees)}):") + for fee in record.assessed_custom_fees: + token = fee.token_id if fee.token_id else "HBAR" + payers = ", ".join(str(p) for p in fee.effective_payer_account_ids) if fee.effective_payer_account_ids else "N/A" + print(f" - {fee.amount} {token} โ†’ Collector: {fee.fee_collector_account_id}, Payers: {payers}") + print(f" Automatic Token Associations ({len(record.automatic_token_associations)}):") + for assoc in record.automatic_token_associations: + print(f" - Token {assoc.token_id} โ†’ Account {assoc.account_id}") + print(f" Alias (hex): {record.alias.hex() if record.alias else 'None'}") + print(f" Ethereum Hash (hex): {record.ethereum_hash.hex() if record.ethereum_hash else 'None'}") + print(f" Paid Staking Rewards ({len(record.paid_staking_rewards)}):") + for account, amount in record.paid_staking_rewards: + print(f" - {account}: {amount} tinybars") + print(f" EVM Address (hex): {record.evm_address.hex() if record.evm_address else 'None'}") + if record.contract_create_result: + print(f" Contract Create Result: {record.contract_create_result.contract_id}") + print(f" Result bytes (first 32): {record.contract_create_result.contract_call_result[:32].hex() if record.contract_create_result.contract_call_result else 'None'}...") + else: + print(" Contract Create Result: None") + + +def print_all_fields(record): + """Print all fields of TransactionRecord in a simple, readable way.""" + print("=== TransactionRecord Example ===") + _print_basic_fields(record) + _print_transfer_fields(record) + _print_new_fields(record) + +def main(): + """Run the TransactionRecord example.""" + print("Creating mock TransactionRecord...\n") + record = create_mock_record() + print_all_fields(record) + + +if __name__ == "__main__": + main() + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 8f0981172..0a2d72446 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,7 @@ dev = [ "grpcio-tools>=1.76.0,<2", "pytest>=8.3.4,<10", "pytest-cov>=7.0.0,<8", + "hypothesis>=6.137.2" ] lint = [ @@ -60,7 +61,12 @@ lint = [ default-groups = ["dev", "lint"] [build-system] -requires = ["pdm-backend", "setuptools", "setuptools_scm", "grpcio-tools"] +requires = [ + "pdm-backend>=2.4.8,<3", + "setuptools>=82,<83", + "setuptools_scm>=7,<9", + "grpcio-tools>=1.80.0,<2" +] build-backend = "pdm.backend" [tool.setuptools_scm] diff --git a/pytest.ini b/pytest.ini index ff9c1ad03..0fb601499 100644 --- a/pytest.ini +++ b/pytest.ini @@ -4,4 +4,5 @@ pythonpath = src markers = integration: mark a test as an integration test. - unit: mark a test as a unit test. \ No newline at end of file + unit: mark a test as a unit test. + fuzz: mark a test as fuzz test. diff --git a/src/hiero_sdk_python/__init__.py b/src/hiero_sdk_python/__init__.py index 10519418d..513044aed 100644 --- a/src/hiero_sdk_python/__init__.py +++ b/src/hiero_sdk_python/__init__.py @@ -52,6 +52,7 @@ from .tokens.token_pause_transaction import TokenPauseTransaction from .tokens.token_airdrop_claim import TokenClaimAirdropTransaction from .tokens.assessed_custom_fee import AssessedCustomFee +from .tokens.token_association import TokenAssociation # Transaction from .transaction.transaction import Transaction @@ -150,6 +151,10 @@ from .system.freeze_transaction import FreezeTransaction from .system.freeze_type import FreezeType +# Errors +from .exceptions import ReceiptStatusError +from .exceptions import PrecheckError + __all__ = [ # Client "Client", @@ -189,6 +194,7 @@ "TokenBurnTransaction", "TokenGrantKycTransaction", "TokenRelationship", + "TokenAssociation", "TokenUpdateTransaction", "TokenAirdropTransaction", "TokenCancelAirdropTransaction", @@ -298,4 +304,8 @@ # System "FreezeTransaction", "FreezeType", + + # Errors + "ReceiptStatusError", + "PrecheckError" ] diff --git a/src/hiero_sdk_python/account/account_create_transaction.py b/src/hiero_sdk_python/account/account_create_transaction.py index 43c33f4f9..02e8b0cb4 100644 --- a/src/hiero_sdk_python/account/account_create_transaction.py +++ b/src/hiero_sdk_python/account/account_create_transaction.py @@ -2,23 +2,23 @@ AccountCreateTransaction class. """ +import ctypes from typing import Optional, Union import warnings from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.channels import _Channel from hiero_sdk_python.crypto.evm_address import EvmAddress -from hiero_sdk_python.crypto.public_key import PublicKey +from hiero_sdk_python.crypto.key import Key from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.Duration import Duration from hiero_sdk_python.executable import _Method -from hiero_sdk_python.hapi.services import crypto_create_pb2, duration_pb2, transaction_pb2, basic_types_pb2 +from hiero_sdk_python.hapi.services import crypto_create_pb2, duration_pb2, transaction_pb2 from hiero_sdk_python.hapi.services.schedulable_transaction_body_pb2 import ( SchedulableTransactionBody, ) from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.transaction.transaction import Transaction -from hiero_sdk_python.utils.key_utils import Key, key_to_proto AUTO_RENEW_PERIOD = Duration(7890000) # around 90 days in seconds DEFAULT_TRANSACTION_FEE = Hbar(3).to_tinybars() # 3 Hbars @@ -258,12 +258,12 @@ def set_staked_account_id( """ self._require_not_frozen() if isinstance(account_id, str): - self.staked_account_id = AccountId.from_string(account_id) - elif isinstance(account_id, AccountId): - self.staked_account_id = account_id - else: + account_id = AccountId.from_string(account_id) + elif not isinstance(account_id, AccountId): raise TypeError("account_id must be of type str or AccountId") - + + self.staked_account_id = account_id + self.staked_node_id = None return self def set_staked_node_id(self, node_id: int) -> "AccountCreateTransaction": @@ -281,6 +281,7 @@ def set_staked_node_id(self, node_id: int) -> "AccountCreateTransaction": raise TypeError("node_id must be of type int") self.staked_node_id = node_id + self.staked_account_id = None return self def set_decline_staking_reward( @@ -315,8 +316,6 @@ def _build_proto_body(self) -> crypto_create_pb2.CryptoCreateTransactionBody: ValueError: If required fields are missing. TypeError: If initial_balance is an invalid type. """ - if not self.key: - raise ValueError("Key must be set before building the transaction.") if isinstance(self.initial_balance, Hbar): initial_balance_tinybars = self.initial_balance.to_tinybars() @@ -324,12 +323,15 @@ def _build_proto_body(self) -> crypto_create_pb2.CryptoCreateTransactionBody: initial_balance_tinybars = self.initial_balance else: raise TypeError("initial_balance must be Hbar or int (tinybars).") - - proto_key = key_to_proto(self.key) + + # Check for overflow + if initial_balance_tinybars >= (2**64): + raise OverflowError(f"Value {initial_balance_tinybars} exceeds 64-bit unsigned integer limit.") proto_body = crypto_create_pb2.CryptoCreateTransactionBody( - key=proto_key, - initialBalance=initial_balance_tinybars, + key=self.key.to_proto_key() if self.key is not None else None, + # triggers an INVALID_INITIAL_BALANCE pre-check error instead of a local error. + initialBalance=ctypes.c_uint64(initial_balance_tinybars).value, receiverSigRequired=self.receiver_signature_required, autoRenewPeriod=duration_pb2.Duration(seconds=self.auto_renew_period.seconds), memo=self.account_memo, @@ -338,9 +340,12 @@ def _build_proto_body(self) -> crypto_create_pb2.CryptoCreateTransactionBody: decline_reward=self.decline_staking_reward ) - if self.staked_account_id: + if self.staked_node_id is not None and self.staked_account_id is not None: + raise ValueError("Specify either staked_node_id or staked_account_id, not both.") + + if self.staked_account_id is not None: proto_body.staked_account_id.CopyFrom(self.staked_account_id._to_proto()) - elif self.staked_node_id: + elif self.staked_node_id is not None: proto_body.staked_node_id = self.staked_node_id return proto_body diff --git a/src/hiero_sdk_python/account/account_info.py b/src/hiero_sdk_python/account/account_info.py index e6844a9f8..477da0351 100644 --- a/src/hiero_sdk_python/account/account_info.py +++ b/src/hiero_sdk_python/account/account_info.py @@ -3,13 +3,14 @@ AccountInfo class. """ +import warnings from dataclasses import dataclass, field from typing import Optional from hiero_sdk_python.account.account_id import AccountId -from hiero_sdk_python.crypto.public_key import PublicKey +from hiero_sdk_python.staking_info import StakingInfo +from hiero_sdk_python.crypto.key import Key from hiero_sdk_python.Duration import Duration -from hiero_sdk_python.hapi.services.basic_types_pb2 import StakingInfo from hiero_sdk_python.hapi.services.crypto_get_info_pb2 import CryptoGetInfoResponse from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.timestamp import Timestamp @@ -26,7 +27,7 @@ class AccountInfo: contract_account_id (Optional[str]): The contract account ID. is_deleted (Optional[bool]): Whether the account has been deleted. proxy_received (Optional[Hbar]): The total number of tinybars proxy staked to this account. - key (Optional[PublicKey]): The key for this account. + key (Optional[Key]): The key for this account. balance (Optional[Hbar]): The current balance of account in hbar. receiver_signature_required (Optional[bool]): If true, this account's key must sign any transaction depositing into this account. @@ -38,16 +39,14 @@ class AccountInfo: associated with this account. account_memo (Optional[str]): The memo associated with this account. owned_nfts (Optional[int]): The number of NFTs owned by this account. - staked_account_id (Optional[AccountId]): The account to which this account is staked. - staked_node_id (Optional[int]): The node to which this account is staked. - decline_staking_reward (bool): Whether this account declines receiving staking rewards. + staking_info (Optional[StakingInfo]): The staking information for this account. """ account_id: Optional[AccountId] = None contract_account_id: Optional[str] = None is_deleted: Optional[bool] = None proxy_received: Optional[Hbar] = None - key: Optional[PublicKey] = None + key: Optional[Key] = None balance: Optional[Hbar] = None receiver_signature_required: Optional[bool] = None expiration_time: Optional[Timestamp] = None @@ -56,9 +55,7 @@ class AccountInfo: account_memo: Optional[str] = None owned_nfts: Optional[int] = None max_automatic_token_associations: Optional[int] = None - staked_account_id: Optional[AccountId] = None - staked_node_id: Optional[int] = None - decline_staking_reward: Optional[bool] = None + staking_info: Optional[StakingInfo] = None @classmethod def _from_proto(cls, proto: CryptoGetInfoResponse.AccountInfo) -> "AccountInfo": @@ -84,7 +81,7 @@ def _from_proto(cls, proto: CryptoGetInfoResponse.AccountInfo) -> "AccountInfo": contract_account_id=proto.contractAccountID, is_deleted=proto.deleted, proxy_received=Hbar.from_tinybars(proto.proxyReceived), - key=PublicKey._from_proto(proto.key) if proto.key else None, + key=Key.from_proto_key(proto.key) if proto.key else None, balance=Hbar.from_tinybars(proto.balance), receiver_signature_required=proto.receiverSigRequired, expiration_time=( @@ -100,21 +97,12 @@ def _from_proto(cls, proto: CryptoGetInfoResponse.AccountInfo) -> "AccountInfo": account_memo=proto.memo, owned_nfts=proto.ownedNfts, max_automatic_token_associations=proto.max_automatic_token_associations, + staking_info=( + StakingInfo._from_proto(proto.staking_info) + if proto.HasField('staking_info') else None + ), ) - staking_info = proto.staking_info if proto.HasField('staking_info') else None - - if staking_info: - account_info.staked_account_id = ( - AccountId._from_proto(staking_info.staked_account_id) - if staking_info.HasField('staked_account_id') else None - ) - account_info.staked_node_id = ( - staking_info.staked_node_id - if staking_info.HasField('staked_node_id') else None - ) - account_info.decline_staking_reward = staking_info.decline_reward - return account_info def _to_proto(self) -> CryptoGetInfoResponse.AccountInfo: @@ -131,7 +119,7 @@ def _to_proto(self) -> CryptoGetInfoResponse.AccountInfo: CryptoGetInfoResponse.AccountInfo: The protobuf message representation of this `AccountInfo` object. """ - return CryptoGetInfoResponse.AccountInfo( + proto = CryptoGetInfoResponse.AccountInfo( accountID=self.account_id._to_proto() if self.account_id else None, contractAccountID=self.contract_account_id, deleted=self.is_deleted, @@ -147,12 +135,10 @@ def _to_proto(self) -> CryptoGetInfoResponse.AccountInfo: memo=self.account_memo, ownedNfts=self.owned_nfts, max_automatic_token_associations=self.max_automatic_token_associations, - staking_info=StakingInfo( - staked_account_id=self.staked_account_id._to_proto() if self.staked_account_id else None, - staked_node_id=self.staked_node_id if self.staked_node_id else None, - decline_reward=self.decline_staking_reward - ), ) + if self.staking_info is not None: + proto.staking_info.CopyFrom(self.staking_info._to_proto()) + return proto def __str__(self) -> str: """Returns a user-friendly string representation of the AccountInfo.""" @@ -166,8 +152,7 @@ def __str__(self) -> str: (self.account_memo, "Memo"), (self.owned_nfts, "Owned NFTs"), (self.max_automatic_token_associations, "Max Automatic Token Associations"), - (self.staked_account_id, "Staked Account ID"), - (self.staked_node_id, "Staked Node ID"), + (self.staking_info, "Staking Info"), (self.proxy_received, "Proxy Received"), (self.expiration_time, "Expiration Time"), (self.auto_renew_period, "Auto Renew Period"), @@ -182,9 +167,6 @@ def __str__(self) -> str: if self.receiver_signature_required is not None: lines.append(f"Receiver Signature Required: {self.receiver_signature_required}") - - if self.decline_staking_reward is not None: - lines.append(f"Decline Staking Reward: {self.decline_staking_reward}") if self.token_relationships: lines.append(f"Token Relationships: {len(self.token_relationships)}") @@ -193,16 +175,158 @@ def __str__(self) -> str: def __repr__(self) -> str: """Returns a string representation of the AccountInfo object for debugging.""" - return ( - f"AccountInfo(" - f"account_id={self.account_id!r}, " - f"contract_account_id={self.contract_account_id!r}, " - f"is_deleted={self.is_deleted!r}, " - f"balance={self.balance!r}, " - f"receiver_signature_required={self.receiver_signature_required!r}, " - f"owned_nfts={self.owned_nfts!r}, " - f"account_memo={self.account_memo!r}, " - f"staked_node_id={self.staked_node_id!r}, " - f"staked_account_id={self.staked_account_id!r}" - f")" - ) \ No newline at end of file + parts = [ + f"account_id={self.account_id!r}", + f"contract_account_id={self.contract_account_id!r}", + f"is_deleted={self.is_deleted!r}", + f"balance={self.balance!r}", + f"receiver_signature_required={self.receiver_signature_required!r}", + f"owned_nfts={self.owned_nfts!r}", + f"account_memo={self.account_memo!r}", + ] + if self.staking_info is not None: + parts.append(f"staking_info={self.staking_info!r}") + return f"AccountInfo({', '.join(parts)})" + + @property + def staked_account_id(self): + """Deprecated: use staking_info.staked_account_id instead.""" + warnings.warn( + "AccountInfo.staked_account_id is deprecated, " + "use AccountInfo.staking_info.staked_account_id instead", + DeprecationWarning, + stacklevel=2, + ) + return self.staking_info.staked_account_id if self.staking_info else None + + @staked_account_id.setter + def staked_account_id(self, value): + """Deprecated setter: use staking_info.staked_account_id instead.""" + warnings.warn( + "AccountInfo.staked_account_id setter is deprecated, " + "use AccountInfo.staking_info instead", + DeprecationWarning, + stacklevel=2, + ) + if self.staking_info is None: + object.__setattr__(self, 'staking_info', StakingInfo(staked_account_id=value)) + else: + # Reconstruct StakingInfo with updated field, clearing staked_node_id oneof conflict + object.__setattr__(self, 'staking_info', StakingInfo( + pending_reward=self.staking_info.pending_reward, + staked_to_me=self.staking_info.staked_to_me, + stake_period_start=self.staking_info.stake_period_start, + staked_account_id=value, + staked_node_id=None, # Clear oneof conflict + decline_reward=self.staking_info.decline_reward, + )) + + @property + def staked_node_id(self): + """Deprecated: use staking_info.staked_node_id instead.""" + warnings.warn( + "AccountInfo.staked_node_id is deprecated, " + "use AccountInfo.staking_info.staked_node_id instead", + DeprecationWarning, + stacklevel=2, + ) + return self.staking_info.staked_node_id if self.staking_info else None + + @staked_node_id.setter + def staked_node_id(self, value): + """Deprecated setter: use staking_info.staked_node_id instead.""" + warnings.warn( + "AccountInfo.staked_node_id setter is deprecated, " + "use AccountInfo.staking_info instead", + DeprecationWarning, + stacklevel=2, + ) + if self.staking_info is None: + object.__setattr__(self, 'staking_info', StakingInfo(staked_node_id=value)) + else: + # Reconstruct StakingInfo with updated field, clearing staked_account_id oneof conflict + object.__setattr__(self, 'staking_info', StakingInfo( + pending_reward=self.staking_info.pending_reward, + staked_to_me=self.staking_info.staked_to_me, + stake_period_start=self.staking_info.stake_period_start, + staked_account_id=None, # Clear oneof conflict + staked_node_id=value, + decline_reward=self.staking_info.decline_reward, + )) + + @property + def decline_staking_reward(self): + """Deprecated: use staking_info.decline_reward instead.""" + warnings.warn( + "AccountInfo.decline_staking_reward is deprecated, " + "use AccountInfo.staking_info.decline_reward instead", + DeprecationWarning, + stacklevel=2, + ) + return self.staking_info.decline_reward if self.staking_info else None + + @decline_staking_reward.setter + def decline_staking_reward(self, value): + """Deprecated setter: use staking_info.decline_reward instead.""" + warnings.warn( + "AccountInfo.decline_staking_reward setter is deprecated, " + "use AccountInfo.staking_info instead", + DeprecationWarning, + stacklevel=2, + ) + if self.staking_info is None: + object.__setattr__(self, 'staking_info', StakingInfo(decline_reward=value)) + else: + # Reconstruct StakingInfo with updated field + object.__setattr__(self, 'staking_info', StakingInfo( + pending_reward=self.staking_info.pending_reward, + staked_to_me=self.staking_info.staked_to_me, + stake_period_start=self.staking_info.stake_period_start, + staked_account_id=self.staking_info.staked_account_id, + staked_node_id=self.staking_info.staked_node_id, + decline_reward=value, + )) + + +# --------------------------------------------------------------------------- +# Backwards-compatible constructor: accept legacy staking kwargs +# --------------------------------------------------------------------------- +_ACCOUNT_INFO_INIT_SENTINEL = object() +_orig_account_info_init = AccountInfo.__init__ + + +def _wrapped_account_info_init( + self, + *args, + staked_account_id=_ACCOUNT_INFO_INIT_SENTINEL, + staked_node_id=_ACCOUNT_INFO_INIT_SENTINEL, + decline_staking_reward=_ACCOUNT_INFO_INIT_SENTINEL, + **kwargs, +): + _orig_account_info_init(self, *args, **kwargs) + _legacy = [ + k for k, v in [ + ("staked_account_id", staked_account_id), + ("staked_node_id", staked_node_id), + ("decline_staking_reward", decline_staking_reward), + ] + if v is not _ACCOUNT_INFO_INIT_SENTINEL + ] + if _legacy: + warnings.warn( + f"Passing {', '.join(_legacy)} to AccountInfo() is deprecated; " + "use staking_info=StakingInfo(...) instead.", + DeprecationWarning, + stacklevel=2, + ) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + if staked_account_id is not _ACCOUNT_INFO_INIT_SENTINEL: + self.staked_account_id = staked_account_id + if staked_node_id is not _ACCOUNT_INFO_INIT_SENTINEL: + self.staked_node_id = staked_node_id + if decline_staking_reward is not _ACCOUNT_INFO_INIT_SENTINEL: + self.decline_staking_reward = decline_staking_reward + + +AccountInfo.__init__ = _wrapped_account_info_init \ No newline at end of file diff --git a/src/hiero_sdk_python/account/account_update_transaction.py b/src/hiero_sdk_python/account/account_update_transaction.py index 27719679f..acca7e3bb 100644 --- a/src/hiero_sdk_python/account/account_update_transaction.py +++ b/src/hiero_sdk_python/account/account_update_transaction.py @@ -10,7 +10,7 @@ from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.channels import _Channel -from hiero_sdk_python.crypto.public_key import PublicKey +from hiero_sdk_python.crypto.key import Key from hiero_sdk_python.Duration import Duration from hiero_sdk_python.executable import _Method from hiero_sdk_python.hapi.services.crypto_update_pb2 import CryptoUpdateTransactionBody @@ -30,7 +30,7 @@ class AccountUpdateParams: Attributes: account_id (Optional[AccountId]): The account ID to update. - key (Optional[PublicKey]): The new key for the account. + key (Optional[Key]): The new key for the account. auto_renew_period (Duration): The new auto-renew period. account_memo (Optional[str]): The new memo for the account. receiver_signature_required (Optional[bool]): Whether receiver signature is required. @@ -46,7 +46,7 @@ class AccountUpdateParams: """ account_id: Optional[AccountId] = None - key: Optional[PublicKey] = None + key: Optional[Key] = None auto_renew_period: Duration = AUTO_RENEW_PERIOD account_memo: Optional[str] = None receiver_signature_required: Optional[bool] = None @@ -101,12 +101,12 @@ def set_account_id(self, account_id: Optional[AccountId]) -> "AccountUpdateTrans self.account_id = account_id return self - def set_key(self, key: Optional[PublicKey]) -> "AccountUpdateTransaction": + def set_key(self, key: Optional[Key]) -> "AccountUpdateTransaction": """ - Sets the new account key (public key) for key rotation. + Sets the new account key (Optional[Key]) for key rotation. Args: - key (Optional[PublicKey]): The new public key for the account. + key (Optional[Key]): The new key for the account. Returns: AccountUpdateTransaction: This transaction instance. @@ -307,7 +307,7 @@ def _build_proto_body(self): proto_body = CryptoUpdateTransactionBody( accountIDToUpdate=self.account_id._to_proto(), - key=self.key._to_proto() if self.key else None, + key=self.key.to_proto_key() if self.key else None, memo=StringValue(value=self.account_memo) if self.account_memo is not None else None, autoRenewPeriod=( self.auto_renew_period._to_proto() if self.auto_renew_period else None diff --git a/src/hiero_sdk_python/consensus/topic_message_submit_transaction.py b/src/hiero_sdk_python/consensus/topic_message_submit_transaction.py index 7757c799b..48ae989e9 100644 --- a/src/hiero_sdk_python/consensus/topic_message_submit_transaction.py +++ b/src/hiero_sdk_python/consensus/topic_message_submit_transaction.py @@ -291,6 +291,7 @@ def execute( client: "Client", timeout: int | float | None = None, wait_for_receipt: Literal[True] = True, + validate_status: bool = False ) -> "TransactionReceipt": ... @@ -300,6 +301,7 @@ def execute( client: "Client", timeout: int | float | None = None, wait_for_receipt: Literal[False] = False, + validate_status: bool = False ) -> "TransactionResponse": ... @@ -307,7 +309,8 @@ def execute( self, client: "Client", timeout: int | float | None = None, - wait_for_receipt: bool = True + wait_for_receipt: bool = True, + validate_status: bool = False ) -> TransactionReceipt | TransactionResponse: """ Executes the topic message submit transaction. @@ -319,13 +322,14 @@ def execute( timeout (int | float | None, optional): The total execution timeout (in seconds) for this execution. wait_for_receipt (bool, optional): Whether to wait for consensus and return the receipt. If False, the method returns a TransactionResponse immediately after submission. + validate_status: (bool): Whether the query should automatically validate the transaction status (default False). Returns: TransactionReceipt: If wait_for_receipt is True (default) TransactionResponse: If wait_for_receipt is False """ # Return the first response as the JS SDK does - return self.execute_all(client, timeout, wait_for_receipt)[0] + return self.execute_all(client, timeout, wait_for_receipt, validate_status)[0] @overload def execute_all( @@ -333,6 +337,7 @@ def execute_all( client: "Client", timeout: int | float | None = None, wait_for_receipt: Literal[True] = True, + validate_status: bool = False ) -> List["TransactionReceipt"]: ... @@ -342,6 +347,7 @@ def execute_all( client: "Client", timeout: int | float | None = None, wait_for_receipt: Literal[False] = False, + validate_status: bool = False ) -> List["TransactionResponse"]: ... @@ -349,7 +355,8 @@ def execute_all( self, client: "Client", timeout: int | float | None = None, - wait_for_receipt: bool = True + wait_for_receipt: bool = True, + validate_status: bool = False ) -> List["TransactionReceipt"] | List["TransactionResponse"]: """ Executes the topic message submit transaction. @@ -361,6 +368,7 @@ def execute_all( timeout (int | float | None, optional): The total execution timeout (in seconds) for this execution. wait_for_receipt (bool, optional): Whether to wait for consensus and return the receipt. If False, the method returns a TransactionResponse immediately after submission. + validate_status: (bool): Whether the query should automatically validate the transaction status (default False). Returns: List[TransactionReceipt]: If wait_for_receipt is True (default) @@ -369,7 +377,7 @@ def execute_all( self._validate_chunking() if self.get_required_chunks() == 1: - return [super().execute(client, timeout, wait_for_receipt)] + return [super().execute(client, timeout, wait_for_receipt, validate_status)] # Multi-chunk transaction - execute all chunks responses = [] @@ -389,7 +397,7 @@ def execute_all( super().sign(signing_key) # Execute the chunk - response = super().execute(client, timeout, wait_for_receipt) + response = super().execute(client, timeout, wait_for_receipt, validate_status) responses.append(response) return responses diff --git a/src/hiero_sdk_python/contract/contract_id.py b/src/hiero_sdk_python/contract/contract_id.py index 97ed95c9b..2e41b787b 100644 --- a/src/hiero_sdk_python/contract/contract_id.py +++ b/src/hiero_sdk_python/contract/contract_id.py @@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Optional from hiero_sdk_python.crypto.evm_address import EvmAddress +from hiero_sdk_python.crypto.key import Key from hiero_sdk_python.hapi.services import basic_types_pb2 from hiero_sdk_python.utils.entity_id_helper import ( parse_from_string, @@ -26,7 +27,7 @@ @dataclass(frozen=True) -class ContractId: +class ContractId(Key): """ Represents a unique contract ID on the Hedera network. @@ -90,6 +91,15 @@ def _to_proto(self): contractNum=self.contract, evm_address=self.evm_address, ) + + def to_proto_key(self) -> basic_types_pb2.Key: + """ + Convert the ContractId instance to a protobuf Key object. + + Returns: + basic_types_pb2.Key: The protobuf object of Key + """ + return basic_types_pb2.Key(contractID=self._to_proto()) @classmethod def from_string(cls, contract_id_str: str) -> "ContractId": diff --git a/src/hiero_sdk_python/contract/delegate_contract_id.py b/src/hiero_sdk_python/contract/delegate_contract_id.py new file mode 100644 index 000000000..69aa2ea57 --- /dev/null +++ b/src/hiero_sdk_python/contract/delegate_contract_id.py @@ -0,0 +1,81 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from hiero_sdk_python.contract.contract_id import ContractId +from hiero_sdk_python.hapi.services import basic_types_pb2 +from hiero_sdk_python.utils.entity_id_helper import format_to_string_with_checksum + +if TYPE_CHECKING: + from hiero_sdk_python.client.client import Client + +@dataclass(frozen=True) +class DelegateContractId(ContractId): + """ + Represents a delegatable contract identifier used as a key in the Hiero network. + + A DelegateContractId is a permissive key type that designates a smart contract + authorized to sign a transaction if it is the recipient of the active message + frame. Unlike a standard ContractID, this key type does not require the code + executing in the current frame to belong to the specified contract. + """ + + def to_proto_key(self) -> basic_types_pb2.Key: + return basic_types_pb2.Key(delegatable_contract_id=self._to_proto()) + + + + def __str__(self) -> str: + """ + Returns the string representation of the DelegateContractId. + + Format will be 'shard.realm.contract' or 'shard.realm.evm_address_hex' + if evm_address is set. Does not include a checksum. + + Returns: + str: The string representation of the ContractId. + """ + if self.evm_address is not None: + return f"{self.shard}.{self.realm}.{self.evm_address.hex()}" + + return f"{self.shard}.{self.realm}.{self.contract}" + + def __repr__(self) -> str: + """ + Returns a detailed string representation of the ContractId for debugging. + + Returns: + str: DelegateContractId(shard=X, realm=Y, contract=Z) or + DelegateContractId(shard=X, realm=Y, evm_address=...) if evm_address is set. + """ + if self.evm_address is not None: + return f"DelegateContractId(shard={self.shard}, realm={self.realm}, evm_address={self.evm_address.hex()})" + + return f"DelegateContractId(shard={self.shard}, realm={self.realm}, contract={self.contract})" + + + def to_string_with_checksum(self, client: "Client") -> str: + """ + Generates a string representation with a network-specific checksum. + + Format: 'shard.realm.contract-checksum' (e.g., "0.0.123-vfmkw"). + + Args: + client (Client): The client instance used to generate the + network-specific checksum. + + Returns: + str: The string representation with checksum. + + Raises: + ValueError: If the DelegateContractId has an `evm_address` set, + as checksums cannot be applied to EVM addresses. + """ + if self.evm_address is not None: + raise ValueError( + "to_string_with_checksum cannot be applied to DelegateContractId with evm_address" + ) + + return format_to_string_with_checksum( + self.shard, self.realm, self.contract, client + ) + diff --git a/src/hiero_sdk_python/crypto/evm_address.py b/src/hiero_sdk_python/crypto/evm_address.py index 1e396d189..596356859 100644 --- a/src/hiero_sdk_python/crypto/evm_address.py +++ b/src/hiero_sdk_python/crypto/evm_address.py @@ -1,4 +1,7 @@ -class EvmAddress: +from hiero_sdk_python.crypto.key import Key + + +class EvmAddress(Key): """ Represents a 20-byte EVM address derived from the rightmost 20 bytes of 32 byte Keccak-256 hash of an ECDSA public key. @@ -34,6 +37,10 @@ def from_string(cls, evm_address: str) -> "EvmAddress": def from_bytes(cls, address_bytes: "bytes") -> "EvmAddress": """Create an EvmAddress from raw bytes.""" return cls(address_bytes) + + + def to_proto_key(self): + raise RuntimeError("to_proto_key() not implemented for EvmAddress") def to_string(self) -> str: """Return the EVM address as a hex string""" diff --git a/src/hiero_sdk_python/crypto/key.py b/src/hiero_sdk_python/crypto/key.py new file mode 100644 index 000000000..d08b63620 --- /dev/null +++ b/src/hiero_sdk_python/crypto/key.py @@ -0,0 +1,117 @@ +from abc import ABC, abstractmethod + +from hiero_sdk_python.hapi.services import basic_types_pb2 + + +class Key(ABC): + """ + Abstract base class representing a Hiero cryptographic key. + This class defines the common interface for all supported key types. + + Concrete implementations must implement to_proto_key. + """ + + @classmethod + def from_proto_key(cls, proto: basic_types_pb2.Key) -> "Key": + """ + Convert a protobuf Key message into the appropriate SDK Key object. + + The method inspects the oneof key field in the protobuf message + and constructs the corresponding SDK key implementation. + + Supported types: + - ed25519 + - ECDSA_secp256k1 + - contractID + - delegatable_contract_id + - keyList + - thresholdKey + + Args: + proto (basic_types_pb2.Key): The protobuf Key message. + + Returns: + Key: The corresponding Key implementation. + + Raises: + TypeError: If proto is not a Key protobuf message. + ValueError: If the key type is unknown. + """ + from hiero_sdk_python.crypto.public_key import PublicKey + from hiero_sdk_python.crypto.evm_address import EvmAddress + from hiero_sdk_python.contract.contract_id import ContractId + from hiero_sdk_python.contract.delegate_contract_id import DelegateContractId + from hiero_sdk_python.crypto.key_list import KeyList + + if not isinstance(proto, basic_types_pb2.Key): + raise TypeError("proto must be an instance of basic_types_pb2.Key") + + key_type = proto.WhichOneof("key") + + match key_type: + case "ed25519": + return PublicKey._from_bytes_ed25519(proto.ed25519) + + case "ECDSA_secp256k1": + if len(proto.ECDSA_secp256k1) == 20: + return EvmAddress.from_bytes(proto.ECDSA_secp256k1) + + return PublicKey.from_bytes_ecdsa(proto.ECDSA_secp256k1) + + case "contractID": + return ContractId._from_proto(proto.contractID) + + case "delegatable_contract_id": + return DelegateContractId._from_proto(proto.delegatable_contract_id) + + case "keyList": + return KeyList.from_proto(proto=proto.keyList) + + case "thresholdKey": + return KeyList.from_proto( + proto=proto.thresholdKey.keys, + threshold=proto.thresholdKey.threshold, + ) + + case _: + raise ValueError(f"Unknown key type: {key_type}") + + @classmethod + def from_bytes(cls, data: bytes) -> "Key": + """ + Deserialize a Key object from protobuf-encoded bytes. + + Args: + data (bytes): Serialized protobuf Key bytes. + + Returns: + Key: The reconstructed Key instance. + + Raises: + TypeError: If data is not bytes. + """ + if not isinstance(data, (bytes, bytearray)): + raise TypeError("data must be bytes") + + key = basic_types_pb2.Key() + key.ParseFromString(data) + return cls.from_proto_key(key) + + @abstractmethod + def to_proto_key(self) -> basic_types_pb2.Key: + """ + Convert this key into its protobuf representation. + + Returns: + basic_types_pb2.Key: The protobuf Key message. + """ + pass + + def to_bytes(self) -> bytes: + """ + Serialize this Key into protobuf bytes. + + Returns: + bytes: The serialized protobuf Key. + """ + return self.to_proto_key().SerializeToString() diff --git a/src/hiero_sdk_python/crypto/key_list.py b/src/hiero_sdk_python/crypto/key_list.py new file mode 100644 index 000000000..c0bf8ae00 --- /dev/null +++ b/src/hiero_sdk_python/crypto/key_list.py @@ -0,0 +1,157 @@ +from typing import List + +from hiero_sdk_python.crypto.key import Key +from hiero_sdk_python.hapi.services import basic_types_pb2 + + +class KeyList(Key): + """ + Represents a list of cryptographic keys that can sign a transaction. + + The list may optionally define a threshold, specifying how many + keys must sign for the transaction to be valid. + """ + + def __init__(self, keys: List[Key] = None, threshold: int | None = None) -> None: + """ + Initialize a KeyList. + + Args: + keys (List[Key], optional): A list of keys that belong to this KeyList. + threshold (int, optional): The minimum number of keys required to sign. + + Raises: + TypeError: If keys are not a list of Key objects or threshold not an int. + """ + if keys is not None: + if not isinstance(keys, list): + raise TypeError("keys must be a list of Key objects") + + for key in keys: + if not isinstance(key, Key): + raise TypeError("All elements in keys must be instances of Key") + + if threshold is not None and not isinstance(threshold, int): + raise TypeError("threshold must be an integer") + + self.keys: List[Key] = keys or [] + self.threshold: int | None = threshold + + def set_keys(self, keys: List[Key]) -> "KeyList": + """ + Set the keys contained in this KeyList. + + Args: + keys (List[Key]): The new list of keys. + + Returns: + KeyList: The current instance for method chaining. + + Raises: + TypeError: If keys is not a list of Key objects. + """ + if not isinstance(keys, list): + raise TypeError("keys must be a list of Key objects") + + for key in keys: + if not isinstance(key, Key): + raise TypeError("All elements in keys must be instances of Key") + + self.keys = keys + return self + + def add_key(self, key: Key) -> "KeyList": + """ + Add a key to the KeyList. + + Args: + key (Key): The key to add. + + Returns: + KeyList: The current instance for method chaining. + + Raises: + TypeError: If key is not an instance of Key. + """ + if not isinstance(key, Key): + raise TypeError("All elements in keys must be instances of Key") + + self.keys.append(key) + return self + + def set_threshold(self, threshold: int | None) -> "KeyList": + """ + Set the threshold for this KeyList. + + Args: + threshold (int | None): The minimum number of keys required to sign. + + Returns: + KeyList: The current instance for method chaining. + + Raises: + TypeError: If threshold is not an integer. + """ + if threshold is not None and not isinstance(threshold, int): + raise TypeError("threshold must be an integer") + + self.threshold = threshold + return self + + @classmethod + def from_proto( + cls, proto: basic_types_pb2.KeyList, threshold: int = None + ) -> "KeyList": + """ + Create a KeyList from a protobuf representation. + + Args: + proto (basic_types_pb2.KeyList): The protobuf KeyList. + threshold (int, optional): Optional threshold value for the KeyList. + + Returns: + KeyList: The constructed KeyList instance. + """ + keys = [] + for key in proto.keys: + keys.append(Key.from_proto_key(key)) + + return cls(keys=keys, threshold=threshold) + + def to_proto(self) -> basic_types_pb2.KeyList: + """ + Convert this KeyList to its protobuf representation. + + Returns: + basic_types_pb2.KeyList: The protobuf KeyList object. + """ + proto_keys = [] + for key in self.keys: + proto_keys.append(key.to_proto_key()) + + return basic_types_pb2.KeyList(keys=proto_keys) + + def to_proto_key(self) -> basic_types_pb2.Key: + """ + Convert this KeyList to a protobuf Key object. + + If a threshold is defined, the result will be a ThresholdKey. + Otherwise, it will be a standard KeyList. + + Returns: + basic_types_pb2.Key: The protobuf Key representation. + """ + proto_key_list = [] + + for key in self.keys: + proto_key_list.append(key.to_proto_key()) + + if self.threshold is not None: + threshold_key = basic_types_pb2.ThresholdKey( + threshold=self.threshold, + keys=basic_types_pb2.KeyList(keys=proto_key_list), + ) + return basic_types_pb2.Key(thresholdKey=threshold_key) + + key_list = basic_types_pb2.KeyList(keys=proto_key_list) + return basic_types_pb2.Key(keyList=key_list) diff --git a/src/hiero_sdk_python/crypto/private_key.py b/src/hiero_sdk_python/crypto/private_key.py index 6b97a38ae..5f65ade14 100644 --- a/src/hiero_sdk_python/crypto/private_key.py +++ b/src/hiero_sdk_python/crypto/private_key.py @@ -5,13 +5,15 @@ from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ed25519, ec from cryptography.hazmat.primitives.asymmetric import utils as asym_utils +from hiero_sdk_python.crypto.key import Key from hiero_sdk_python.crypto.public_key import PublicKey +from hiero_sdk_python.hapi.services import basic_types_pb2 from hiero_sdk_python.utils.crypto_utils import keccak256 _LEGACY_ECDSA_PRIVATE_KEY_PREFIX = "3030020100300706052b8104000a04220420" -class PrivateKey: +class PrivateKey(Key): """ Represents a private key that can be either Ed25519 or ECDSA (secp256k1). Can load from raw 32-byte seeds/scalars or DER-encoded keys. @@ -470,6 +472,15 @@ def _parse_legacy_ecdsa_der_key(key_bytes: bytes) -> "ec.EllipticCurvePrivateKey except Exception as exc: raise ValueError(f"Failed to derive ECDSA private key: {exc}") from exc + def to_proto_key(self) -> basic_types_pb2.Key: + """ + Convert the instance of PrivateKey to the protobuf object of Key. + + Returns: + basic_types_pb2.Key: The protobuf object of Key. + """ + return self.public_key().to_proto_key() + def __eq__(self, other: object) -> bool: """ Compare two PrivateKey objects for equality. @@ -486,3 +497,4 @@ def __eq__(self, other: object) -> bool: def __hash__(self) -> int: """Returns the hash value for the private key.""" return hash((self.is_ed25519(), self.to_bytes_raw())) + \ No newline at end of file diff --git a/src/hiero_sdk_python/crypto/public_key.py b/src/hiero_sdk_python/crypto/public_key.py index e7f55ee1f..7142d7b11 100644 --- a/src/hiero_sdk_python/crypto/public_key.py +++ b/src/hiero_sdk_python/crypto/public_key.py @@ -7,7 +7,7 @@ from cryptography.hazmat.primitives.asymmetric import utils as asym_utils from hiero_sdk_python.contract.contract_id import ContractId from hiero_sdk_python.crypto.evm_address import EvmAddress -from hiero_sdk_python.hapi.services.basic_types_pb2 import Key +from hiero_sdk_python.crypto.key import Key from hiero_sdk_python.hapi.services import basic_types_pb2 from hiero_sdk_python.utils.crypto_utils import keccak256 @@ -19,7 +19,7 @@ def _warn_ed25519_ambiguity(caller_name: str) -> None: stacklevel=3 ) -class PublicKey: +class PublicKey(Key): """ Represents a public key. Supports multiple key formats: raw bytes, DER-encoded keys, @@ -307,7 +307,7 @@ def from_string(cls, hex_str: str) -> "PublicKey": # @classmethod - def _from_proto(cls, proto: Key) -> "PublicKey": + def _from_proto(cls, proto: basic_types_pb2.Key) -> "PublicKey": """ Load a public key from a protobuf Key message. """ @@ -342,6 +342,15 @@ def _to_proto(self) -> Key: if self.is_ed25519(): return basic_types_pb2.Key(ed25519=pub_bytes) return basic_types_pb2.Key(ECDSA_secp256k1=pub_bytes) + + def to_proto_key(self) -> basic_types_pb2.Key: + """ + Convert the instance of PublicKey to the protobuf object of Key. + + Returns: + basic_types_pb2.Key: The protobuf object of Key. + """ + return self._to_proto() # # --------------------------------- diff --git a/src/hiero_sdk_python/file/file_append_transaction.py b/src/hiero_sdk_python/file/file_append_transaction.py index 48da95320..3265f9884 100644 --- a/src/hiero_sdk_python/file/file_append_transaction.py +++ b/src/hiero_sdk_python/file/file_append_transaction.py @@ -327,6 +327,7 @@ def execute( client: "Client", timeout: int | float | None = None, wait_for_receipt: Literal[True] = True, + validate_status: bool = False ) -> "TransactionReceipt": ... @@ -336,6 +337,7 @@ def execute( client: "Client", timeout: int | float | None = None, wait_for_receipt: Literal[False] = False, + validate_status: bool = False ) -> "TransactionResponse": ... @@ -343,7 +345,8 @@ def execute( self, client: "Client", timeout: int | float | None = None, - wait_for_receipt: bool = True + wait_for_receipt: bool = True, + validate_status: bool = False ) -> "TransactionReceipt" | "TransactionResponse": """ Executes the file append transaction. @@ -355,13 +358,14 @@ def execute( timeout (int | float | None, optional): The total execution timeout (in seconds) for this execution. wait_for_receipt (bool, optional): Whether to wait for consensus and return the receipt. If False, the method returns a TransactionResponse immediately after submission. + validate_status: (bool): Whether the query should automatically validate the transaction status (default False). Returns: TransactionReceipt: If wait_for_receipt is True (default) TransactionResponse: If wait_for_receipt is False """ # Return the first response (as per JavaScript implementation) - return self.execute_all(client, timeout, wait_for_receipt)[0] + return self.execute_all(client, timeout, wait_for_receipt, validate_status)[0] @overload def execute_all( @@ -369,6 +373,7 @@ def execute_all( client: "Client", timeout: int | float | None = None, wait_for_receipt: Literal[True] = True, + validate_status: bool = False ) -> List["TransactionReceipt"]: ... @@ -378,6 +383,7 @@ def execute_all( client: "Client", timeout: int | float | None = None, wait_for_receipt: Literal[False] = False, + validate_status: bool = False ) -> List["TransactionResponse"]: ... @@ -385,7 +391,8 @@ def execute_all( self, client: "Client", timeout: int | float | None = None, - wait_for_receipt: bool = True + wait_for_receipt: bool = True, + validate_status: bool = False ) -> List["TransactionReceipt"] | List["TransactionResponse"]: """ Executes the file append transaction. @@ -397,6 +404,7 @@ def execute_all( timeout (int | float | None, optional): The total execution timeout (in seconds) for this execution. wait_for_receipt (bool, optional): Whether to wait for consensus and return the receipt. If False, the method returns a TransactionResponse immediately after submission. + validate_status: (bool): Whether the query should automatically validate the transaction status (default False). Returns: List[TransactionReceipt]: If wait_for_receipt is True (default) @@ -406,7 +414,7 @@ def execute_all( if self.get_required_chunks() == 1: # Single chunk transaction - return [super().execute(client, timeout, wait_for_receipt)] + return [super().execute(client, timeout, wait_for_receipt, validate_status)] # Multi-chunk transaction - execute all chunks responses = [] @@ -430,7 +438,7 @@ def execute_all( super().sign(signing_key) # Execute the chunk - response = super().execute(client, timeout, wait_for_receipt) + response = super().execute(client, timeout, wait_for_receipt, validate_status) responses.append(response) return responses diff --git a/src/hiero_sdk_python/hbar.py b/src/hiero_sdk_python/hbar.py index cc080b66e..fbdc2ad57 100644 --- a/src/hiero_sdk_python/hbar.py +++ b/src/hiero_sdk_python/hbar.py @@ -45,6 +45,8 @@ def __init__( if isinstance(amount, float) and not math.isfinite(amount): raise ValueError("Hbar amount must be finite") + if isinstance(amount, Decimal) and not amount.is_finite(): + raise ValueError("Hbar amount must be finite") if unit == HbarUnit.TINYBAR: if not isinstance(amount, int): @@ -56,7 +58,7 @@ def __init__( amount = Decimal(str(amount)) tinybar = amount * Decimal(unit.tinybar) - if tinybar % 1 != 0: + if tinybar != tinybar.to_integral_value(): raise ValueError("Fractional tinybar value not allowed") self._amount_in_tinybar = int(tinybar) @@ -67,7 +69,7 @@ def to(self, unit: HbarUnit) -> float: def to_tinybars(self) -> int: """Return the amount of hbars in tinybars.""" - return int(self.to(HbarUnit.TINYBAR)) + return self._amount_in_tinybar def to_hbars(self) -> float: """ @@ -220,4 +222,4 @@ def __ge__(self, other: object) -> bool: Hbar.ZERO = Hbar(0) Hbar.MAX = Hbar(50_000_000_000) -Hbar.MIN = Hbar(-50_000_000_000) \ No newline at end of file +Hbar.MIN = Hbar(-50_000_000_000) diff --git a/src/hiero_sdk_python/query/transaction_get_receipt_query.py b/src/hiero_sdk_python/query/transaction_get_receipt_query.py index 49418728d..0b8ab52a5 100644 --- a/src/hiero_sdk_python/query/transaction_get_receipt_query.py +++ b/src/hiero_sdk_python/query/transaction_get_receipt_query.py @@ -29,6 +29,7 @@ def __init__( transaction_id: Optional[TransactionId] = None, include_children: bool = False, include_duplicates: bool = False, + validate_status: bool = False ) -> None: """ Initializes a new instance of the TransactionGetReceiptQuery class. @@ -37,12 +38,14 @@ def __init__( transaction_id (TransactionId, optional): The ID of the transaction. include_children (bool): Whether to include child transaction receipts. include_duplicates (bool): Whether to include duplicate transaction receipts. + validate_status: (bool): Whether the query should automatically validate the transaction status. """ super().__init__() self.transaction_id: Optional[TransactionId] = transaction_id self._frozen: bool = False self.include_children = include_children self.include_duplicates = include_duplicates + self.validate_status = validate_status # To keep backward compatible def _require_not_frozen(self) -> None: """ @@ -108,6 +111,27 @@ def set_include_duplicates( self._require_not_frozen() self.include_duplicates = include_duplicates return self + + def set_validate_status(self, validate_status: bool) -> "TransactionGetReceiptQuery": + """ + Sets whether the query should automatically validate the transaction status. + + When set to True, the execute() method will raise a ReceiptStatusError if + the transaction receipt status is anything other than SUCCESS. + + Args: + validate_status (bool): True to enable automatic error raising on failure statuses; + False to return the receipt regardless of outcome. (default False) + + Returns: + TransactionGetReceiptQuery: The current instance for method chaining. + + Raises: + ValueError: If the query is frozen and cannot be modified. + """ + self._require_not_frozen() + self.validate_status = validate_status + return self def freeze(self) -> "TransactionGetReceiptQuery": """ @@ -211,7 +235,11 @@ def _should_retry(self, response: response_pb2.Response) -> _ExecutionState: if status in retryable_statuses or status == ResponseCode.OK: return _ExecutionState.RETRY + elif status == ResponseCode.SUCCESS: + return _ExecutionState.FINISHED else: + if self.validate_status: + return _ExecutionState.ERROR return _ExecutionState.FINISHED def _map_status_error(self, response: response_pb2.Response) -> Union[PrecheckError, ReceiptStatusError]: diff --git a/src/hiero_sdk_python/tokens/token_association.py b/src/hiero_sdk_python/tokens/token_association.py new file mode 100644 index 000000000..5c8848049 --- /dev/null +++ b/src/hiero_sdk_python/tokens/token_association.py @@ -0,0 +1,75 @@ +"""Dataclass for automatic token associations in Hedera transaction records.""" +from __future__ import annotations + +from dataclasses import dataclass + +from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.hapi.services.basic_types_pb2 import TokenAssociation as TokenAssociationProto +from hiero_sdk_python.tokens.token_id import TokenId + + +@dataclass(frozen=True) +class TokenAssociation: + """Represents an automatic token association between a token and an account. + + This class appears in `TransactionRecord.automatic_token_associations` (repeated field) + when the network creates associations automatically (e.g., during token transfers + or airdrops to unassociated accounts). + + These associations are informational only and cannot be used to create new associations + on the ledger โ€” use TokenAssociateTransaction for that. + """ + + token_id: TokenId | None = None + """The ID of the token that was automatically associated.""" + + account_id: AccountId | None = None + """The ID of the account that received the automatic association.""" + + @classmethod + def _from_proto(cls, proto: TokenAssociationProto) -> TokenAssociation: + """Create a TokenAssociation instance from the protobuf message.""" + return cls( + token_id=( + TokenId._from_proto(proto.token_id) + if proto.HasField("token_id") + else None + ), + account_id=( + AccountId._from_proto(proto.account_id) + if proto.HasField("account_id") + else None + ), + ) + + def _to_proto(self) -> TokenAssociationProto: + """Convert this TokenAssociation instance back to a protobuf message.""" + proto = TokenAssociationProto() + + if self.token_id is not None: + proto.token_id.CopyFrom(self.token_id._to_proto()) + + if self.account_id is not None: + proto.account_id.CopyFrom(self.account_id._to_proto()) + + return proto + + def to_bytes(self) -> bytes: + """Serialize this TokenAssociation to raw protobuf bytes.""" + return self._to_proto().SerializeToString() + + @classmethod + def from_bytes(cls, data: bytes) -> TokenAssociation: + """Deserialize a TokenAssociation from raw protobuf bytes.""" + proto = TokenAssociationProto() + proto.ParseFromString(data) + return cls._from_proto(proto) + + def __repr__(self) -> str: + """Returns an unambiguous string representation for debugging.""" + return f"TokenAssociation(token_id={self.token_id!r}, account_id={self.account_id!r})" + + def __str__(self) -> str: + """Returns a human-readable string representation.""" + return self.__repr__() + \ No newline at end of file diff --git a/src/hiero_sdk_python/tokens/token_id.py b/src/hiero_sdk_python/tokens/token_id.py index 8defa71f5..39ca06d9c 100644 --- a/src/hiero_sdk_python/tokens/token_id.py +++ b/src/hiero_sdk_python/tokens/token_id.py @@ -170,6 +170,15 @@ def __str__(self) -> str: str: The token ID string in 'shard.realm.num' format. """ return format_to_string(self.shard, self.realm, self.num) + + def __repr__(self) -> str: + """ + Returns a detailed representation of the TokenId suitable for debugging. + + Returns: + str: A string in constructor format 'TokenId(shard=X, realm=Y, num=Z)'. + """ + return f"TokenId(shard={self.shard}, realm={self.realm}, num={self.num})" def __hash__(self) -> int: """Generates a hash based on the shard, realm, and num. diff --git a/src/hiero_sdk_python/transaction/transaction.py b/src/hiero_sdk_python/transaction/transaction.py index 0fe86b183..22a721491 100644 --- a/src/hiero_sdk_python/transaction/transaction.py +++ b/src/hiero_sdk_python/transaction/transaction.py @@ -335,6 +335,7 @@ def execute( client: "Client", timeout: int | float | None = None, wait_for_receipt: Literal[True] = True, + validate_status: bool = False ) -> "TransactionReceipt": ... @@ -344,6 +345,7 @@ def execute( client: "Client", timeout: int | float | None = None, wait_for_receipt: Literal[False] = False, + validate_status: bool = False ) -> "TransactionResponse": ... @@ -351,7 +353,8 @@ def execute( self, client: "Client", timeout: int | float | None = None, - wait_for_receipt: bool = True + wait_for_receipt: bool = True, + validate_status: bool = False ) -> TransactionReceipt | TransactionResponse: """ Executes the transaction on the Hedera network using the provided client. @@ -363,6 +366,7 @@ def execute( timeout (int | float | None, optional): The total execution timeout (in seconds) for this execution. wait_for_receipt (bool, optional): Whether to wait for consensus and return the receipt. If False, the method returns a TransactionResponse immediately after submission. + validate_status: (bool, optional): Whether the query should automatically validate the transaction status. Returns: TransactionReceipt: If wait_for_receipt is True (default) @@ -394,7 +398,7 @@ def execute( response.transaction_id = self.transaction_id if wait_for_receipt: - return response.get_receipt(client, timeout=timeout) + return response.get_receipt(client, timeout=timeout, validate_status=validate_status) return response diff --git a/src/hiero_sdk_python/transaction/transaction_record.py b/src/hiero_sdk_python/transaction/transaction_record.py index 91f9479eb..75bb7d6d1 100644 --- a/src/hiero_sdk_python/transaction/transaction_record.py +++ b/src/hiero_sdk_python/transaction/transaction_record.py @@ -1,4 +1,6 @@ """ +Module for the TransactionRecord class. + This module implements the TransactionRecord class which represents a complete record of a transaction executed on the Hiero network. It serves as a comprehensive data structure that captures all aspects of a transaction's execution and its effects. @@ -16,14 +18,18 @@ """ +from __future__ import annotations from collections import defaultdict from dataclasses import dataclass, field -from typing import Optional from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.contract.contract_function_result import ContractFunctionResult from hiero_sdk_python.hapi.services import transaction_record_pb2 +from hiero_sdk_python.schedule.schedule_id import ScheduleId +from hiero_sdk_python.timestamp import Timestamp +from hiero_sdk_python.tokens.assessed_custom_fee import AssessedCustomFee from hiero_sdk_python.tokens.token_airdrop_pending_record import PendingAirdropRecord +from hiero_sdk_python.tokens.token_association import TokenAssociation from hiero_sdk_python.tokens.token_id import TokenId from hiero_sdk_python.tokens.token_nft_transfer import TokenNftTransfer from hiero_sdk_python.transaction.transaction_id import TransactionId @@ -34,6 +40,7 @@ class TransactionRecord: """ Represents a record of a completed transaction on the Hiero network. + This class combines detailed information about the a transaction including the transaction ID, receipt, token and NFT transfers, fees & other metadata such as pseudo-random number generation(PRNG) results and @@ -60,32 +67,58 @@ class TransactionRecord: with include_duplicates=True. Empty by default. children (list[TransactionRecord]): A list of children transaction records returned when queried - with include_children=True. Empty by default. + with include_children=True. Empty by default. + consensus_timestamp (Optional[Timestamp]): Network-assigned consensus timestamp when the transaction was finalized. + schedule_ref (Optional[ScheduleId]): Schedule ID if this transaction was executed via a schedule. + assessed_custom_fees (list[AssessedCustomFee]): Custom fees that were assessed and charged during execution. + automatic_token_associations (list[TokenAssociation]): + Automatic token-account associations created by the network. + parent_consensus_timestamp (Optional[Timestamp]): Consensus timestamp of the parent transaction (for child records). + alias (Optional[bytes]): New account alias created (e.g., during account creation with alias). + ethereum_hash (Optional[bytes]): Keccak-256 hash of the Ethereum-compatible transaction data. + paid_staking_rewards (list[tuple[AccountId, int]]): + Staking rewards paid out (account ID โ†’ amount in tinybars). + evm_address (Optional[bytes]): EVM address created or associated with the account. + contract_create_result (Optional[ContractFunctionResult]): + Result of a contract creation transaction (if applicable) """ - transaction_id: Optional[TransactionId] = None - transaction_hash: Optional[bytes] = None - transaction_memo: Optional[str] = None - transaction_fee: Optional[int] = None - receipt: Optional[TransactionReceipt] = None - call_result: Optional[ContractFunctionResult] = None + transaction_id: TransactionId | None = None + transaction_hash: bytes | None = None + transaction_memo: str | None = None + transaction_fee: int | None = None + receipt: TransactionReceipt | None = None + call_result: ContractFunctionResult | None = None token_transfers: defaultdict[TokenId, defaultdict[AccountId, int]] = field(default_factory=lambda: defaultdict(lambda: defaultdict(int))) nft_transfers: defaultdict[TokenId, list[TokenNftTransfer]] = field(default_factory=lambda: defaultdict(list[TokenNftTransfer])) transfers: defaultdict[AccountId, int] = field(default_factory=lambda: defaultdict(int)) new_pending_airdrops: list[PendingAirdropRecord] = field(default_factory=list) - prng_number: Optional[int] = None - prng_bytes: Optional[bytes] = None - duplicates: list['TransactionRecord'] = field(default_factory=list) - children: list['TransactionRecord'] = field(default_factory=list) + prng_number: int | None = None + prng_bytes: bytes | None = None + duplicates: list[TransactionRecord] = field(default_factory=list) + children: list[TransactionRecord] = field(default_factory=list) + + consensus_timestamp: Timestamp | None = None + schedule_ref: ScheduleId | None = None + assessed_custom_fees: list[AssessedCustomFee] = field(default_factory=list) + automatic_token_associations: list[TokenAssociation] = field(default_factory=list) + parent_consensus_timestamp: Timestamp | None = None + alias: bytes | None = None + ethereum_hash: bytes | None = None + paid_staking_rewards: list[tuple[AccountId, int]] = field(default_factory=list) + evm_address: bytes | None = None + contract_create_result: ContractFunctionResult | None = None def __repr__(self) -> str: """Returns a human-readable string representation of the TransactionRecord. This method constructs a detailed string containing all significant fields of the - transaction record including transaction ID, hash, memo, fees, status, transfers, - and PRNG results. For the receipt status, it attempts to resolve the numeric status + transaction record including transaction ID, hash, memo, fees, status, transfers, + PRNG results, consensus timestamp, schedule ref, custom fees, automatic associations, + parent timestamp, alias, Ethereum hash, staking rewards, EVM address, contract create result. + For the receipt status, it attempts to resolve the numeric status to a human-readable ResponseCode name. Returns: @@ -112,16 +145,26 @@ def __repr__(self) -> str: f"prng_number={self.prng_number}, " f"prng_bytes={self.prng_bytes}, " f"duplicates_count={len(self.duplicates)}, " - f"children_count={len(self.children)})") - + f"children_count={len(self.children)}, " + f"consensus_timestamp={self.consensus_timestamp}, " + f"schedule_ref={self.schedule_ref}, " + f"assessed_custom_fees={self.assessed_custom_fees}, " + f"automatic_token_associations={self.automatic_token_associations}, " + f"parent_consensus_timestamp={self.parent_consensus_timestamp}, " + f"alias={self.alias}, " + f"ethereum_hash={self.ethereum_hash}, " + f"paid_staking_rewards={self.paid_staking_rewards}, " + f"evm_address={self.evm_address}, " + f"contract_create_result={self.contract_create_result})") + @classmethod def _from_proto( cls, proto: transaction_record_pb2.TransactionRecord, - transaction_id: Optional[TransactionId] = None, - duplicates: Optional[list['TransactionRecord']] = None, - children: Optional[list['TransactionRecord']] = None, - ) -> 'TransactionRecord': + transaction_id: TransactionId | None = None, + duplicates: list[TransactionRecord] | None = None, + children: list[TransactionRecord] | None = None, + ) -> TransactionRecord: """Creates a TransactionRecord instance from a protobuf transaction record. This method performs complex data aggregation from the protobuf message, @@ -132,6 +175,11 @@ def _from_proto( - Contract execution results - Airdrop records - PRNG (pseudo-random number generation) results + - Other protobuf fields: consensus timestamp, schedule reference, + assessed custom fees, automatic token associations, parent consensus + timestamp, alias, ethereum hash, paid staking rewards, EVM address, + and contract create result + The method maps all nested transfer data from the raw protobuf message into the structured format used by the TransactionRecord class & organizing them @@ -155,7 +203,54 @@ def _from_proto( token_transfers, nft_transfers = cls._parse_token_transfers(proto) transfers = cls._parse_hbar_transfers(proto) new_pending_airdrops = cls._parse_pending_airdrops(proto) - call_result = cls._parse_contract_call_result(proto) + call_result = None + contract_create_result = None + + body_case = proto.WhichOneof("body") + if body_case == "contractCallResult": + call_result = ContractFunctionResult._from_proto(proto.contractCallResult) + elif body_case == "contractCreateResult": + contract_create_result = ContractFunctionResult._from_proto(proto.contractCreateResult) + + consensus_timestamp = ( + Timestamp._from_protobuf(proto.consensusTimestamp) + if proto.HasField("consensusTimestamp") else None + ) + parent_consensus_timestamp = ( + Timestamp._from_protobuf(proto.parent_consensus_timestamp) + if proto.HasField("parent_consensus_timestamp") else None + ) + schedule_ref = ( + ScheduleId._from_proto(proto.scheduleRef) + if proto.HasField("scheduleRef") else None + ) + assessed_custom_fees = [ + AssessedCustomFee._from_proto(fee) for fee in proto.assessed_custom_fees + ] + automatic_token_associations = [ + TokenAssociation._from_proto(assoc) for assoc in proto.automatic_token_associations + ] + paid_staking_rewards = [ + (AccountId._from_proto(r.accountID), r.amount) + for r in proto.paid_staking_rewards + ] + contract_create_result = ( + ContractFunctionResult._from_proto(proto.contractCreateResult) + if proto.HasField("contractCreateResult") else None + ) + alias = proto.alias if proto.alias else None + ethereum_hash = proto.ethereum_hash if proto.ethereum_hash else None + evm_address = proto.evm_address if proto.evm_address else None + + entropy_case = proto.WhichOneof("entropy") + + prng_number = None + prng_bytes = None + + if entropy_case == "prng_number": + prng_number = proto.prng_number + elif entropy_case == "prng_bytes": + prng_bytes = proto.prng_bytes return cls( transaction_id=tx_id, @@ -168,16 +263,26 @@ def _from_proto( transfers=transfers, new_pending_airdrops=new_pending_airdrops, call_result=call_result, - prng_number=proto.prng_number, - prng_bytes=proto.prng_bytes, + prng_number=prng_number, + prng_bytes=prng_bytes, duplicates=duplicates, children=children, + consensus_timestamp=consensus_timestamp, + schedule_ref=schedule_ref, + assessed_custom_fees=assessed_custom_fees, + automatic_token_associations=automatic_token_associations, + parent_consensus_timestamp=parent_consensus_timestamp, + alias=alias, + ethereum_hash=ethereum_hash, + paid_staking_rewards=paid_staking_rewards, + evm_address=evm_address, + contract_create_result=contract_create_result, ) @staticmethod def _resolve_transaction_id( proto: transaction_record_pb2.TransactionRecord, - transaction_id: Optional[TransactionId], + transaction_id: TransactionId | None, ) -> TransactionId: """Resolves the transaction ID from proto or raises error if not available.""" if proto.HasField("transactionID"): @@ -235,7 +340,7 @@ def _parse_pending_airdrops( @staticmethod def _parse_contract_call_result( proto: transaction_record_pb2.TransactionRecord, - ) -> Optional[ContractFunctionResult]: + ) -> ContractFunctionResult | None: """Parses contract call result from proto if present.""" if proto.HasField("contractCallResult"): return ContractFunctionResult._from_proto(proto.contractCallResult) @@ -256,6 +361,10 @@ def _to_proto(self) -> transaction_record_pb2.TransactionRecord: - Contract execution results - Airdrop records - PRNG (pseudo-random number generation) results + - Other protobuf fields: consensus timestamp, schedule reference, + assessed custom fees, automatic token associations, parent consensus + timestamp, alias, ethereum hash, paid staking rewards, EVM address, + and contract create result The method performs a deep conversion of all nested objects (token transfers, NFT transfers, account transfers, and pending airdrops) to their respective @@ -266,17 +375,33 @@ def _to_proto(self) -> transaction_record_pb2.TransactionRecord: all the transaction record data in a format suitable for network transmission or storage. """ + if self.call_result is not None and self.contract_create_result is not None: + raise ValueError( + "contractCallResult and contractCreateResult are mutually exclusive " + "proto oneof fields and cannot both be set simultaneously." + ) + + if self.prng_number is not None and self.prng_bytes is not None: + raise ValueError( + "prng_number and prng_bytes are mutually exclusive " + "proto oneof fields (entropy) and cannot both be set simultaneously." + ) + record_proto = transaction_record_pb2.TransactionRecord( transactionHash=self.transaction_hash, memo=self.transaction_memo, transactionFee=self.transaction_fee, receipt=self.receipt._to_proto() if self.receipt else None, - contractCallResult=( - self.call_result._to_proto() if self.call_result else None - ), - prng_number=self.prng_number, - prng_bytes=self.prng_bytes, ) + + if self.call_result is not None: + record_proto.contractCallResult.CopyFrom(self.call_result._to_proto()) + elif self.contract_create_result is not None: + record_proto.contractCreateResult.CopyFrom(self.contract_create_result._to_proto()) + if self.prng_number is not None: + record_proto.prng_number = self.prng_number + if self.prng_bytes is not None: + record_proto.prng_bytes = self.prng_bytes if self.transaction_id is not None: record_proto.transactionID.CopyFrom(self.transaction_id._to_proto()) @@ -302,6 +427,38 @@ def _to_proto(self) -> transaction_record_pb2.TransactionRecord: for pending_airdrop in self.new_pending_airdrops: record_proto.new_pending_airdrops.add().CopyFrom(pending_airdrop._to_proto()) + if self.consensus_timestamp is not None: + record_proto.consensusTimestamp.CopyFrom(self.consensus_timestamp._to_protobuf()) + + if self.schedule_ref is not None: + record_proto.scheduleRef.CopyFrom(self.schedule_ref._to_proto()) + + record_proto.assessed_custom_fees.extend( + fee._to_proto() for fee in self.assessed_custom_fees + ) + + record_proto.automatic_token_associations.extend( + assoc._to_proto() for assoc in self.automatic_token_associations + ) + + if self.parent_consensus_timestamp is not None: + record_proto.parent_consensus_timestamp.CopyFrom(self.parent_consensus_timestamp._to_protobuf()) + + if self.alias is not None: + record_proto.alias = self.alias + + if self.ethereum_hash is not None: + record_proto.ethereum_hash = self.ethereum_hash + + for account_id, amount in self.paid_staking_rewards: + aa = record_proto.paid_staking_rewards.add() + aa.accountID.CopyFrom(account_id._to_proto()) + aa.amount = amount + + if self.evm_address is not None: + record_proto.evm_address = self.evm_address + + if self.contract_create_result is not None: + record_proto.contractCreateResult.CopyFrom(self.contract_create_result._to_proto()) return record_proto - diff --git a/src/hiero_sdk_python/transaction/transaction_response.py b/src/hiero_sdk_python/transaction/transaction_response.py index 6ef7e7618..e52835793 100644 --- a/src/hiero_sdk_python/transaction/transaction_response.py +++ b/src/hiero_sdk_python/transaction/transaction_response.py @@ -34,10 +34,13 @@ def __init__(self) -> None: self.validate_status: bool = False self.transaction: Optional["Transaction"] = None - def get_receipt_query(self): + def get_receipt_query(self, validate_status: bool = False): """ Create a receipt query for this transaction. + Args: + validate_status (bool, Optional): The query should automatically validate the transaction status. (default False) + Returns: TransactionGetReceiptQuery: A configured receipt query. """ @@ -46,21 +49,28 @@ def get_receipt_query(self): TransactionGetReceiptQuery() .set_transaction_id(self.transaction_id) .set_node_account_id(self.node_id) + .set_validate_status(validate_status) ) - def get_receipt(self, client: "Client", timeout: Optional[Union[int, float]] = None) -> "TransactionReceipt": + def get_receipt( + self, + client: "Client", + timeout: Optional[Union[int, float]] = None, + validate_status: bool = False + ) -> "TransactionReceipt": """ Retrieves the receipt for this transaction from the network. Args: client (Client): The client instance to use for receipt retrieval. timeout (Optional[Union[int, float]]): The total execution timeout (in seconds) for this execution. + validate_status (bool, Optional): The query should automatically validate the transaction status. (default False) Returns: TransactionReceipt: The receipt from the network, containing the status and any entities created by the transaction """ - receipt = self.get_receipt_query().execute(client, timeout) + receipt = self.get_receipt_query(validate_status=validate_status).execute(client, timeout) return receipt def get_record_query(self): diff --git a/tck/__init__.py b/tck/__init__.py index 7e31d063a..f34a652cb 100644 --- a/tck/__init__.py +++ b/tck/__init__.py @@ -1,3 +1,5 @@ """tck package initialization.""" + from .server import start_server + __all__ = ["start_server"] diff --git a/tck/__main__.py b/tck/__main__.py index 44f1394b7..85a432b8d 100644 --- a/tck/__main__.py +++ b/tck/__main__.py @@ -1,9 +1,12 @@ """Main module to start the TCK server.""" + from . import start_server + def main(): """Main function to start the TCK server.""" start_server() + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/tck/errors.py b/tck/errors.py index 12dd4ea9d..781a6758a 100644 --- a/tck/errors.py +++ b/tck/errors.py @@ -1,15 +1,25 @@ - """Error code constants for the TCK server.""" + +from functools import wraps +import logging + +from hiero_sdk_python.exceptions import MaxAttemptsError, PrecheckError, ReceiptStatusError +from hiero_sdk_python.response_code import ResponseCode + +# These constants can be used throughout the codebase to represent specific error conditions. PARSE_ERROR = -32700 INVALID_REQUEST = -32600 METHOD_NOT_FOUND = -32601 INVALID_PARAMS = -32602 INTERNAL_ERROR = -32603 HIERO_ERROR = -32001 -# These constants can be used throughout the codebase to represent specific error conditions. + +logger = logging.getLogger(__name__) + class JsonRpcError(Exception): """Class representing a JSON-RPC error.""" + def __init__(self, code: int, message: str, data=None) -> None: super().__init__(message) self.code = code @@ -18,71 +28,107 @@ def __init__(self, code: int, message: str, data=None) -> None: def to_dict(self): """Convert the error to a dictionary representation.""" - error = { - "code": self.code, - "message": self.message - } + error = {"code": self.code, "message": self.message} if self.data is not None: error["data"] = self.data return error - @classmethod def parse_error(cls, data=None, message: str = "Parse error") -> "JsonRpcError": """Create a Parse Error JSON-RPC error. - + Args: data: Optional error details message: Optional custom error message (defaults to 'Parse error') """ return cls(PARSE_ERROR, message, data) - + @classmethod - def invalid_request_error(cls, data=None, message: str = "Invalid Request") -> "JsonRpcError": + def invalid_request_error( + cls, data=None, message: str = "Invalid Request" + ) -> "JsonRpcError": """Create an Invalid Request JSON-RPC error. - + Args: data: Optional error details message: Optional custom error message (defaults to 'Invalid Request') """ return cls(INVALID_REQUEST, message, data) - + @classmethod - def method_not_found_error(cls, data=None, message: str = "Method not found") -> "JsonRpcError": + def method_not_found_error( + cls, data=None, message: str = "Method not found" + ) -> "JsonRpcError": """Create a Method Not Found JSON-RPC error. - + Args: data: Optional error details message: Optional custom error message (defaults to 'Method not found') """ return cls(METHOD_NOT_FOUND, message, data) - + @classmethod - def invalid_params_error(cls, data=None, message: str = "Invalid params") -> "JsonRpcError": + def invalid_params_error( + cls, data=None, message: str = "Invalid params" + ) -> "JsonRpcError": """Create an Invalid Params JSON-RPC error. - + Args: data: Optional error details message: Optional custom error message (defaults to 'Invalid params') """ return cls(INVALID_PARAMS, message, data) - + @classmethod - def internal_error(cls, data=None, message: str = "Internal error") -> "JsonRpcError": + def internal_error( + cls, data=None, message: str = "Internal error" + ) -> "JsonRpcError": """Create an Internal Error JSON-RPC error. - + Args: data: Optional error details message: Optional custom error message (defaults to 'Internal error') """ return cls(INTERNAL_ERROR, message, data) - + @classmethod def hiero_error(cls, data=None, message: str = "Hiero error") -> "JsonRpcError": """Create a Hiero-specific JSON-RPC error. - + Args: data: Optional error details message: Optional custom error message (defaults to 'Hiero error') """ - return cls(HIERO_ERROR, message, data) \ No newline at end of file + return cls(HIERO_ERROR, message, data) + + +def handle_sdk_errors(func): + @wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except JsonRpcError: + raise + except PrecheckError as e: + logger.error(f"PrecheckError (status: {ResponseCode(e.status).name}, method: {func.__name__})") + raise JsonRpcError.hiero_error( + {"status": ResponseCode(e.status).name} + ) + + except ReceiptStatusError as e: + logger.error(f"ReceiptStatusError (status: {ResponseCode(e.status).name}, method: {func.__name__})") + raise JsonRpcError.hiero_error( + {"status": ResponseCode(e.status).name} + ) + + except MaxAttemptsError as e: + logger.error(f"MaxAttemptsError (method: {func.__name__})") + raise JsonRpcError.hiero_error( + message= str(e) + ) + + except Exception as e: + logger.exception("Unhandled error in RPC handler") + raise JsonRpcError.internal_error(message="Internal error") + + return wrapper diff --git a/tck/handlers/__init__.py b/tck/handlers/__init__.py index 2facc2b42..93dad61ea 100644 --- a/tck/handlers/__init__.py +++ b/tck/handlers/__init__.py @@ -1,9 +1,19 @@ """TCK handlers - auto-import all handler modules.""" # Import registry functions first to make them available -from .registry import get_handler, get_all_handlers, safe_dispatch, validate_request_params +from .registry import ( + get_handler, + get_all_handlers, + safe_dispatch, +) -# Import all handler modules to trigger @register_handler decorators -from . import sdk # setup, reset +# Import all handler modules to trigger @rpc_method decorators +from . import sdk # setup, reset +from . import key +from . import account -__all__ = ["get_handler", "get_all_handlers", "safe_dispatch", "validate_request_params"] \ No newline at end of file +__all__ = [ + "get_handler", + "get_all_handlers", + "safe_dispatch", +] diff --git a/tck/handlers/account.py b/tck/handlers/account.py new file mode 100644 index 000000000..521a6633e --- /dev/null +++ b/tck/handlers/account.py @@ -0,0 +1,68 @@ +from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction +from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.hbar import Hbar +from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt +from tck.handlers.registry import rpc_method +from tck.param.account import CreateAccountParams +from tck.response.account import CreateAccountResponse +from tck.util.client_utils import get_client +from tck.util.constants import DEFAULT_GRPC_TIMEOUT +from tck.util.key_utils import get_key_from_string + + +def _build_create_account_transaction(params: CreateAccountParams) -> AccountCreateTransaction: + transaction = AccountCreateTransaction().set_grpc_deadline(DEFAULT_GRPC_TIMEOUT) + + if params.key is not None: + transaction.set_key_without_alias(get_key_from_string(params.key)) + + if params.initialBalance is not None: + transaction.set_initial_balance(Hbar.from_tinybars(params.initialBalance)) + + if params.receiverSignatureRequired is not None: + transaction.set_receiver_signature_required(params.receiverSignatureRequired) + + if params.maxAutoTokenAssociations is not None: + transaction.set_max_automatic_token_associations( + params.maxAutoTokenAssociations + ) + + if params.stakedAccountId is not None: + transaction.set_staked_account_id(AccountId.from_string(params.stakedAccountId)) + + if params.stakedNodeId is not None: + transaction.set_staked_node_id(params.stakedNodeId) + + if params.declineStakingReward is not None: + transaction.set_decline_staking_reward(params.declineStakingReward) + + if params.memo is not None: + transaction.set_account_memo(params.memo) + + if params.autoRenewPeriod is not None: + transaction.set_auto_renew_period(params.autoRenewPeriod) + + if params.alias is not None: + transaction.set_alias(params.alias) + + return transaction + + +@rpc_method("createAccount") +def create_account(params: CreateAccountParams) -> CreateAccountResponse: + client = get_client(params.sessionId) + + transaction = _build_create_account_transaction(params) + + if params.commonTransactionParams is not None: + params.commonTransactionParams.apply_common_params(transaction, client) + + response = transaction.execute(client, wait_for_receipt=False) + receipt: TransactionReceipt = response.get_receipt(client, validate_status=True) + + account_id = "" + if receipt.status == ResponseCode.SUCCESS: + account_id = str(receipt.account_id) + + return CreateAccountResponse(account_id, ResponseCode(receipt.status).name) diff --git a/tck/handlers/key.py b/tck/handlers/key.py new file mode 100644 index 000000000..a93c596bd --- /dev/null +++ b/tck/handlers/key.py @@ -0,0 +1,142 @@ +from hiero_sdk_python.crypto.key_list import KeyList +from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.crypto.public_key import PublicKey +from hiero_sdk_python.hapi.services.basic_types_pb2 import Key +from tck.errors import JsonRpcError +from tck.handlers.registry import rpc_method +from tck.param.key import KeyGenerationParams +from tck.response.key import KeyGenerationResponse +from tck.util.key_utils import KeyType, get_key_from_string + + +@rpc_method("generateKey") +def generate_key(params: KeyGenerationParams) -> KeyGenerationResponse: + if params.fromKey and params.type not in { + KeyType.ED25519_PUBLIC_KEY, + KeyType.ECDSA_SECP256K1_PUBLIC_KEY, + KeyType.EVM_ADDRESS_KEY, + }: + raise JsonRpcError.invalid_params_error( + "invalid parameters: fromKey is only allowed for " + "ed25519PublicKey, ecdsaSecp256k1PublicKey, or evmAddress types." + ) + + if params.threshold is not None and params.type != KeyType.THRESHOLD_KEY: + raise JsonRpcError.invalid_params_error( + "invalid parameters: threshold is only allowed for thresholdKey types." + ) + + if params.type == KeyType.THRESHOLD_KEY and params.threshold is None: + raise JsonRpcError.invalid_params_error( + "invalid request: threshold is required for generating a ThresholdKey type." + ) + + if params.keys and params.type not in {KeyType.THRESHOLD_KEY, KeyType.LIST_KEY}: + raise JsonRpcError.invalid_params_error( + "invalid parameters: keys are only allowed for keyList or thresholdKey types." + ) + + if params.type in {KeyType.THRESHOLD_KEY, KeyType.LIST_KEY} and not params.keys: + raise JsonRpcError.invalid_params_error( + "invalid parameters: keys must be provided for keyList or thresholdKey types." + ) + + response = KeyGenerationResponse() + response.key = _process_key_recursively( + params=params, response=response, is_list=False + ) + + return response + + +def _handle_private_key( + params: KeyGenerationParams, response: KeyGenerationResponse, is_list: bool +) -> str: + if params.type == KeyType.ED25519_PRIVATE_KEY: + private_key = PrivateKey.generate_ed25519() + else: + private_key = PrivateKey.generate_ecdsa() + + private_key_string = private_key.to_string_der() + + if is_list: + response.privateKeys.append(private_key_string) + + return private_key_string + + +def _handle_public_key( + params: KeyGenerationParams, response: KeyGenerationResponse, is_list: bool +) -> str: + if params.fromKey: + return PrivateKey.from_string(params.fromKey).public_key().to_string_der() + + if params.type == KeyType.ED25519_PUBLIC_KEY: + private_key = PrivateKey.generate_ed25519() + else: + private_key = PrivateKey.generate_ecdsa() + + if is_list: + response.privateKeys.append(private_key.to_string_der()) + + return private_key.public_key().to_string_der() + + +def _handle_key_list( + params: KeyGenerationParams, response: KeyGenerationResponse, is_list: bool +) -> str: + key_list = KeyList() + + for key_params in params.keys: + key_string = _process_key_recursively( + params=key_params, response=response, is_list=True + ) + key_list.add_key(get_key_from_string(key_string)) + + if params.type == KeyType.THRESHOLD_KEY: + key_list.set_threshold(int(params.threshold)) + + return key_list.to_bytes().hex() + + +def _handle_evm_address( + params: KeyGenerationParams, response: KeyGenerationResponse, is_list: bool +) -> str: + if params.fromKey: + key = get_key_from_string(params.fromKey) + + if isinstance(key, PrivateKey): + return str(key.public_key().to_evm_address()) + + elif isinstance(key, PublicKey): + return str(key.to_evm_address()) + + else: + raise JsonRpcError.invalid_params_error( + "invalid parameters: fromKey for evmAddress is not ECDSAsecp256k1." + ) + + return str(PrivateKey.generate_ecdsa().public_key().to_evm_address()) + + +def _process_key_recursively( + params: KeyGenerationParams, response: KeyGenerationResponse, is_list: bool +) -> str: + if params.type in { + KeyType.ED25519_PRIVATE_KEY, + KeyType.ECDSA_SECP256K1_PRIVATE_KEY, + }: + return _handle_private_key(params, response, is_list) + elif params.type in { + KeyType.ED25519_PUBLIC_KEY, + KeyType.ECDSA_SECP256K1_PUBLIC_KEY, + }: + return _handle_public_key(params, response, is_list) + elif params.type in {KeyType.LIST_KEY, KeyType.THRESHOLD_KEY}: + return _handle_key_list(params, response, is_list) + elif params.type == KeyType.EVM_ADDRESS_KEY: + return _handle_evm_address(params, response, is_list) + else: + raise JsonRpcError.invalid_params_error( + "invalid request: key type not recognized." + ) diff --git a/tck/handlers/registry.py b/tck/handlers/registry.py index 87da68145..1960a2c4f 100644 --- a/tck/handlers/registry.py +++ b/tck/handlers/registry.py @@ -1,69 +1,79 @@ -"""Build a flexible registry-based method routing system that can dispatch +"""Build a flexible registry-based method routing system that can dispatch requests to handlers and transform exceptions into JSON-RPC errors.""" + +from dataclasses import asdict +import inspect from typing import Any, Dict, Optional, Union, Callable -from tck.errors import ( - JsonRpcError -) +from tck.errors import JsonRpcError, handle_sdk_errors from tck.protocol import build_json_rpc_error_response -from hiero_sdk_python.exceptions import PrecheckError, ReceiptStatusError, MaxAttemptsError - # A global _HANDLERS dict to store method name -> handler function mappings _HANDLERS: Dict[str, Callable] = {} -def register_handler(method_name: str): + +def rpc_method(method_name: str): """Register a handler function for a given method name.""" + def decorator(func: Callable) -> Callable: """Decorator to register a handler function for a given method name.""" - _HANDLERS[method_name] = func + _HANDLERS[method_name] = handle_sdk_errors(func) return func + return decorator + def get_handler(method_name: str) -> Optional[Callable]: """Retrieve a handler by method name.""" return _HANDLERS.get(method_name) + def get_all_handlers() -> Dict[str, Callable]: """Get all registered handlers.""" return _HANDLERS.copy() -def dispatch(method_name: str, params: Any, session_id: Optional[str]) -> Any: +def dispatch(method_name: str, params: Any) -> Any: """Dispatch the request to the appropriate handler based on method_name.""" handler = get_handler(method_name) - + if handler is None: - raise JsonRpcError.method_not_found_error(message=f'Method not found: {method_name}') + raise JsonRpcError.method_not_found_error( + message=f"Method not found: {method_name}" + ) + try: - if session_id is not None: - return handler(params, session_id) - return handler(params) + signature = inspect.signature(handler) + parameters = list(signature.parameters.values()) + param_type = parameters[0].annotation + + try: + params = param_type.parse_json_params(params) + except (TypeError, ValueError) as e: + raise JsonRpcError.invalid_params_error(data=str(e)) from e + + result = handler(params) + + return parse_result(result) + except JsonRpcError: raise - except (PrecheckError, ReceiptStatusError, MaxAttemptsError) as e: - raise JsonRpcError.hiero_error(data=str(e)) from e except Exception as e: raise JsonRpcError.internal_error(data=str(e)) from e -def safe_dispatch(method_name: str, - params: Any, - session_id: Optional[str], - request_id: Optional[Union[str, int]]) -> Union[Any, Dict[str, Any]]: + +def safe_dispatch( + method_name: str, params: Any, request_id: Optional[Union[str, int]] +) -> Union[Any, Dict[str, Any]]: """Safely dispatch the request and handle exceptions.""" try: - return dispatch(method_name, params, session_id) + return dispatch(method_name, params) except JsonRpcError as e: return build_json_rpc_error_response(e, request_id) - except Exception as e: # Defensive fallback for any uncaught exceptions + except Exception as e: error = JsonRpcError.internal_error(data=str(e)) return build_json_rpc_error_response(error, request_id) -def validate_request_params(params: Any, required_fields: Dict[str, type]) -> None: - """Validate that required fields are present in params with correct types.""" - if not isinstance(params, dict): - raise JsonRpcError.invalid_params_error(message='Invalid params: expected object') - - for field, field_type in required_fields.items(): - if field not in params or not isinstance(params[field], field_type): - raise JsonRpcError.invalid_params_error(message=f'Invalid params: missing or incorrect type for {field}') +def parse_result(result: Any) -> dict: + """Parse the result from the methods to dict containing non none key:values""" + return {k: v for k, v in asdict(result).items() if v is not None} diff --git a/tck/handlers/sdk.py b/tck/handlers/sdk.py index 1bef18e06..964cd37d9 100644 --- a/tck/handlers/sdk.py +++ b/tck/handlers/sdk.py @@ -1,121 +1,56 @@ -from typing import Any, Dict, Optional, List from hiero_sdk_python.node import _Node from hiero_sdk_python import Client, AccountId, PrivateKey, Network -from tck.handlers.registry import register_handler, validate_request_params -from tck.client_manager import store_client, remove_client -from tck.errors import JsonRpcError - - -def _create_node_objects(node_addresses: List[str]) -> List[_Node]: - """Convert node addresses to _Node objects with generated AccountIds. - Args: - node_addresses: List of node address strings - - Returns: - List of _Node objects with sequential AccountIds starting from 0.0.3 - """ - node_objects = [] - for idx, node_address in enumerate(node_addresses): - account_id = AccountId(0, 0, 3 + idx) - node_obj = _Node(account_id, node_address, None) - node_objects.append(node_obj) - return node_objects - - -def _create_custom_network_client(network_config: Dict[str, Any]) -> Client: - """Create a client with custom network configuration. - Args: - network_config: Dictionary containing 'nodes' key with list of node addresses - - Returns: - Configured Client instance - - Raises: - JsonRpcError: If nodes configuration is invalid - """ - nodes = network_config.get('nodes') - if not isinstance(nodes, list) or len(nodes) == 0 or not all(isinstance(node, str) for node in nodes): - raise JsonRpcError.invalid_params_error(message='Invalid params: nodes must be a non-empty list of strings') - - node_objects = _create_node_objects(nodes) - network = Network(nodes=node_objects) - return Client(network) - - -def _create_client(network_param: Optional[Any]) -> Client: - """Create and return the appropriate Client based on network parameter. - Args: - network_param: Network specification (None/'testnet' for testnet, 'mainnet', or custom dict) - - Returns: - Configured Client instance - - Raises: - JsonRpcError: If network specification is invalid - """ - if network_param is None or network_param == 'testnet': - return Client.for_testnet() - - if network_param == 'mainnet': - return Client.for_mainnet() - - if isinstance(network_param, dict) and 'nodes' in network_param: - return _create_custom_network_client(network_param) - - raise JsonRpcError.invalid_params_error(message='Invalid params: unknown network specification') - - -def _parse_operator_credentials(params: Dict[str, Any]) -> tuple[AccountId, PrivateKey]: - """Parse and validate operator credentials from parameters. - - Args: - params: Request parameters containing operatorAccountId and operatorPrivateKey - - Returns: - Tuple of (AccountId, PrivateKey) - Raises: - JsonRpcError: If credentials cannot be parsed - """ - operator_account_id_str = params['operatorAccountId'] - operator_private_key_str = params['operatorPrivateKey'] +from tck.handlers.registry import rpc_method +from tck.param.base import BaseParams +from tck.param.sdk import SetupParams +from tck.response.sdk import SetupResponse +from tck.util.client_utils import get_client, remove_client, store_client + + +@rpc_method("setup") +def setup_handler(params: SetupParams) -> SetupResponse: + operator_account_id = AccountId.from_string(params.operatorAccountId) + operator_private_key = PrivateKey.from_string(params.operatorPrivateKey) + + if params.nodeIp and params.nodeAccountId and params.mirrorNetworkIp: + client = Client() + client.network = Network( + nodes=[ + _Node(AccountId.from_string(params.nodeAccountId), params.nodeIp, None) + ], + mirror_address=params.mirrorNetworkIp, + ) + client.set_operator(operator_account_id, operator_private_key) + + client_type = "custom" + store_client(params.sessionId, client) + else: + client = Client.for_testnet() + client_type = "testnet" + store_client(params.sessionId, client) + + client = get_client(params.sessionId) + client.set_operator(operator_account_id, operator_private_key) - try: - operator_account_id = AccountId.from_string(operator_account_id_str) - operator_private_key = PrivateKey.from_string(operator_private_key_str) - except Exception as e: - raise JsonRpcError.invalid_params_error(message=f'Invalid params: invalid operatorAccountId/operatorPrivateKey format - {str(e)}') from e + return SetupResponse(f"Successfully setup {client_type} client") - return operator_account_id, operator_private_key +@rpc_method("setOperator") +def set_operator(params: SetupParams) -> SetupResponse: + operator_account_id = AccountId.from_string(params.operatorAccountId) + operator_private_key = PrivateKey.from_string(params.operatorPrivateKey) -@register_handler("setup") -def setup_handler(params: Dict[str, Any], session_id: Optional[str] = None) -> Dict[str, Any]: - """Setup handler to initialize SDK clients with operator credentials and network configuration.""" - # Validate required parameters - required_fields = { - 'operatorAccountId': str, - 'operatorPrivateKey': str - } - validate_request_params(params, required_fields) + client = get_client(params.sessionId) + client.set_operator(operator_account_id, operator_private_key) - # Parse operator credentials - operator_account_id, operator_private_key = _parse_operator_credentials(params) + return SetupResponse("") - # Create client based on network configuration - client = _create_client(params.get('network')) - client.set_operator(operator_account_id, operator_private_key) - # Store the initialized client - effective_session_id = session_id or "default" - store_client(effective_session_id, client) +@rpc_method("reset") +def reset_handler(params: BaseParams) -> SetupResponse: + client = remove_client(params.sessionId) - return {"status": "success", "sessionId": effective_session_id} + if client is not None: + client.close() -@register_handler("reset") -def reset_handler(params: Dict[str, Any], session_id: Optional[str] = None) -> Dict[str, Any]: - """Reset handler to close connections and clear client state.""" - target_session_id = params.get('sessionId') or session_id - if target_session_id is None: - target_session_id = "default" - remove_client(target_session_id) - return {"status": "reset completed"} + return SetupResponse("Successfully reset client") diff --git a/tck/param/account.py b/tck/param/account.py new file mode 100644 index 000000000..5682e41a7 --- /dev/null +++ b/tck/param/account.py @@ -0,0 +1,40 @@ +from dataclasses import dataclass + +from tck.param.base import BaseTransactionParams +from tck.util.param_utils import ( + parse_common_transaction_params, + parse_session_id, + to_bool, + to_int, +) + + +@dataclass +class CreateAccountParams(BaseTransactionParams): + key: str | None = None + initialBalance: int | None = None + receiverSignatureRequired: bool | None = None + maxAutoTokenAssociations: int | None = None + stakedAccountId: str | None = None + stakedNodeId: int | None = None + declineStakingReward: bool | None = None + memo: str | None = None + autoRenewPeriod: int | None = None + alias: str | None = None + + @classmethod + def parse_json_params(cls, params: dict) -> "CreateAccountParams": + return cls( + key=params.get("key"), + initialBalance=to_int(params.get("initialBalance")), + receiverSignatureRequired=to_bool(params.get("receiverSignatureRequired")), + maxAutoTokenAssociations=to_int(params.get("maxAutoTokenAssociations")), + stakedAccountId=params.get("stakedAccountId"), + stakedNodeId=to_int(params.get("stakedNodeId")), + declineStakingReward=to_bool(params.get("declineStakingReward")), + memo=params.get("memo"), + autoRenewPeriod=to_int(params.get("autoRenewPeriod")), + alias=params.get("alias"), + sessionId=parse_session_id(params), + commonTransactionParams=parse_common_transaction_params(params), + ) diff --git a/tck/param/base.py b/tck/param/base.py new file mode 100644 index 000000000..4ca35089e --- /dev/null +++ b/tck/param/base.py @@ -0,0 +1,18 @@ +from dataclasses import dataclass + +from tck.param.common import CommonTransactionParams +from tck.util.param_utils import parse_session_id + + +@dataclass +class BaseParams: + sessionId: str = None + + @classmethod + def parse_json_params(cls, params: dict) -> "BaseParams": + return cls(parse_session_id(params)) + + +@dataclass +class BaseTransactionParams(BaseParams): + commonTransactionParams: CommonTransactionParams | None = None diff --git a/tck/param/common.py b/tck/param/common.py new file mode 100644 index 000000000..b9e3da87c --- /dev/null +++ b/tck/param/common.py @@ -0,0 +1,56 @@ +from dataclasses import dataclass + +from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.client.client import Client +from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.transaction.transaction import Transaction +from hiero_sdk_python.transaction.transaction_id import TransactionId +from tck.util.param_utils import to_bool, to_int + + +@dataclass +class CommonTransactionParams: + transactionId: str | None = None + maxTransactionFee: int | None = None + validTransactionDuration: int | None = None + memo: str | None = None + regenerateTransactionId: bool | None = None + signers: list[str] | None = None + + @classmethod + def parse_json_params(cls, params: dict) -> "CommonTransactionParams": + return cls( + transactionId=params.get("transactionId"), + maxTransactionFee=to_int(params.get("maxTransactionFee")), + validTransactionDuration=to_int(params.get("validTransactionDuration")), + memo=params.get("memo"), + regenerateTransactionId=to_bool(params.get("regenerateTransactionId")), + signers=[signer for signer in params.get("signers", [])], + ) + + def apply_common_params(self, transaction: Transaction, client: Client) -> None: + """Apply commonTransactionParams to a given transaction.""" + if self.transactionId is not None: + try: + transaction.set_transaction_id( + TransactionId.from_string(self.transactionId) + ) + except: + transaction.set_transaction_id( + TransactionId.generate(AccountId.from_string(self.transactionId)) + ) + + # TODO add a max_transaction_fee sdk missing func + + if self.validTransactionDuration is not None: + transaction.set_transaction_valid_duration(self.validTransactionDuration) + + if self.memo is not None: + transaction.set_transaction_memo(self.memo) + + # TODO add regenerate_transaction_id sdk missing func + + if self.signers: + transaction.freeze_with(client) + for signer in self.signers: + transaction.sign(PrivateKey.from_string(signer)) diff --git a/tck/param/key.py b/tck/param/key.py new file mode 100644 index 000000000..cf7998699 --- /dev/null +++ b/tck/param/key.py @@ -0,0 +1,25 @@ +from dataclasses import dataclass +from typing import List + +from tck.util.key_utils import KeyType + + +@dataclass +class KeyGenerationParams: + type: KeyType = None + fromKey: str | None = None + threshold: int | None = None + keys: List["KeyGenerationParams"] | None = None + + @classmethod + def parse_json_params(cls, params: dict) -> "KeyGenerationParams": + key_list = params.get("keys") or [] + + return cls( + type=( + KeyType.from_string(params.get("type")) if params.get("type") else None + ), + fromKey=params.get("fromKey"), + threshold=params.get("threshold"), + keys=[cls.parse_json_params(k) for k in key_list], + ) diff --git a/tck/param/sdk.py b/tck/param/sdk.py new file mode 100644 index 000000000..b9ce539aa --- /dev/null +++ b/tck/param/sdk.py @@ -0,0 +1,24 @@ +from dataclasses import dataclass + +from tck.param.base import BaseParams +from tck.util.param_utils import parse_session_id + + +@dataclass +class SetupParams(BaseParams): + operatorAccountId: str = None + operatorPrivateKey: str = None + nodeIp: str | None = None + nodeAccountId: str | None = None + mirrorNetworkIp: str | None = None + + @classmethod + def parse_json_params(cls, params: dict) -> "SetupParams": + return cls( + operatorAccountId=params.get("operatorAccountId"), + operatorPrivateKey=params.get("operatorPrivateKey"), + nodeIp=params.get("nodeIp"), + nodeAccountId=params.get("nodeAccountId"), + mirrorNetworkIp=params.get("mirrorNetworkIp"), + sessionId=parse_session_id(params), + ) diff --git a/tck/protocol.py b/tck/protocol.py index dcbffdcb4..a5e5a938d 100644 --- a/tck/protocol.py +++ b/tck/protocol.py @@ -7,7 +7,7 @@ def _normalize_request_input(request_in: Any) -> Union[Dict[str, Any], JsonRpcEr """Normalize request input to a dictionary Args: request_in: Either a JSON string or a pre-parsed dict - + Returns: Parsed dictionary or JsonRpcError if parsing fails """ @@ -34,17 +34,17 @@ def _validate_json_rpc_structure(request: Dict[str, Any]) -> Optional[JsonRpcErr if not isinstance(request, dict): return JsonRpcError.invalid_request_error() - if request.get('jsonrpc') != '2.0': + if request.get("jsonrpc") != "2.0": return JsonRpcError.invalid_request_error() - if 'id' not in request: + if "id" not in request: return JsonRpcError.invalid_request_error() - method = request.get('method') + method = request.get("method") if not isinstance(method, str): return JsonRpcError.invalid_request_error() - params = request.get('params', {}) + params = request.get("params", {}) if not (isinstance(params, (dict, list)) or params is None): return JsonRpcError.invalid_request_error() @@ -55,12 +55,12 @@ def _extract_session_id(params: Any) -> Optional[str]: """Extract session ID from params if present. Args: params: Request parameters (dict, list, or None) - + Returns: Session ID if present, None otherwise """ if isinstance(params, dict): - return params.get('sessionId') + return params.get("sessionId") return None @@ -80,34 +80,39 @@ def parse_json_rpc_request(request_in: Any) -> Union[Dict[str, Any], JsonRpcErro return validation_error # Extract session ID from params - params = request.get('params', {}) + params = request.get("params", {}) session_id = _extract_session_id(params) return { - 'jsonrpc': '2.0', - 'method': request['method'], - 'params': params, - 'id': request['id'], - 'sessionId': session_id + "jsonrpc": "2.0", + "method": request["method"], + "params": params, + "id": request["id"], + "sessionId": session_id, } -def build_json_rpc_success_response(result: Any, request_id: Optional[Union[str, int]]) -> Dict[str, Any]: + +def build_json_rpc_success_response( + result: Any, request_id: Optional[Union[str, int]] +) -> Dict[str, Any]: """Build a JSON-RPC 2.0 success response.""" response = { - 'jsonrpc': '2.0', - 'id': request_id, - 'result': result, + "jsonrpc": "2.0", + "id": request_id, + "result": result, } return response -def build_json_rpc_error_response(error: JsonRpcError, - request_id: Optional[Union[str, int]]) -> Dict[str, Any]: + +def build_json_rpc_error_response( + error: JsonRpcError, request_id: Optional[Union[str, int]] +) -> Dict[str, Any]: """Build a JSON-RPC 2.0 error response.""" error_obj = error.to_dict() - + response = { - 'jsonrpc': '2.0', - 'id': request_id, - 'error': error_obj, + "jsonrpc": "2.0", + "id": request_id, + "error": error_obj, } return response diff --git a/tck/response/account.py b/tck/response/account.py new file mode 100644 index 000000000..8cd41a469 --- /dev/null +++ b/tck/response/account.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass + + +@dataclass +class CreateAccountResponse: + accountId: str | None = None + status: str | None = None diff --git a/tck/response/key.py b/tck/response/key.py new file mode 100644 index 000000000..4413ef89d --- /dev/null +++ b/tck/response/key.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass, field + + +@dataclass +class KeyGenerationResponse: + key: str = None + privateKeys: list[str] = field(default_factory=list) diff --git a/tck/response/sdk.py b/tck/response/sdk.py new file mode 100644 index 000000000..3bfa835ab --- /dev/null +++ b/tck/response/sdk.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass + + +@dataclass +class SetupResponse: + message: str = None + status: str = None + + def __init__(self, message: str): + self.message = message + self.status = "SUCCESS" diff --git a/tck/server.py b/tck/server.py index fa195ddd4..6d57fc9a1 100644 --- a/tck/server.py +++ b/tck/server.py @@ -1,66 +1,79 @@ """TCK Server implementation using Flask.""" + import os from dataclasses import dataclass, field import logging from flask import Flask, jsonify, request from tck.errors import JsonRpcError from tck.handlers import safe_dispatch -from tck.protocol import build_json_rpc_error_response, build_json_rpc_success_response, parse_json_rpc_request +from tck.protocol import ( + build_json_rpc_error_response, + build_json_rpc_success_response, + parse_json_rpc_request, +) + @dataclass class ServerConfig: """Configuration for the TCK server.""" + host: str = field(default_factory=lambda: os.getenv("TCK_HOST", "localhost")) - port: int = field(default_factory=lambda: _parse_port(os.getenv("TCK_PORT", "8544"))) + port: int = field( + default_factory=lambda: _parse_port(os.getenv("TCK_PORT", "8544")) + ) + def _parse_port(port_str: str) -> int: try: port = int(port_str) except ValueError as exc: - raise ValueError(f"TCK_PORT must be a valid integer, got: '{port_str}'") from exc + raise ValueError( + f"TCK_PORT must be a valid integer, got: '{port_str}'" + ) from exc if not (1 <= port <= 65535): raise ValueError(f"TCK_PORT must be between 1 and 65535, got: {port}") return port + app = Flask(__name__) logger = logging.getLogger(__name__) -@app.route("/", methods=['POST']) + +@app.route("/", methods=["POST"]) def json_rpc_endpoint(): """JSON-RPC 2.0 endpoint to handle requests.""" - if request.mimetype != 'application/json': - error = JsonRpcError.parse_error(message='Parse error: Content-Type must be application/json') + if request.mimetype != "application/json": + error = JsonRpcError.parse_error( + message="Parse error: Content-Type must be application/json" + ) return jsonify(build_json_rpc_error_response(error, None)) + try: request_json = request.get_json(force=True) except Exception: - # Malformed JSON - return parse error error = JsonRpcError.parse_error() return jsonify(build_json_rpc_error_response(error, None)) - + # Parse and validate the JSON-RPC request parsed_request = parse_json_rpc_request(request_json) if isinstance(parsed_request, JsonRpcError): # Use request id if available, else None per JSON-RPC 2.0 spec - request_id = request_json.get('id') if isinstance(request_json, dict) else None + request_id = request_json.get("id") if isinstance(request_json, dict) else None return jsonify(build_json_rpc_error_response(parsed_request, request_id)) + method_name = parsed_request["method"] + params = parsed_request["params"] + request_id = parsed_request["id"] - method_name = parsed_request['method'] - params = parsed_request['params'] - request_id = parsed_request['id'] - session_id = parsed_request.get('sessionId') - - # Safely dispatch the request to the appropriate handler - response = safe_dispatch(method_name, params, session_id, request_id) + response = safe_dispatch(method_name, params, request_id) # If the response is already an error response, return it directly - if isinstance(response, dict) and 'jsonrpc' in response and 'error' in response: + if isinstance(response, dict) and "jsonrpc" in response and "error" in response: return jsonify(response) - - # Build and return the success response + return jsonify(build_json_rpc_success_response(response, request_id)) + def start_server(config: ServerConfig | None = None): """Start the JSON-RPC server using Flask.""" if config is None: @@ -69,6 +82,5 @@ def start_server(config: ServerConfig | None = None): app.run(host=config.host, port=config.port) - if __name__ == "__main__": - start_server() \ No newline at end of file + start_server() diff --git a/tck/client_manager.py b/tck/util/client_utils.py similarity index 55% rename from tck/client_manager.py rename to tck/util/client_utils.py index 87753f833..b8ab1c8ef 100644 --- a/tck/client_manager.py +++ b/tck/util/client_utils.py @@ -1,26 +1,28 @@ -from typing import Optional, Dict from hiero_sdk_python import Client import threading -_clients: Dict[str, Client] = {} -_lock = threading.Lock() +_CLIENTS: dict[str, Client] = {} +_LOCK = threading.Lock() + def store_client(session_id: str, client: Client) -> None: """Store a client instance associated with a session ID.""" - with _lock: - old_client = _clients.get(session_id) - _clients[session_id] = client + with _LOCK: + old_client = _CLIENTS.get(session_id) + _CLIENTS[session_id] = client if old_client is not None: old_client.close() -def get_client(session_id: str) -> Optional[Client]: + +def get_client(session_id: str) -> Client | None: """Retrieve a client instance by session ID.""" - with _lock: - return _clients.get(session_id) + with _LOCK: + return _CLIENTS.get(session_id) + def remove_client(session_id: str) -> None: """Remove and close the client instance associated with a session ID.""" - with _lock: - client = _clients.pop(session_id, None) + with _LOCK: + client = _CLIENTS.pop(session_id, None) if client is not None: client.close() diff --git a/tck/util/constants.py b/tck/util/constants.py new file mode 100644 index 000000000..f200c1a67 --- /dev/null +++ b/tck/util/constants.py @@ -0,0 +1 @@ +DEFAULT_GRPC_TIMEOUT=30 diff --git a/tck/util/key_utils.py b/tck/util/key_utils.py new file mode 100644 index 000000000..dbc9eef84 --- /dev/null +++ b/tck/util/key_utils.py @@ -0,0 +1,46 @@ +from enum import Enum + +from hiero_sdk_python.crypto.key import Key +from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.crypto.public_key import PublicKey + + +class KeyType(Enum): + ED25519_PRIVATE_KEY = "ed25519PrivateKey" + ED25519_PUBLIC_KEY = "ed25519PublicKey" + ECDSA_SECP256K1_PRIVATE_KEY = "ecdsaSecp256k1PrivateKey" + ECDSA_SECP256K1_PUBLIC_KEY = "ecdsaSecp256k1PublicKey" + LIST_KEY = "keyList" + THRESHOLD_KEY = "thresholdKey" + EVM_ADDRESS_KEY = "evmAddress" + + @classmethod + def from_string(cls, key_type_str: str): + """Helper to get KeyType from string.""" + for key_type in cls: + if key_type.value == key_type_str: + return key_type + + raise ValueError(f"Unknown key type: {key_type_str}") + + +def get_key_from_string(key_string: str) -> Key: + """Helper to convert the str value to Key.""" + key_bytes = bytes.fromhex(key_string) + + try: + return Key.from_bytes(key_bytes) + except Exception: + pass + + try: + return PublicKey.from_string_der(key_string) + except Exception: + pass + + try: + return PrivateKey.from_string_der(key_string) + except Exception: + pass + + raise ValueError("Invalid key string") diff --git a/tck/util/param_utils.py b/tck/util/param_utils.py new file mode 100644 index 000000000..0be500259 --- /dev/null +++ b/tck/util/param_utils.py @@ -0,0 +1,36 @@ +def parse_session_id(params: dict) -> str: + """Parse sessionId from the json rpc params.""" + session_id = params.get("sessionId") + + if isinstance(session_id, str) and session_id != "": + return session_id + + raise ValueError("sessionId is required and must be a non-empty string") + + +def parse_common_transaction_params(params: dict): + """Parse the commonTransactionParams form json the rpc params.""" + from tck.param.common import CommonTransactionParams + + common_params = params.get("commonTransactionParams") + if common_params is None: + return None + + return CommonTransactionParams.parse_json_params( + params.get("commonTransactionParams") + ) + + +def to_int(value) -> int | None: + """Helper to convert value to int.""" + try: + return int(value) + except (TypeError, ValueError): + return None + + +def to_bool(value) -> bool | None: + """Helper to convert value to bool.""" + if isinstance(value, str): + return value.lower() == "true" + return bool(value) if value is not None else None diff --git a/tests/fuzz/__init__.py b/tests/fuzz/__init__.py new file mode 100644 index 000000000..2e839778b --- /dev/null +++ b/tests/fuzz/__init__.py @@ -0,0 +1 @@ +# with <3 from Anto \ No newline at end of file diff --git a/tests/fuzz/conftest.py b/tests/fuzz/conftest.py new file mode 100644 index 000000000..60048d26c --- /dev/null +++ b/tests/fuzz/conftest.py @@ -0,0 +1,32 @@ +"""Shared Hypothesis setup, fixtures, and compatibility re-exports for fuzz tests.""" + +from tests.fuzz.support.classes import ( + AccountIdAliasCase, + ContractValueCase, + EntityIdCase, + HbarConstructorCase, + HbarStringCase, + InvalidContractValueCase, +) +from tests.fuzz.support.profiles import load_hypothesis_profile +from tests.fuzz.support.registry import ( + FUZZ_STRATEGIES, + fuzz_strategies_fixture, + get_strategy, + get_strategy_fixture, +) + +load_hypothesis_profile() + +__all__ = [ + "AccountIdAliasCase", + "ContractValueCase", + "EntityIdCase", + "FUZZ_STRATEGIES", + "HbarConstructorCase", + "HbarStringCase", + "InvalidContractValueCase", + "fuzz_strategies_fixture", + "get_strategy", + "get_strategy_fixture", +] diff --git a/tests/fuzz/support/__init__.py b/tests/fuzz/support/__init__.py new file mode 100644 index 000000000..fe7052834 --- /dev/null +++ b/tests/fuzz/support/__init__.py @@ -0,0 +1,20 @@ +from tests.fuzz.support.classes import ( + AccountIdAliasCase, + ContractValueCase, + EntityIdCase, + HbarConstructorCase, + HbarStringCase, + InvalidContractValueCase, +) +from tests.fuzz.support.registry import FUZZ_STRATEGIES, get_strategy + +__all__ = [ + "AccountIdAliasCase", + "ContractValueCase", + "EntityIdCase", + "FUZZ_STRATEGIES", + "HbarConstructorCase", + "HbarStringCase", + "InvalidContractValueCase", + "get_strategy", +] diff --git a/tests/fuzz/support/classes.py b/tests/fuzz/support/classes.py new file mode 100644 index 000000000..28e076888 --- /dev/null +++ b/tests/fuzz/support/classes.py @@ -0,0 +1,61 @@ +from dataclasses import dataclass +from decimal import Decimal +from typing import Any + +from hiero_sdk_python import HbarUnit + + +@dataclass(frozen=True) +class EntityIdCase: + """A parsed entity ID expectation for public string parsers.""" + + text: str + shard: int + realm: int + value: int + checksum: str | None = None + + +@dataclass(frozen=True) +class AccountIdAliasCase: + """A valid account alias or EVM-address input.""" + + text: str + shard: int + realm: int + alias_hex: str | None = None + evm_hex: str | None = None + + +@dataclass(frozen=True) +class HbarStringCase: + """A valid public Hbar string and its exact tinybar value.""" + + text: str + tinybars: int + + +@dataclass(frozen=True) +class HbarConstructorCase: + """A valid Hbar constructor input and its exact tinybar value.""" + + amount: int | float | Decimal + unit: HbarUnit + tinybars: int + + +@dataclass(frozen=True) +class ContractValueCase: + """A valid contract parameter case routed to an explicit public add_* method.""" + + method_name: str + value: Any + + +@dataclass(frozen=True) +class InvalidContractValueCase: + """An invalid contract parameter case with a precise expected exception.""" + + method_name: str + value: Any + expected_exception: type[BaseException] diff --git a/tests/fuzz/support/helpers.py b/tests/fuzz/support/helpers.py new file mode 100644 index 000000000..fa7453133 --- /dev/null +++ b/tests/fuzz/support/helpers.py @@ -0,0 +1,65 @@ +from decimal import Decimal + +from hypothesis import strategies as st +from hypothesis.strategies import SearchStrategy + +from hiero_sdk_python import AccountId, HbarUnit, PrivateKey, TransactionId, TransferTransaction + +from tests.fuzz.support.classes import HbarConstructorCase, HbarStringCase + + +def sized_hex(byte_length: int) -> SearchStrategy[str]: + """Return a fixed-length hex string strategy.""" + return st.binary(min_size=byte_length, max_size=byte_length).map(bytes.hex) + + +def with_optional_0x(hex_strategy: SearchStrategy[str]) -> SearchStrategy[str]: + """Allow a hex string with or without the `0x` prefix.""" + return st.one_of(hex_strategy, hex_strategy.map(lambda value: f"0x{value}")) + + +def decimal_string(value: Decimal) -> str: + """Format a Decimal without trailing zeros.""" + text = format(value, "f") + if "." in text: + text = text.rstrip("0").rstrip(".") + return text or "0" + + +def hbar_string_case(unit: HbarUnit, tinybars: int) -> HbarStringCase: + """Build a valid Hbar string case from an exact tinybar amount.""" + amount = Decimal(tinybars) / Decimal(unit.tinybar) + if unit == HbarUnit.HBAR: + return HbarStringCase(text=decimal_string(amount), tinybars=tinybars) + return HbarStringCase(text=f"{decimal_string(amount)} {unit.symbol}", tinybars=tinybars) + + +def hbar_constructor_case(unit: HbarUnit, tinybars: int) -> HbarConstructorCase: + """Build a valid Hbar constructor case from an exact tinybar amount.""" + if unit == HbarUnit.TINYBAR: + amount: int | float | Decimal = tinybars + else: + amount = Decimal(tinybars) / Decimal(unit.tinybar) + return HbarConstructorCase(amount=amount, unit=unit, tinybars=tinybars) + + +def build_valid_transaction_bytes() -> tuple[bytes, bytes]: + """Build one valid unsigned and one valid signed transaction payload.""" + operator_id = AccountId.from_string("0.0.1234") + node_id = AccountId.from_string("0.0.3") + receiver_id = AccountId.from_string("0.0.5678") + + tx = ( + TransferTransaction() + .add_hbar_transfer(operator_id, -100_000_000) + .add_hbar_transfer(receiver_id, 100_000_000) + ) + tx.transaction_id = TransactionId.generate(operator_id) + tx.node_account_id = node_id + tx.freeze() + unsigned_bytes = tx.to_bytes() + + signed_tx = TransferTransaction.from_bytes(unsigned_bytes) + signed_tx.sign(PrivateKey.from_string_ed25519("02" * 32)) + signed_bytes = signed_tx.to_bytes() + return unsigned_bytes, signed_bytes diff --git a/tests/fuzz/support/profiles.py b/tests/fuzz/support/profiles.py new file mode 100644 index 000000000..5554550f2 --- /dev/null +++ b/tests/fuzz/support/profiles.py @@ -0,0 +1,31 @@ +import os + +from hypothesis import HealthCheck, settings + + +def load_hypothesis_profile() -> None: + """Register and load the active Hypothesis profile.""" + settings.register_profile( + "ci", + settings( + derandomize=True, + max_examples=300, + deadline=750, + suppress_health_check=[HealthCheck.too_slow], + ), + ) + settings.register_profile( + "local", + settings( + derandomize=False, + max_examples=1000, + deadline=None, + ), + ) + + requested = os.getenv("HYPOTHESIS_PROFILE") + if requested: + settings.load_profile(requested) + return + + settings.load_profile("ci" if os.getenv("CI") else "local") diff --git a/tests/fuzz/support/registry.py b/tests/fuzz/support/registry.py new file mode 100644 index 000000000..a5b93c1f7 --- /dev/null +++ b/tests/fuzz/support/registry.py @@ -0,0 +1,558 @@ +import string +from decimal import Decimal +from typing import Any, NamedTuple + +import pytest +from eth_abi.exceptions import EncodingTypeError, ValueOutOfBounds +from hypothesis import strategies as st +from hypothesis.strategies import SearchStrategy + +from hiero_sdk_python import HbarUnit, PrivateKey + +from tests.fuzz.support.classes import ( + AccountIdAliasCase, + ContractValueCase, + EntityIdCase, + InvalidContractValueCase, +) +from tests.fuzz.support.helpers import ( + build_valid_transaction_bytes, + hbar_constructor_case, + hbar_string_case, + sized_hex, + with_optional_0x, +) + +MAX_I64 = 2**63 - 1 +MIN_I64 = -(2**63) + + +class _IdentifierPrimitives(NamedTuple): + """Shared scalar strategies reused by the identifier-related builders.""" + + shard: SearchStrategy[int] + realm: SearchStrategy[int] + entity_num: SearchStrategy[int] + checksum: SearchStrategy[str] + + +class _KeySampleValues(NamedTuple): + """Precomputed valid key encodings reused across identifier and key strategies.""" + + ed_private_raw: str + ecdsa_private_raw: str + ed_private_der: str + ecdsa_private_der: str + ed_public_raw: str + ecdsa_public_compressed: str + ecdsa_public_uncompressed: str + ed_public_der: str + ecdsa_public_der: str + ed25519_alias_hex: str + ecdsa_alias_hex: str + evm_hex: str + + +def _build_identifier_primitives() -> _IdentifierPrimitives: + """Build the shared scalar strategies used to compose Hedera-style identifiers.""" + + return _IdentifierPrimitives( + shard=st.integers(min_value=0, max_value=4096), + realm=st.integers(min_value=0, max_value=4096), + entity_num=st.integers(min_value=0, max_value=MAX_I64), + checksum=st.text(alphabet=string.ascii_lowercase, min_size=5, max_size=5), + ) + + +def _build_key_sample_values() -> _KeySampleValues: + """Build canonical valid key samples once so all dependent strategies stay consistent.""" + + ed_private_raw = "01" * 32 + ecdsa_private_raw = "00" * 31 + "01" + + ed_private_key = PrivateKey.from_string_ed25519(ed_private_raw) + ecdsa_private_key = PrivateKey.from_string_ecdsa(ecdsa_private_raw) + ed_public_key = ed_private_key.public_key() + ecdsa_public_key = ecdsa_private_key.public_key() + + return _KeySampleValues( + ed_private_raw=ed_private_raw, + ecdsa_private_raw=ecdsa_private_raw, + ed_private_der=ed_private_key.to_bytes_der().hex(), + ecdsa_private_der=ecdsa_private_key.to_bytes_der().hex(), + ed_public_raw=ed_public_key.to_bytes_raw().hex(), + ecdsa_public_compressed=ecdsa_public_key.to_bytes_ecdsa().hex(), + ecdsa_public_uncompressed=ecdsa_public_key.to_bytes_ecdsa(compressed=False).hex(), + ed_public_der=ed_public_key.to_bytes_der().hex(), + ecdsa_public_der=ecdsa_public_key.to_bytes_der().hex(), + ed25519_alias_hex=ed_public_key.to_bytes_raw().hex(), + ecdsa_alias_hex=ecdsa_public_key.to_bytes_ecdsa().hex(), + evm_hex="abcdef0123456789abcdef0123456789abcdef01", + ) + + +def _build_entity_id_strategies( + primitives: _IdentifierPrimitives, +) -> dict[str, SearchStrategy[Any]]: + """Build valid dotted and checksum entity ID strategies with the current numeric bounds.""" + + entity_id_valid_dotted = st.builds( + lambda shard_value, realm_value, num_value: EntityIdCase( + text=f"{shard_value}.{realm_value}.{num_value}", + shard=shard_value, + realm=realm_value, + value=num_value, + ), + primitives.shard, + primitives.realm, + primitives.entity_num, + ) + entity_id_valid_checksum = st.builds( + lambda base, checksum_value: EntityIdCase( + text=f"{base.shard}.{base.realm}.{base.value}-{checksum_value}", + shard=base.shard, + realm=base.realm, + value=base.value, + checksum=checksum_value, + ), + entity_id_valid_dotted, + primitives.checksum, + ) + + return { + "entity_id_valid_dotted": entity_id_valid_dotted, + "entity_id_valid_checksum": entity_id_valid_checksum, + } + + +def _build_alias_identifier_strategies( + primitives: _IdentifierPrimitives, key_samples: _KeySampleValues +) -> dict[str, SearchStrategy[Any]]: + """Build valid account and contract alias strategies using canonical alias and EVM samples.""" + + ed25519_alias_hex = st.just(key_samples.ed25519_alias_hex) + ecdsa_alias_hex = st.just(key_samples.ecdsa_alias_hex) + evm_hex = st.just(key_samples.evm_hex) + + account_id_valid_alias = st.builds( + lambda shard_value, realm_value, alias_hex: AccountIdAliasCase( + text=f"{shard_value}.{realm_value}.{alias_hex}", + shard=shard_value, + realm=realm_value, + alias_hex=alias_hex, + ), + primitives.shard, + primitives.realm, + st.one_of(ed25519_alias_hex, ecdsa_alias_hex), + ) + account_id_valid_evm = st.one_of( + evm_hex.map(lambda value: AccountIdAliasCase(text=value, shard=0, realm=0, evm_hex=value)), + evm_hex.map( + lambda value: AccountIdAliasCase(text=f"0x{value}", shard=0, realm=0, evm_hex=value) + ), + st.builds( + lambda shard_value, realm_value, value: AccountIdAliasCase( + text=f"{shard_value}.{realm_value}.{value}", + shard=shard_value, + realm=realm_value, + evm_hex=value, + ), + primitives.shard, + primitives.realm, + evm_hex, + ), + ) + contract_id_valid_evm = st.builds( + lambda shard_value, realm_value, value: AccountIdAliasCase( + text=f"{shard_value}.{realm_value}.{value}", + shard=shard_value, + realm=realm_value, + evm_hex=value, + ), + primitives.shard, + primitives.realm, + evm_hex, + ) + + return { + "account_id_valid_alias": account_id_valid_alias, + "account_id_valid_evm": account_id_valid_evm, + "contract_id_valid_evm": contract_id_valid_evm, + } + + +def _build_invalid_identifier_strategies() -> dict[str, SearchStrategy[Any]]: + """Build malformed identifier strategies while preserving existing invalid-string coverage.""" + + invalid_entity_strings = st.one_of( + st.sampled_from( + [ + "", + ".", + "..", + "0", + "0.0", + "0.0.0.0", + "0_0_0", + "0/0/0", + "0.0.-1", + "-1.0.0", + "0.-1.0", + "abc.def.ghi", + "1e3", + "nan", + "inf", + ] + ), + st.text(alphabet=string.ascii_letters + "_-:/ ", min_size=1, max_size=48), + ) + invalid_account_strings = st.one_of( + invalid_entity_strings, + st.sampled_from( + [ + "0.0.0xabcdef0123456789abcdef0123456789abcdef01", + "0x1234", + "abcdef", + ] + ), + ) + invalid_contract_strings = st.one_of( + invalid_entity_strings, + st.sampled_from( + [ + "abcdef0123456789abcdef0123456789abcdef01", + "0xabcdef0123456789abcdef0123456789abcdef01", + "1.2.0xabcdef0123456789abcdef0123456789abcdef01", + ] + ), + ) + invalid_token_strings = st.one_of( + invalid_entity_strings, + st.sampled_from( + [ + "abcdef0123456789abcdef0123456789abcdef01", + "0xabcdef0123456789abcdef0123456789abcdef01", + "1.2.abcdef0123456789abcdef0123456789abcdef01", + ] + ), + ) + entity_id_invalid_type = st.sampled_from([None, 123, True, {}, []]) + + return { + "account_id_invalid_string": invalid_account_strings, + "token_id_invalid_string": invalid_token_strings, + "contract_id_invalid_string": invalid_contract_strings, + "entity_id_invalid_type": entity_id_invalid_type, + } + + +def _build_private_key_strategies(key_samples: _KeySampleValues) -> dict[str, SearchStrategy[Any]]: + """Build the private key strategies from the canonical valid sample encodings.""" + + private_key_valid_string = st.one_of( + with_optional_0x( + st.sampled_from([key_samples.ed_private_raw, key_samples.ed_private_der]) + ), + ) + private_key_valid_bytes = st.sampled_from( + [ + bytes.fromhex(key_samples.ed_private_raw), + bytes.fromhex(key_samples.ed_private_der), + bytes.fromhex(key_samples.ecdsa_private_der), + ] + ) + private_key_invalid_string = st.one_of( + st.sampled_from( + [ + "not-hex", + "xyz", + "0xzz", + "11" * 31, + "11" * 33, + "30", + ] + ), + st.text(alphabet="ghijklmnopqrstuvwxyz_-", min_size=1, max_size=66), + ) + private_key_invalid_bytes = st.one_of( + st.binary(min_size=0, max_size=31), + st.binary(min_size=33, max_size=96), + st.binary(min_size=32, max_size=64).map(lambda data: b"\x30" + data), + ) + + return { + "private_key_valid_string": private_key_valid_string, + "private_key_valid_bytes": private_key_valid_bytes, + "private_key_invalid_string": private_key_invalid_string, + "private_key_invalid_bytes": private_key_invalid_bytes, + } + + +def _build_public_key_strategies(key_samples: _KeySampleValues) -> dict[str, SearchStrategy[Any]]: + """Build the public key strategies from the canonical valid sample encodings.""" + + public_key_valid_string = st.one_of( + with_optional_0x( + st.sampled_from( + [ + key_samples.ed_public_raw, + key_samples.ecdsa_public_compressed, + key_samples.ecdsa_public_uncompressed, + key_samples.ed_public_der, + key_samples.ecdsa_public_der, + ] + ) + ), + ) + public_key_valid_bytes = st.sampled_from( + [ + bytes.fromhex(key_samples.ed_public_raw), + bytes.fromhex(key_samples.ecdsa_public_compressed), + bytes.fromhex(key_samples.ecdsa_public_uncompressed), + bytes.fromhex(key_samples.ed_public_der), + bytes.fromhex(key_samples.ecdsa_public_der), + ] + ) + public_key_invalid_string = st.one_of( + st.sampled_from( + [ + "not-hex", + "0xzz", + "05" + "00" * 32, + "11" * 31, + "11" * 34, + "30", + ] + ), + st.text(alphabet="ghijklmnopqrstuvwxyz_-", min_size=1, max_size=130), + ) + public_key_invalid_bytes = st.one_of( + st.binary(min_size=0, max_size=31), + st.just(bytes.fromhex("05" + "00" * 32)), + st.binary(min_size=34, max_size=64), + st.binary(min_size=32, max_size=64).map(lambda data: b"\x30" + data), + ) + + return { + "public_key_valid_string": public_key_valid_string, + "public_key_valid_bytes": public_key_valid_bytes, + "public_key_invalid_string": public_key_invalid_string, + "public_key_invalid_bytes": public_key_invalid_bytes, + } + + +def _build_key_strategies(key_samples: _KeySampleValues) -> dict[str, SearchStrategy[Any]]: + """Build the complete private and public key strategy registry entries.""" + + strategies: dict[str, SearchStrategy[Any]] = {} + strategies.update(_build_private_key_strategies(key_samples)) + strategies.update(_build_public_key_strategies(key_samples)) + return strategies + + +def _build_hbar_strategies() -> dict[str, SearchStrategy[Any]]: + """Build Hbar parsing and constructor strategies, including intentional invalid edge cases.""" + + hbar_valid_tinybars = st.integers(min_value=-(10**12), max_value=10**12) + hbar_valid_string = st.one_of( + hbar_valid_tinybars.map(lambda tinybars: hbar_string_case(HbarUnit.HBAR, tinybars)), + hbar_valid_tinybars.map(lambda tinybars: hbar_string_case(HbarUnit.MICROBAR, tinybars)), + hbar_valid_tinybars.map(lambda tinybars: hbar_string_case(HbarUnit.MILLIBAR, tinybars)), + hbar_valid_tinybars.map(lambda tinybars: hbar_string_case(HbarUnit.KILOBAR, tinybars)), + hbar_valid_tinybars.map(lambda tinybars: hbar_string_case(HbarUnit.MEGABAR, tinybars)), + hbar_valid_tinybars.map(lambda tinybars: hbar_string_case(HbarUnit.GIGABAR, tinybars)), + ) + hbar_invalid_string = st.one_of( + st.sampled_from( + [ + "1e3", + "1 hbar", + "1 tinybar", + "1 tโ„", + "1.5 tโ„", + " 1 โ„", + "1 โ„", + "1\tโ„", + "nan", + "inf", + "-inf", + "", + ] + ), + st.text(alphabet=string.ascii_letters + "_-", min_size=1, max_size=24), + ) + hbar_valid_constructor = st.one_of( + hbar_valid_tinybars.map(lambda tinybars: hbar_constructor_case(HbarUnit.TINYBAR, tinybars)), + hbar_valid_tinybars.map(lambda tinybars: hbar_constructor_case(HbarUnit.HBAR, tinybars)), + hbar_valid_tinybars.map(lambda tinybars: hbar_constructor_case(HbarUnit.MICROBAR, tinybars)), + ) + hbar_invalid_nonfinite_float = st.sampled_from([float("inf"), float("-inf"), float("nan")]) + fractional_tinybar_amount = st.decimals( + min_value=Decimal("-1000"), + max_value=Decimal("1000"), + allow_nan=False, + allow_infinity=False, + places=1, + ).filter(lambda value: value != value.to_integral_value()) + hbar_invalid_constructor_type = st.sampled_from(["1", None, True, object()]) + + return { + "hbar_valid_string": hbar_valid_string, + "hbar_invalid_string": hbar_invalid_string, + "hbar_valid_constructor": hbar_valid_constructor, + "hbar_invalid_nonfinite_float": hbar_invalid_nonfinite_float, + "fractional_tinybar_amount": fractional_tinybar_amount, + "hbar_invalid_constructor_type": hbar_invalid_constructor_type, + } + + +def _build_transaction_byte_strategies() -> dict[str, SearchStrategy[Any]]: + """Build valid and intentionally broken transaction byte payload strategies.""" + + unsigned_tx_bytes, signed_tx_bytes = build_valid_transaction_bytes() + tx_valid_bytes = st.sampled_from([unsigned_tx_bytes, signed_tx_bytes]) + tx_invalid_empty = st.just(b"") + tx_invalid_random = st.binary(min_size=1, max_size=8) + tx_invalid_truncated = st.sampled_from( + [unsigned_tx_bytes[:10], signed_tx_bytes[:10], unsigned_tx_bytes[:-1], signed_tx_bytes[:-1]] + ) + tx_invalid_corrupted = st.sampled_from( + [ + bytes([unsigned_tx_bytes[0] ^ 0x01]) + unsigned_tx_bytes[1:], + unsigned_tx_bytes[:1] + bytes([unsigned_tx_bytes[1] ^ 0x01]) + unsigned_tx_bytes[2:], + unsigned_tx_bytes[:20] + b"\x00" + unsigned_tx_bytes[20:], + b"\x00" + unsigned_tx_bytes, + unsigned_tx_bytes + b"\x00", + bytes([signed_tx_bytes[0] ^ 0x01]) + signed_tx_bytes[1:], + signed_tx_bytes[:1] + bytes([signed_tx_bytes[1] ^ 0x01]) + signed_tx_bytes[2:], + signed_tx_bytes[:20] + b"\x00" + signed_tx_bytes[20:], + b"\x00" + signed_tx_bytes, + signed_tx_bytes + b"\x00", + ] + ) + + return { + "tx_valid_bytes": tx_valid_bytes, + "tx_invalid_empty": tx_invalid_empty, + "tx_invalid_random": tx_invalid_random, + "tx_invalid_truncated": tx_invalid_truncated, + "tx_invalid_corrupted": tx_invalid_corrupted, + } + + +def _build_contract_value_strategies() -> dict[str, SearchStrategy[Any]]: + """Build valid and invalid contract parameter cases without changing ABI edge coverage.""" + + valid_contract_value = st.one_of( + st.booleans().map(lambda value: ContractValueCase("add_bool", value)), + st.integers(min_value=-(2**31), max_value=2**31 - 1).map( + lambda value: ContractValueCase("add_int32", value) + ), + st.integers(min_value=0, max_value=2**32 - 1).map( + lambda value: ContractValueCase("add_uint32", value) + ), + st.integers(min_value=MIN_I64, max_value=MAX_I64).map( + lambda value: ContractValueCase("add_int64", value) + ), + st.integers(min_value=0, max_value=2**64 - 1).map( + lambda value: ContractValueCase("add_uint64", value) + ), + st.integers(min_value=-(2**255), max_value=2**255 - 1).map( + lambda value: ContractValueCase("add_int256", value) + ), + st.integers(min_value=0, max_value=2**256 - 1).map( + lambda value: ContractValueCase("add_uint256", value) + ), + st.text(min_size=0, max_size=32).map(lambda value: ContractValueCase("add_string", value)), + st.binary(min_size=0, max_size=64).map(lambda value: ContractValueCase("add_bytes", value)), + st.binary(min_size=0, max_size=32).map( + lambda value: ContractValueCase("add_bytes32", value) + ), + st.binary(min_size=20, max_size=20).map( + lambda value: ContractValueCase("add_address", value) + ), + with_optional_0x(sized_hex(20)).map(lambda value: ContractValueCase("add_address", value)), + st.lists(st.integers(min_value=-(2**31), max_value=2**31 - 1), min_size=0, max_size=6).map( + lambda value: ContractValueCase("add_int32_array", value) + ), + st.lists(st.integers(min_value=0, max_value=2**32 - 1), min_size=0, max_size=6).map( + lambda value: ContractValueCase("add_uint32_array", value) + ), + st.lists(st.binary(min_size=0, max_size=24), min_size=0, max_size=6).map( + lambda value: ContractValueCase("add_bytes_array", value) + ), + st.lists(st.binary(min_size=0, max_size=32), min_size=0, max_size=6).map( + lambda value: ContractValueCase("add_bytes32_array", value) + ), + st.lists(st.text(min_size=0, max_size=16), min_size=0, max_size=6).map( + lambda value: ContractValueCase("add_string_array", value) + ), + st.lists(st.binary(min_size=20, max_size=20), min_size=0, max_size=6).map( + lambda value: ContractValueCase("add_address_array", value) + ), + ) + invalid_contract_value = st.one_of( + st.just(InvalidContractValueCase("add_bool", "true", EncodingTypeError)), + st.just(InvalidContractValueCase("add_int32", 2**31, ValueOutOfBounds)), + st.just(InvalidContractValueCase("add_uint32", -1, ValueOutOfBounds)), + st.just(InvalidContractValueCase("add_bytes32", b"a" * 33, ValueOutOfBounds)), + st.just(InvalidContractValueCase("add_address", b"a" * 19, EncodingTypeError)), + st.just(InvalidContractValueCase("add_address", "0x1234", EncodingTypeError)), + st.just(InvalidContractValueCase("add_bytes_array", [b"a", "b"], EncodingTypeError)), + st.just(InvalidContractValueCase("add_string_array", ["ok", b"bad"], EncodingTypeError)), + st.just(InvalidContractValueCase("add_address_array", [b"a" * 19], EncodingTypeError)), + ) + contract_function_name = st.one_of( + st.none(), + st.text(alphabet=string.ascii_letters + string.digits + "_", min_size=1, max_size=24), + ) + + return { + "contract_value_valid": valid_contract_value, + "contract_value_invalid": invalid_contract_value, + "contract_function_name": contract_function_name, + } + + +def build_strategy_registry() -> dict[str, SearchStrategy[Any]]: + """Assemble all domain-specific strategy groups into the shared fuzz registry.""" + + primitives = _build_identifier_primitives() + key_samples = _build_key_sample_values() + + registry: dict[str, SearchStrategy[Any]] = {} + registry.update(_build_entity_id_strategies(primitives)) + registry.update(_build_alias_identifier_strategies(primitives, key_samples)) + registry.update(_build_invalid_identifier_strategies()) + registry.update(_build_key_strategies(key_samples)) + registry.update(_build_hbar_strategies()) + registry.update(_build_transaction_byte_strategies()) + registry.update(_build_contract_value_strategies()) + return registry + + +FUZZ_STRATEGIES = build_strategy_registry() + + +def get_strategy(name: str) -> SearchStrategy[Any]: + """Return the named fuzz strategy from the shared registry or raise a helpful KeyError.""" + + try: + return FUZZ_STRATEGIES[name] + except KeyError as exc: + available = ", ".join(sorted(FUZZ_STRATEGIES)) + raise KeyError(f"Unknown strategy {name!r}. Available strategies: {available}") from exc + + +@pytest.fixture(name="fuzz_strategies") +def fuzz_strategies_fixture() -> dict[str, SearchStrategy[Any]]: + """Provide the shared fuzz strategy registry.""" + + return FUZZ_STRATEGIES + + +@pytest.fixture(name="get_strategy") +def get_strategy_fixture() -> Any: + """Provide the named strategy accessor.""" + + return get_strategy diff --git a/tests/fuzz/test_contract_function_parameters_fuzz.py b/tests/fuzz/test_contract_function_parameters_fuzz.py new file mode 100644 index 000000000..22121bf5b --- /dev/null +++ b/tests/fuzz/test_contract_function_parameters_fuzz.py @@ -0,0 +1,61 @@ +import pytest +from eth_abi.exceptions import EncodingTypeError, ValueOutOfBounds +from hypothesis import given + +from hiero_sdk_python import ContractFunctionParameters + +from tests.fuzz.conftest import ContractValueCase, InvalidContractValueCase, get_strategy + +pytestmark = pytest.mark.fuzz + + +def _encode(function_name: str | None, case: ContractValueCase) -> bytes: + params = ContractFunctionParameters(function_name) + getattr(params, case.method_name)(case.value) + encoded = params.to_bytes() + + assert isinstance(encoded, bytes) + assert bytes(params) == encoded + assert params.to_bytes() == encoded + return encoded + + +@given( + function_name=get_strategy("contract_function_name"), + case=get_strategy("contract_value_valid"), +) +def test_contract_function_parameters_encode_valid_values( + function_name: str | None, + case: ContractValueCase, +) -> None: + """Valid add_* inputs must encode deterministically.""" + encoded = _encode(function_name, case) + unnamed_encoded = _encode(None, case) + + if function_name is None: + assert encoded == unnamed_encoded + else: + assert len(encoded) >= 4 + assert encoded[4:] == unnamed_encoded + + +@given( + function_name=get_strategy("contract_function_name"), + case=get_strategy("contract_value_invalid"), +) +def test_contract_function_parameters_reject_invalid_values( + function_name: str | None, + case: InvalidContractValueCase, +) -> None: + """Wrong-shaped or out-of-range values must raise the expected encoder exception.""" + params = ContractFunctionParameters(function_name) + + with pytest.raises(case.expected_exception): + getattr(params, case.method_name)(case.value) + params.to_bytes() + + +def test_contract_function_parameters_invalid_case_exceptions_are_specific() -> None: + """Guardrail: invalid-case expectations should stay narrow and intentional.""" + assert ValueOutOfBounds is not ValueError + assert EncodingTypeError is not TypeError diff --git a/tests/fuzz/test_entity_id_fuzz.py b/tests/fuzz/test_entity_id_fuzz.py new file mode 100644 index 000000000..df64eeb16 --- /dev/null +++ b/tests/fuzz/test_entity_id_fuzz.py @@ -0,0 +1,184 @@ +import warnings + +import pytest +from hypothesis import given + +from hiero_sdk_python import AccountId, TokenId +from hiero_sdk_python.contract.contract_id import ContractId + +from tests.fuzz.conftest import AccountIdAliasCase, EntityIdCase, get_strategy + +pytestmark = pytest.mark.fuzz + + +def _assert_account_roundtrip(case: EntityIdCase) -> None: + parsed = AccountId.from_string(case.text) + assert parsed.shard == case.shard + assert parsed.realm == case.realm + assert parsed.num == case.value + assert parsed.checksum == case.checksum + assert str(parsed) == f"{case.shard}.{case.realm}.{case.value}" + canonical = AccountId.from_string(str(parsed)) + assert canonical.shard == parsed.shard + assert canonical.realm == parsed.realm + assert canonical.num == parsed.num + assert canonical.alias_key == parsed.alias_key + assert canonical.evm_address == parsed.evm_address + assert canonical.checksum is None + + +def _assert_token_roundtrip(case: EntityIdCase) -> None: + parsed = TokenId.from_string(case.text) + assert parsed.shard == case.shard + assert parsed.realm == case.realm + assert parsed.num == case.value + assert parsed.checksum == case.checksum + assert str(parsed) == f"{case.shard}.{case.realm}.{case.value}" + canonical = TokenId.from_string(str(parsed)) + assert canonical.shard == parsed.shard + assert canonical.realm == parsed.realm + assert canonical.num == parsed.num + assert canonical.checksum is None + + +def _assert_contract_roundtrip(case: EntityIdCase) -> None: + parsed = ContractId.from_string(case.text) + assert parsed.shard == case.shard + assert parsed.realm == case.realm + assert parsed.contract == case.value + assert parsed.checksum == case.checksum + assert str(parsed) == f"{case.shard}.{case.realm}.{case.value}" + canonical = ContractId.from_string(str(parsed)) + assert canonical.shard == parsed.shard + assert canonical.realm == parsed.realm + assert canonical.contract == parsed.contract + assert canonical.checksum is None + + +@given(case=get_strategy("entity_id_valid_dotted")) +def test_account_id_accepts_valid_dotted_ids(case: EntityIdCase) -> None: + """Valid dotted account IDs must parse and round-trip canonically.""" + _assert_account_roundtrip(case) + + +@given(case=get_strategy("entity_id_valid_checksum")) +def test_account_id_preserves_checksum_text(case: EntityIdCase) -> None: + """Checksum-bearing account IDs must retain checksum metadata after parsing.""" + _assert_account_roundtrip(case) + + +@given(case=get_strategy("account_id_valid_alias")) +def test_account_id_accepts_valid_alias_hex(case: AccountIdAliasCase) -> None: + """Alias-style account IDs must decode into alias_key and reserialize canonically.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + parsed = AccountId.from_string(case.text) + + assert parsed.shard == case.shard + assert parsed.realm == case.realm + assert parsed.num == 0 + assert parsed.alias_key is not None + assert parsed.evm_address is None + assert parsed.alias_key.to_bytes_raw().hex() == case.alias_hex + assert str(parsed) == case.text + + +@given(case=get_strategy("account_id_valid_evm")) +def test_account_id_accepts_valid_evm_forms(case: AccountIdAliasCase) -> None: + """Raw, prefixed, and scoped EVM-address inputs must parse without silent coercion.""" + parsed = AccountId.from_string(case.text) + + assert parsed.shard == case.shard + assert parsed.realm == case.realm + assert parsed.num == 0 + assert parsed.alias_key is None + assert parsed.evm_address is not None + assert parsed.evm_address.address_bytes.hex() == case.evm_hex + assert str(parsed) == f"{case.shard}.{case.realm}.{case.evm_hex}" + assert AccountId.from_string(str(parsed)) == parsed + + +@given(case=get_strategy("entity_id_valid_dotted")) +def test_token_id_accepts_valid_dotted_ids(case: EntityIdCase) -> None: + """Valid dotted token IDs must parse and round-trip canonically.""" + _assert_token_roundtrip(case) + + +@given(case=get_strategy("entity_id_valid_checksum")) +def test_token_id_preserves_checksum_text(case: EntityIdCase) -> None: + """Checksum-bearing token IDs must retain checksum metadata after parsing.""" + _assert_token_roundtrip(case) + + +@given(case=get_strategy("entity_id_valid_dotted")) +def test_contract_id_accepts_valid_dotted_ids(case: EntityIdCase) -> None: + """Valid dotted contract IDs must parse and round-trip canonically.""" + _assert_contract_roundtrip(case) + + +@given(case=get_strategy("entity_id_valid_checksum")) +def test_contract_id_preserves_checksum_text(case: EntityIdCase) -> None: + """Checksum-bearing contract IDs must retain checksum metadata after parsing.""" + _assert_contract_roundtrip(case) + + +@given(case=get_strategy("contract_id_valid_evm")) +def test_contract_id_accepts_scoped_evm_forms(case: AccountIdAliasCase) -> None: + """Scoped 20-byte hex EVM addresses are a valid public ContractId format.""" + parsed = ContractId.from_string(case.text) + + assert parsed.shard == case.shard + assert parsed.realm == case.realm + assert parsed.contract == 0 + assert parsed.evm_address is not None + assert parsed.evm_address.hex() == case.evm_hex + assert str(parsed) == case.text + assert ContractId.from_string(str(parsed)) == parsed + + +@given(value=get_strategy("entity_id_invalid_type")) +def test_account_id_rejects_non_string_inputs(value: object) -> None: + """AccountId.from_string() documents a str-only API.""" + with pytest.raises(TypeError): + AccountId.from_string(value) # type: ignore[arg-type] + + +@given(value=get_strategy("entity_id_invalid_type")) +def test_token_id_rejects_non_string_inputs(value: object) -> None: + """TokenId.from_string() must reject non-string inputs rather than coercing them.""" + if value is None: + with pytest.raises(ValueError, match="cannot be None"): + TokenId.from_string(value) # type: ignore[arg-type] + else: + with pytest.raises(ValueError): + TokenId.from_string(value) # type: ignore[arg-type] + + +@given(value=get_strategy("entity_id_invalid_type")) +def test_contract_id_rejects_non_string_inputs(value: object) -> None: + """ContractId.from_string() documents a str-only API.""" + with pytest.raises(TypeError): + ContractId.from_string(value) # type: ignore[arg-type] + + +@given(text=get_strategy("account_id_invalid_string")) +def test_account_id_rejects_invalid_strings(text: str) -> None: + """Malformed account ID text must raise ValueError instead of partially parsing.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + with pytest.raises(ValueError): + AccountId.from_string(text) + + +@given(text=get_strategy("token_id_invalid_string")) +def test_token_id_rejects_invalid_strings(text: str) -> None: + """Malformed token ID text must raise ValueError.""" + with pytest.raises(ValueError, match="Invalid token ID string"): + TokenId.from_string(text) + + +@given(text=get_strategy("contract_id_invalid_string")) +def test_contract_id_rejects_invalid_strings(text: str) -> None: + """Malformed contract ID text must raise ValueError.""" + with pytest.raises(ValueError): + ContractId.from_string(text) diff --git a/tests/fuzz/test_hbar_fuzz.py b/tests/fuzz/test_hbar_fuzz.py new file mode 100644 index 000000000..d84ae26f8 --- /dev/null +++ b/tests/fuzz/test_hbar_fuzz.py @@ -0,0 +1,60 @@ +import math + +import pytest +from hypothesis import given + +from hiero_sdk_python import Hbar, HbarUnit + +from tests.fuzz.conftest import HbarConstructorCase, HbarStringCase, get_strategy + +pytestmark = pytest.mark.fuzz + + +def _assert_hbar_invariants(value: Hbar, expected_tinybars: int) -> None: + assert value.to_tinybars() == expected_tinybars + assert math.isfinite(value.to_hbars()) + roundtripped = Hbar.from_tinybars(value.to_tinybars()) + assert roundtripped == value + assert roundtripped.to_tinybars() == expected_tinybars + + +@given(case=get_strategy("hbar_valid_string")) +def test_hbar_from_string_accepts_valid_inputs(case: HbarStringCase) -> None: + """Valid public Hbar strings must parse to the exact tinybar value.""" + parsed = Hbar.from_string(case.text) + _assert_hbar_invariants(parsed, case.tinybars) + + +@given(text=get_strategy("hbar_invalid_string")) +def test_hbar_from_string_rejects_invalid_inputs(text: str) -> None: + """Invalid Hbar strings must raise ValueError instead of partially parsing.""" + with pytest.raises(ValueError): + Hbar.from_string(text) + + +@given(case=get_strategy("hbar_valid_constructor")) +def test_hbar_constructor_preserves_exact_value(case: HbarConstructorCase) -> None: + """Valid constructor inputs must convert to the exact expected tinybar count.""" + parsed = Hbar(case.amount, unit=case.unit) + _assert_hbar_invariants(parsed, case.tinybars) + + +@given(amount=get_strategy("hbar_invalid_nonfinite_float")) +def test_hbar_constructor_rejects_nonfinite_floats(amount: float) -> None: + """The constructor must reject NaN and infinities explicitly.""" + with pytest.raises(ValueError): + Hbar(amount) + + +@given(amount=get_strategy("fractional_tinybar_amount")) +def test_hbar_rejects_fractional_tinybars(amount: object) -> None: + """Tinybar inputs are integral only; fractional values are invalid.""" + with pytest.raises(ValueError): + Hbar(amount, unit=HbarUnit.TINYBAR) + + +@given(value=get_strategy("hbar_invalid_constructor_type")) +def test_hbar_constructor_rejects_invalid_types(value: object) -> None: + """The constructor must not silently coerce unsupported input types.""" + with pytest.raises(TypeError): + Hbar(value) \ No newline at end of file diff --git a/tests/fuzz/test_keys_fuzz.py b/tests/fuzz/test_keys_fuzz.py new file mode 100644 index 000000000..51b5ddf83 --- /dev/null +++ b/tests/fuzz/test_keys_fuzz.py @@ -0,0 +1,112 @@ +import warnings + +import pytest +from hypothesis import given + +from hiero_sdk_python import PrivateKey, PublicKey + +from tests.fuzz.conftest import get_strategy + +pytestmark = pytest.mark.fuzz + + +def _load_private_key_from_string(text: str) -> PrivateKey: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + return PrivateKey.from_string(text) + + +def _load_private_key_from_bytes(data: bytes) -> PrivateKey: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + return PrivateKey.from_bytes(data) + + +def _load_public_key_from_string(text: str) -> PublicKey: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + return PublicKey.from_string(text) + + +def _load_public_key_from_bytes(data: bytes) -> PublicKey: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + return PublicKey.from_bytes(data) + + +@given(text=get_strategy("private_key_valid_string")) +def test_private_key_from_string_roundtrips_valid_inputs(text: str) -> None: + """Valid private-key strings must produce a stable key object and raw bytes.""" + parsed = _load_private_key_from_string(text) + reparsed = _load_private_key_from_string(parsed.to_string_raw()) + + assert parsed.to_bytes_raw() == reparsed.to_bytes_raw() + assert parsed.is_ed25519() == reparsed.is_ed25519() + assert parsed.to_string_raw() == parsed.to_bytes_raw().hex() + + +@given(data=get_strategy("private_key_valid_bytes")) +def test_private_key_from_bytes_roundtrips_valid_inputs(data: bytes) -> None: + """Valid private-key byte encodings must round-trip through raw or DER serialization.""" + parsed = _load_private_key_from_bytes(data) + reparsed = _load_private_key_from_bytes(parsed.to_bytes_der()) + + assert parsed.to_bytes_raw() == reparsed.to_bytes_raw() + assert parsed.is_ed25519() == reparsed.is_ed25519() + + +@given(text=get_strategy("private_key_invalid_string")) +def test_private_key_from_string_rejects_invalid_inputs(text: str) -> None: + """Malformed private-key strings must raise ValueError.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + with pytest.raises(ValueError): + PrivateKey.from_string(text) + + +@given(data=get_strategy("private_key_invalid_bytes")) +def test_private_key_from_bytes_rejects_invalid_inputs(data: bytes) -> None: + """Malformed private-key bytes must raise ValueError.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + with pytest.raises(ValueError): + PrivateKey.from_bytes(data) + + +@given(text=get_strategy("public_key_valid_string")) +def test_public_key_from_string_roundtrips_valid_inputs(text: str) -> None: + """Valid public-key strings must preserve canonical raw bytes.""" + parsed = _load_public_key_from_string(text) + reparsed = _load_public_key_from_string(parsed.to_string_raw()) + + assert parsed.to_bytes_raw() == reparsed.to_bytes_raw() + assert parsed.is_ed25519() == reparsed.is_ed25519() + assert parsed.to_string_raw() == parsed.to_bytes_raw().hex() + + +@given(data=get_strategy("public_key_valid_bytes")) +def test_public_key_from_bytes_roundtrips_valid_inputs(data: bytes) -> None: + """Valid public-key byte encodings must round-trip through DER serialization.""" + parsed = _load_public_key_from_bytes(data) + reparsed = _load_public_key_from_bytes(parsed.to_bytes_der()) + + assert parsed.to_bytes_raw() == reparsed.to_bytes_raw() + assert parsed.is_ed25519() == reparsed.is_ed25519() + + +@given(text=get_strategy("public_key_invalid_string")) +def test_public_key_from_string_rejects_invalid_inputs(text: str) -> None: + """Malformed public-key strings must raise ValueError.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + with pytest.raises(ValueError): + PublicKey.from_string(text) + + +@given(data=get_strategy("public_key_invalid_bytes")) +def test_public_key_from_bytes_rejects_invalid_inputs(data: bytes) -> None: + """Malformed public-key bytes must raise ValueError.""" + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + with pytest.raises(ValueError): + PublicKey.from_bytes(data) diff --git a/tests/fuzz/test_transaction_from_bytes_fuzz.py b/tests/fuzz/test_transaction_from_bytes_fuzz.py new file mode 100644 index 000000000..f793dfffd --- /dev/null +++ b/tests/fuzz/test_transaction_from_bytes_fuzz.py @@ -0,0 +1,52 @@ +import pytest +from hypothesis import given, settings + +from hiero_sdk_python import Transaction + +from tests.fuzz.conftest import get_strategy + +pytestmark = pytest.mark.fuzz + + +@settings(max_examples=80) +@given(data=get_strategy("tx_valid_bytes")) +def test_transaction_from_bytes_roundtrips_valid_payloads(data: bytes) -> None: + """Serialized transactions must deserialize and reserialize byte-for-byte.""" + parsed = Transaction.from_bytes(data) + reparsed = Transaction.from_bytes(parsed.to_bytes()) + + assert parsed.to_bytes() == data + assert type(reparsed) is type(parsed) + assert reparsed.to_bytes() == parsed.to_bytes() + + +@settings(max_examples=30) +@given(data=get_strategy("tx_invalid_empty")) +def test_transaction_from_bytes_rejects_empty_payloads(data: bytes) -> None: + """Empty payloads are explicitly invalid for Transaction.from_bytes().""" + with pytest.raises(ValueError): + Transaction.from_bytes(data) + + +@settings(max_examples=80) +@given(data=get_strategy("tx_invalid_truncated")) +def test_transaction_from_bytes_rejects_truncated_payloads(data: bytes) -> None: + """Truncated transaction payloads must raise ValueError.""" + with pytest.raises(ValueError): + Transaction.from_bytes(data) + + +@settings(max_examples=80) +@given(data=get_strategy("tx_invalid_corrupted")) +def test_transaction_from_bytes_rejects_corrupted_payloads(data: bytes) -> None: + """Byte-level corruption of a valid payload must not deserialize successfully.""" + with pytest.raises(ValueError): + Transaction.from_bytes(data) + + +@settings(max_examples=80) +@given(data=get_strategy("tx_invalid_random")) +def test_transaction_from_bytes_rejects_random_payloads(data: bytes) -> None: + """Random byte payloads must not parse as valid SDK transactions.""" + with pytest.raises(ValueError): + Transaction.from_bytes(data) diff --git a/tests/integration/account_create_transaction_e2e_test.py b/tests/integration/account_create_transaction_e2e_test.py index 3e4239c88..0a69acacf 100644 --- a/tests/integration/account_create_transaction_e2e_test.py +++ b/tests/integration/account_create_transaction_e2e_test.py @@ -2,6 +2,7 @@ from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction from hiero_sdk_python.account.account_update_transaction import AccountUpdateTransaction from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.exceptions import PrecheckError from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.query.account_info_query import AccountInfoQuery from hiero_sdk_python.response_code import ResponseCode @@ -214,7 +215,7 @@ def test_create_account_with_staked_account_id(env): info = AccountInfoQuery(account_id=account_id).execute(env.client) assert info.account_id == account_id - assert info.staked_account_id == env.operator_id + assert info.staking_info.staked_account_id == env.operator_id def test_create_account_with_staked_node_id(env): @@ -265,8 +266,9 @@ def test_create_account_with_decline_reward(env): info = AccountInfoQuery(account_id=account_id).execute(env.client) assert info.account_id == account_id - assert info.staked_account_id == env.operator_id - assert info.decline_staking_reward is True + assert info.staking_info is not None, "Staking info should be set" + assert info.staking_info.staked_account_id == env.operator_id + assert info.staking_info.decline_reward is True def test_integration_account_create_transaction_can_execute_with_private_key(env): """Test AccountCreateTransaction can be executed when key is a PrivateKey.""" @@ -367,3 +369,18 @@ def test_proto_excludes_alias_when_not_set(env): assert not tx_body.cryptoCreateAccount.alias # Key must still be present assert tx_body.cryptoCreateAccount.key == account_public_key._to_proto() + + +def test_create_account_with_negative_initial_balance(env): + """Test create_account_transaction raise precheck-error for negative initial balance""" + public_key = PrivateKey.generate_ed25519().public_key() + tx = ( + AccountCreateTransaction() + .set_key_without_alias(public_key) + .set_initial_balance(-1) + ) + + with pytest.raises(PrecheckError) as e: + tx.execute(env.client) + + assert e.value.status == ResponseCode.INVALID_INITIAL_BALANCE diff --git a/tests/integration/account_update_transaction_e2e_test.py b/tests/integration/account_update_transaction_e2e_test.py index 06aac3c5a..41605fe08 100644 --- a/tests/integration/account_update_transaction_e2e_test.py +++ b/tests/integration/account_update_transaction_e2e_test.py @@ -8,6 +8,7 @@ from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.account.account_update_transaction import AccountUpdateTransaction +from hiero_sdk_python.crypto.key_list import KeyList from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.Duration import Duration from hiero_sdk_python.hbar import Hbar @@ -75,6 +76,59 @@ def test_integration_account_update_transaction_can_execute(env): ), "Auto renew period should be updated" +@pytest.mark.integration +def test_integration_account_update_transaction_set_key_with_threshold_keylist(env): + """Test rotating to a threshold KeyList and authorizing follow-up updates.""" + # Create an account controlled by the operator key (payer signature is automatic). + receipt = ( + AccountCreateTransaction() + .set_key_without_alias(env.operator_key.public_key()) + .set_initial_balance(Hbar(2)) + .execute(env.client) + ) + assert ( + receipt.status == ResponseCode.SUCCESS + ), f"Account creation failed with status: {ResponseCode(receipt.status).name}" + + account_id = receipt.account_id + assert account_id is not None, "Account ID should not be None" + + # Build a 2-of-2 threshold KeyList and rotate the account key to it. + key_1_private = PrivateKey.generate_ed25519() + key_2_private = PrivateKey.generate_ed25519() + threshold_key = KeyList( + [key_1_private.public_key(), key_2_private.public_key()], threshold=2 + ) + + receipt = ( + AccountUpdateTransaction() + .set_account_id(account_id) + .set_key(threshold_key) + .freeze_with(env.client) + .sign(key_1_private) + .sign(key_2_private) + .execute(env.client) + ) + assert ( + receipt.status == ResponseCode.SUCCESS + ), f"Account key rotation to KeyList failed with status: {ResponseCode(receipt.status).name}" + + # Verify a follow-up update can be authorized with both threshold keys. + new_memo = "Updated using threshold KeyList" + receipt = ( + AccountUpdateTransaction() + .set_account_id(account_id) + .set_account_memo(new_memo) + .freeze_with(env.client) + .sign(key_1_private) + .sign(key_2_private) + .execute(env.client) + ) + assert ( + receipt.status == ResponseCode.SUCCESS + ), f"Account update signed by threshold keys failed with status: {ResponseCode(receipt.status).name}" + + @pytest.mark.integration def test_integration_account_update_transaction_fails_with_invalid_account_id(env): """Test that AccountUpdateTransaction fails with an invalid account ID.""" @@ -369,9 +423,9 @@ def test_integration_account_update_transaction_with_staking_fields(env): # Verify staking info reflects the updated values info = AccountInfoQuery(account_id).execute(env.client) - assert info.staked_account_id == staked_account_id, "Staked account ID should match" - assert info.staked_node_id is None, "Staked node ID should be cleared when staking to an account" - assert info.decline_staking_reward is True, "Decline staking reward should be true" + assert info.staking_info.staked_account_id == staked_account_id, "Staked account ID should match" + assert info.staking_info.staked_node_id is None, "Staked node ID should be cleared when staking to an account" + assert info.staking_info.decline_reward is True, "Decline staking reward should be true" @pytest.mark.integration @@ -403,5 +457,6 @@ def test_integration_account_update_transaction_with_staked_node_id(env): if receipt.status == ResponseCode.SUCCESS: info = AccountInfoQuery(account_id).execute(env.client) - assert info.staked_node_id == 0 - assert info.staked_account_id is None \ No newline at end of file + assert info.staking_info is not None, "Staking info should be set" + assert info.staking_info.staked_node_id == 0 + assert info.staking_info.staked_account_id is None \ No newline at end of file diff --git a/tests/integration/prng_transaction_e2e_test.py b/tests/integration/prng_transaction_e2e_test.py index e467bd68f..ae9df7d83 100644 --- a/tests/integration/prng_transaction_e2e_test.py +++ b/tests/integration/prng_transaction_e2e_test.py @@ -24,7 +24,7 @@ def test_integration_prng_transaction_can_execute(env): assert ( record.prng_number >= 0 and record.prng_number <= 100 ), "PRNG number should be between 0 and 100" - assert record.prng_bytes == b"", "PRNG bytes should be empty bytes" + assert record.prng_bytes is None, "PRNG bytes should be None" @pytest.mark.integration @@ -37,10 +37,9 @@ def test_integration_prng_transaction_can_execute_without_range(env): record = TransactionRecordQuery(receipt.transaction_id).execute(env.client) - assert record.prng_number == 0, "PRNG number should be 0" - assert len(record.prng_bytes) == 48, "PRNG bytes should be 48 bytes" + assert record.prng_number is None, "PRNG number should be None" assert record.prng_bytes is not None, "PRNG bytes should not be None" - + assert len(record.prng_bytes) == 48, "PRNG bytes should be 48 bytes" @pytest.mark.integration def test_integration_prng_transaction_can_execute_with_zero_range(env): @@ -52,6 +51,6 @@ def test_integration_prng_transaction_can_execute_with_zero_range(env): record = TransactionRecordQuery(receipt.transaction_id).execute(env.client) - assert record.prng_number == 0, "PRNG number should be 0" - assert len(record.prng_bytes) == 48, "PRNG bytes should be 48 bytes" + assert record.prng_number is None, "PRNG number should be None" assert record.prng_bytes is not None, "PRNG bytes should not be None" + assert len(record.prng_bytes) == 48, "PRNG bytes should be 48 bytes" \ No newline at end of file diff --git a/tests/integration/transaction_e2e_test.py b/tests/integration/transaction_e2e_test.py index 2aa5c2fc2..65fe9b577 100644 --- a/tests/integration/transaction_e2e_test.py +++ b/tests/integration/transaction_e2e_test.py @@ -1,10 +1,13 @@ import pytest from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction +from hiero_sdk_python.account.account_delete_transaction import AccountDeleteTransaction +from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.consensus.topic_create_transaction import TopicCreateTransaction from hiero_sdk_python.consensus.topic_delete_transaction import TopicDeleteTransaction from hiero_sdk_python.consensus.topic_message_submit_transaction import TopicMessageSubmitTransaction from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.exceptions import ReceiptStatusError from hiero_sdk_python.query.topic_info_query import TopicInfoQuery from hiero_sdk_python.query.transaction_get_receipt_query import ( TransactionGetReceiptQuery, @@ -238,3 +241,39 @@ def test_chunk_tx_returns_receipts_with_wait_for_receipt(env): TopicDeleteTransaction().set_topic_id(topic_id).execute(env.client) +@pytest.mark.integration +def test_get_receipt_returns_failed_status_by_default(env): + """Test receipt is returned normally on failure when validation is disabled.""" + tx = ( + AccountDeleteTransaction() + .set_transfer_account_id(env.operator_id) + .set_account_id(AccountId(0, 0, 0)) + ) + response = tx.execute(env.client, wait_for_receipt=False) + + assert isinstance(response, TransactionResponse) + assert response.transaction is tx + + receipt = response.get_receipt(env.client) + assert isinstance(receipt, TransactionReceipt) + assert receipt.transaction_id == tx.transaction_id + assert receipt.status == ResponseCode.INVALID_ACCOUNT_ID + + +@pytest.mark.integration +def test_get_receipt_raises_exception_on_failure_with_validation(env): + """Test error is raised for failing transactions when validation is enabled.""" + tx = ( + AccountDeleteTransaction() + .set_transfer_account_id(env.operator_id) + .set_account_id(AccountId(0, 0, 0)) + ) + response = tx.execute(env.client, wait_for_receipt=False) + + assert isinstance(response, TransactionResponse) + assert response.transaction is tx + + with pytest.raises(ReceiptStatusError) as e: + response.get_receipt(env.client, validate_status=True) + + assert e.value.status == ResponseCode.INVALID_ACCOUNT_ID diff --git a/tests/integration/transaction_get_receipt_query_e2e_test.py b/tests/integration/transaction_get_receipt_query_e2e_test.py index a80608998..6874b6bde 100644 --- a/tests/integration/transaction_get_receipt_query_e2e_test.py +++ b/tests/integration/transaction_get_receipt_query_e2e_test.py @@ -13,6 +13,9 @@ not children count > 0. """ +from hiero_sdk_python.account.account_delete_transaction import AccountDeleteTransaction +from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.exceptions import ReceiptStatusError from hiero_sdk_python.transaction.transaction_id import TransactionId import pytest import threading @@ -20,6 +23,7 @@ from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.query.transaction_get_receipt_query import TransactionGetReceiptQuery from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt from hiero_sdk_python.transaction.transfer_transaction import TransferTransaction from tests.integration.utils import env @@ -218,4 +222,47 @@ def test_get_receipt_query_include_duplicates_execute_e2e(env): assert queried.status == ResponseCode.SUCCESS assert isinstance(queried.duplicates, list) - assert len(queried.duplicates) == 1 \ No newline at end of file + assert len(queried.duplicates) == 1 + +@pytest.mark.integration +def test_get_receipt_returns_failed_status_receipt_if_validate_status_false(env): + """Test receipt is returned despite failure when validate_status is False.""" + tx = ( + AccountDeleteTransaction() + .set_transfer_account_id(env.operator_id) + .set_account_id(AccountId(0, 0, 0)) + ) + response = tx.execute(env.client, wait_for_receipt=False) + + query = ( + TransactionGetReceiptQuery() + .set_transaction_id(response.transaction_id) + .set_validate_status(False) # Default value + ) + + receipt = query.execute(env.client) + assert isinstance(receipt, TransactionReceipt) + assert receipt.transaction_id == tx.transaction_id + assert receipt.status == ResponseCode.INVALID_ACCOUNT_ID + + +@pytest.mark.integration +def test_get_receipt_throws_status_error_when_validation_enabled(env): + """Test error is raised for failures when validate_status is True.""" + tx = ( + AccountDeleteTransaction() + .set_transfer_account_id(env.operator_id) + .set_account_id(AccountId(1, 0, 0)) + ) + response = tx.execute(env.client, wait_for_receipt=False) + + query = ( + TransactionGetReceiptQuery() + .set_transaction_id(response.transaction_id) + .set_validate_status(True) + ) + + with pytest.raises(ReceiptStatusError) as e: + query.execute(env.client) + + assert e.value.status == ResponseCode.INVALID_ACCOUNT_ID diff --git a/tests/integration/transaction_record_query_e2e_test.py b/tests/integration/transaction_record_query_e2e_test.py index 523938864..a0b24f291 100644 --- a/tests/integration/transaction_record_query_e2e_test.py +++ b/tests/integration/transaction_record_query_e2e_test.py @@ -1,11 +1,16 @@ -import pytest +"""Integration tests for TransactionRecordQuery end-to-end functionality.""" + import os -from hiero_sdk_python import AccountId, Client, TransactionRecord + +import pytest + +from hiero_sdk_python import AccountId, Timestamp, TransactionRecord +from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.hbar import Hbar -from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction from hiero_sdk_python.query.transaction_record_query import TransactionRecordQuery from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.schedule.schedule_id import ScheduleId from hiero_sdk_python.tokens.nft_id import NftId from hiero_sdk_python.tokens.token_associate_transaction import ( TokenAssociateTransaction, @@ -16,7 +21,6 @@ IntegrationTestEnv, create_fungible_token, create_nft_token, - env, ) @@ -41,6 +45,7 @@ def _submit_alias_auto_create_transfer(env: IntegrationTestEnv): @pytest.mark.integration def test_transaction_record_query_can_execute(): + """Test that a basic transaction record query can execute successfully.""" env = IntegrationTestEnv() try: @@ -75,41 +80,46 @@ def test_transaction_record_query_can_execute(): @pytest.mark.integration -def test_transaction_record_query_include_children_returns_child_records(env): +def test_transaction_record_query_include_children_returns_child_records(): """Querying an alias auto-create parent record should return parsed child records.""" - parent_transaction_id = _submit_alias_auto_create_transfer(env) - parent_account_id = parent_transaction_id.account_id - - parent_record = ( - TransactionRecordQuery() - .set_transaction_id(parent_transaction_id) - .set_include_children(True) - .execute(env.client) - ) - - assert parent_record.transaction_id == parent_transaction_id - assert parent_record.receipt.status == ResponseCode.SUCCESS - assert len(parent_record.children) > 0 - assert parent_record.transfers[parent_account_id] < 0 + env = IntegrationTestEnv() + try: + parent_transaction_id = _submit_alias_auto_create_transfer(env) + parent_account_id = parent_transaction_id.account_id - child_record = parent_record.children[0] - created_account_id = child_record.receipt.account_id + parent_record = ( + TransactionRecordQuery() + .set_transaction_id(parent_transaction_id) + .set_include_children(True) + .execute(env.client) + ) - assert isinstance(child_record, TransactionRecord) - assert child_record.receipt.status == ResponseCode.SUCCESS - assert child_record.transaction_id == parent_transaction_id - assert created_account_id is not None - assert created_account_id.shard == 0 - assert created_account_id.realm == 0 - assert created_account_id.num > 0 - assert child_record.transaction_hash != parent_record.transaction_hash - assert child_record.transaction_memo == "" - assert child_record.children == [] - assert child_record.duplicates == [] + assert parent_record.transaction_id == parent_transaction_id + assert parent_record.receipt.status == ResponseCode.SUCCESS + assert len(parent_record.children) > 0 + assert parent_record.transfers[parent_account_id] < 0 + + child_record = parent_record.children[0] + created_account_id = child_record.receipt.account_id + + assert isinstance(child_record, TransactionRecord) + assert child_record.receipt.status == ResponseCode.SUCCESS + assert child_record.transaction_id == parent_transaction_id + assert created_account_id is not None + assert created_account_id.shard == 0 + assert created_account_id.realm == 0 + assert created_account_id.num > 0 + assert child_record.transaction_hash != parent_record.transaction_hash + assert child_record.transaction_memo == "" + assert child_record.children == [] + assert child_record.duplicates == [] + finally: + env.close() @pytest.mark.integration def test_transaction_record_query_can_execute_nft_transfer(): + """Test that NFT transfers are correctly captured in the transaction record.""" env = IntegrationTestEnv() try: @@ -196,6 +206,7 @@ def test_transaction_record_query_can_execute_nft_transfer(): @pytest.mark.integration def test_transaction_record_query_can_execute_fungible_transfer(): + """Test that fungible token transfers are correctly captured in the record.""" env = IntegrationTestEnv() try: @@ -265,6 +276,7 @@ def test_transaction_record_query_can_execute_fungible_transfer(): ), ) def test_query_with_include_duplicates(): + """Verify that duplicate records are returned when the flag is enabled.""" env = IntegrationTestEnv() try: # Use a fresh keypair for isolation @@ -329,3 +341,83 @@ def test_query_with_include_duplicates(): finally: env.close() +@pytest.mark.integration +def test_transaction_record_new_fields(): + """Simple test to verify some fields in TransactionRecord. + + consensus_timestamp, automatic_token_associations, paid_staking_rewards, + evm_address, alias, ethereum_hash, parent_consensus_timestamp, + assessed_custom_fees, schedule_ref, and PRNG oneof handling. + """ + env = IntegrationTestEnv() + try: + new_account_private_key = PrivateKey.generate_ed25519() + new_account_public_key = new_account_private_key.public_key() + + receipt = ( + AccountCreateTransaction() + .set_key_without_alias(new_account_public_key) + .set_initial_balance(Hbar(1)) + .execute(env.client) + ) + assert receipt.status == ResponseCode.SUCCESS + + record: TransactionRecord = TransactionRecordQuery( + receipt.transaction_id + ).execute(env.client) + + _assert_basic_record_fields(record, receipt) + _assert_fields(record) + finally: + env.close() + +def _assert_basic_record_fields(record: TransactionRecord, receipt) -> None: + """Assert basic fields that existed before this PR.""" + assert record.transaction_id == receipt.transaction_id + assert record.transaction_fee > 0 + assert record.consensus_timestamp is not None + assert record.transaction_hash is not None and len(record.transaction_hash) > 0 + +def _assert_fields(record: TransactionRecord) -> None: + """Assert all newly exposed fields from this PR.""" + _assert_list_fields(record) + _assert_optional_bytes_fields(record) + _assert_timestamp_and_schedule_fields(record) + _assert_prng_fields(record) + _assert_token_associations(record) + +def _assert_list_fields(record: TransactionRecord) -> None: + """Assert list fields are properly initialized.""" + assert isinstance(record.automatic_token_associations, list) + assert isinstance(record.paid_staking_rewards, list) + assert isinstance(record.assessed_custom_fees, list) + +def _assert_optional_bytes_fields(record: TransactionRecord) -> None: + """Assert optional bytes fields.""" + assert record.evm_address is None or isinstance(record.evm_address, bytes) + assert record.alias is None or isinstance(record.alias, bytes) + assert record.ethereum_hash is None or isinstance(record.ethereum_hash, bytes) + +def _assert_timestamp_and_schedule_fields(record: TransactionRecord) -> None: + """Assert timestamp and schedule related fields.""" + assert record.parent_consensus_timestamp is None or isinstance( + record.parent_consensus_timestamp, Timestamp + ) + assert record.schedule_ref is None or isinstance(record.schedule_ref, ScheduleId) + +def _assert_prng_fields(record: TransactionRecord) -> None: + """Assert PRNG oneof handling.""" + assert not (record.prng_number is not None and record.prng_bytes is not None), \ + "prng_number and prng_bytes are mutually exclusive" + + if record.prng_number is not None: + assert isinstance(record.prng_number, int) + if record.prng_bytes is not None: + assert isinstance(record.prng_bytes, bytes) + +def _assert_token_associations(record: TransactionRecord) -> None: + """Assert TokenAssociation model fields.""" + for assoc in record.automatic_token_associations: + assert hasattr(assoc, "token_id") + assert hasattr(assoc, "account_id") + \ No newline at end of file diff --git a/tests/unit/tck/__init__.py b/tests/tck/__init__.py similarity index 100% rename from tests/unit/tck/__init__.py rename to tests/tck/__init__.py diff --git a/tests/unit/tck/client_manager_test.py b/tests/tck/client_utils_test.py similarity index 97% rename from tests/unit/tck/client_manager_test.py rename to tests/tck/client_utils_test.py index 9c1d852af..9ec7df376 100644 --- a/tests/unit/tck/client_manager_test.py +++ b/tests/tck/client_utils_test.py @@ -1,16 +1,16 @@ """Unit tests for the client manager module.""" from unittest.mock import MagicMock import pytest -from tck.client_manager import store_client, get_client, remove_client, _clients +from tck.util.client_utils import store_client, get_client, remove_client, _CLIENTS pytestmark = pytest.mark.unit @pytest.fixture(autouse=True) def clear_clients(): """Clear the clients registry before each test.""" - _clients.clear() + _CLIENTS.clear() yield - _clients.clear() + _CLIENTS.clear() class TestClientManager: diff --git a/tests/tck/common_params_test.py b/tests/tck/common_params_test.py new file mode 100644 index 000000000..83ac633b5 --- /dev/null +++ b/tests/tck/common_params_test.py @@ -0,0 +1,70 @@ +from unittest.mock import MagicMock + +import pytest + +from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction +from hiero_sdk_python.client.client import Client +from hiero_sdk_python.crypto.private_key import PrivateKey +from tck.param.common import CommonTransactionParams + + +pytestmark = pytest.mark.unit + + +@pytest.fixture +def json_params_dict(): + return { + 'transactionId': "0.0.90@1773846665.518000566", + "maxTransactionFee": "100000000", + "validTransactionDuration": "120", + "memo": "Test Memo", + "regenerateTransactionId": "False", + "signers": ["30540201010420d0b3d3c266ad9aa414f41e3050d64f4012765abc94a745cbd0607bf41da51a96a00706052b8104000aa124032200037aa11171d538daf5c624f313bc106fff289e4a24768880d0fa71dd302a1fa9e7"] + } + + +def test_parse_common_params(json_params_dict): + """Test the commonTransaction params can be parse form dict""" + params = CommonTransactionParams.parse_json_params(json_params_dict) + + assert isinstance(params.transactionId, str) + assert params.transactionId == "0.0.90@1773846665.518000566" + + assert isinstance(params.maxTransactionFee, int) + assert params.maxTransactionFee == 100000000 + + assert isinstance(params.memo, str) + assert params.memo == "Test Memo" + + assert isinstance(params.validTransactionDuration, int) + assert params.validTransactionDuration == 120 + + assert isinstance(params.regenerateTransactionId, bool) + assert params.regenerateTransactionId == False + + assert isinstance(params.signers, list) + assert len(params.signers) == 1 + assert params.signers[0] == "30540201010420d0b3d3c266ad9aa414f41e3050d64f4012765abc94a745cbd0607bf41da51a96a00706052b8104000aa124032200037aa11171d538daf5c624f313bc106fff289e4a24768880d0fa71dd302a1fa9e7" + + +def test_apply_common_params(json_params_dict): + """Test commonTransactionParams can be apply to transaction""" + params = CommonTransactionParams.parse_json_params(json_params_dict) + tx = ( + AccountCreateTransaction() + .set_key_without_alias(PrivateKey.generate()) + ) + + client = MagicMock(spec=Client) + + tx.freeze_with = MagicMock() + tx.sign = MagicMock() + + params.apply_common_params(tx, client) + + assert tx.memo == "Test Memo" + assert tx.transaction_valid_duration == 120 + assert str(tx.transaction_id) == "0.0.90@1773846665.518000566" + + tx.freeze_with.assert_called_once_with(client) + assert tx.sign.call_count == 1 \ No newline at end of file diff --git a/tests/unit/tck/conftest.py b/tests/tck/conftest.py similarity index 100% rename from tests/unit/tck/conftest.py rename to tests/tck/conftest.py diff --git a/tests/tck/handlers_test.py b/tests/tck/handlers_test.py new file mode 100644 index 000000000..06e2b7882 --- /dev/null +++ b/tests/tck/handlers_test.py @@ -0,0 +1,286 @@ +"""Test cases for the Hiero SDK TCK handlers registry and dispatch functionality.""" + +import pytest +from unittest.mock import MagicMock +from dataclasses import dataclass + +from hiero_sdk_python.exceptions import ( + PrecheckError, + ReceiptStatusError, + MaxAttemptsError, +) + +from tck.handlers.registry import ( + rpc_method, + get_handler, + get_all_handlers, + dispatch, + safe_dispatch, + parse_result, +) + +from tck.errors import ( + JsonRpcError, + METHOD_NOT_FOUND, + INTERNAL_ERROR, + HIERO_ERROR, + INVALID_PARAMS, +) + +from tck.handlers import registry + +pytestmark = pytest.mark.unit + + +@pytest.fixture(autouse=True) +def clear_handlers(): + """Clear the handlers registry before each test.""" + registry._HANDLERS.clear() + yield + registry._HANDLERS.clear() + + +class DummyParams: + """Mock params class used for dispatch tests.""" + + @classmethod + def parse_json_params(cls, params): + return params + + +@dataclass +class DummyResult: + """Dataclass used to test parse_result.""" + value: int | None = None + other: str | None = None + + +class TestHandlerRegistration: + + def test_handler_registration_via_decorator(self): + """Test that @rpc_method decorator registers handler.""" + + @rpc_method("test_method") + def handler(params: DummyParams): + return {"status": "ok"} + + handler_fn = get_handler("test_method") + + assert handler_fn is not None, "Expected handler to be registered" + + result = handler_fn({}) + assert result == {"status": "ok"}, "Expected handler result" + + + def test_get_all_handlers(self): + """Test retrieving all handlers.""" + + @rpc_method("method1") + def handler1(params: DummyParams): + return "result1" + + @rpc_method("method2") + def handler2(params: DummyParams): + return "result2" + + handlers = get_all_handlers() + + assert len(handlers) == 2, "Expected two handlers" + assert "method1" in handlers, "method1 missing" + assert "method2" in handlers, "method2 missing" + + + def test_get_nonexistent_handler_returns_none(self): + """Nonexistent handler should return None.""" + assert get_handler("missing") is None, "Expected None" + + + def test_handler_override(self): + """Second registration should overwrite first.""" + + @rpc_method("override") + def first(params: DummyParams): + return "first" + + @rpc_method("override") + def second(params: DummyParams): + return "second" + + handler = get_handler("override") + + assert handler({}) == "second", "Override failed" + + + def test_get_all_handlers_returns_copy(self): + """Returned dict should not modify registry.""" + + @rpc_method("protected") + def handler(params: DummyParams): + return "ok" + + handlers = get_all_handlers() + + handlers["protected"] = lambda: "bad" + handlers["injected"] = lambda: "bad" + + assert get_handler("protected")({}) == "ok", "Registry mutated" + assert get_handler("injected") is None, "Unexpected handler injected" + + +class TestDispatch: + + def test_dispatch_success(self): + """Dispatch should call registered handler.""" + + @rpc_method("setup") + def handler(params: DummyParams): + return DummyResult(value=1, other="value") + + params = DummyParams() + result = dispatch("setup", params) + + + assert result == {"value": 1, "other": "value"}, "Dispatch failed" + + + def test_dispatch_unknown_method(self): + """Unknown method should raise METHOD_NOT_FOUND.""" + + with pytest.raises(JsonRpcError) as excinfo: + dispatch("missing", DummyParams()) + + assert excinfo.value.code == METHOD_NOT_FOUND, "Expected METHOD_NOT_FOUND" + + + def test_dispatch_reraises_json_rpc_error(self): + """JsonRpcError should pass through.""" + + @rpc_method("error") + def handler(params: DummyParams): + raise JsonRpcError.invalid_params_error() + + with pytest.raises(JsonRpcError) as excinfo: + dispatch("error", DummyParams()) + + assert excinfo.value.code == INVALID_PARAMS, "Expected INVALID_PARAMS" + + + def test_dispatch_converts_generic_exception(self): + """Generic exception โ†’ INTERNAL_ERROR.""" + + @rpc_method("crash") + def handler(params: DummyParams): + raise ValueError("Boom") + + with pytest.raises(JsonRpcError) as excinfo: + dispatch("crash", DummyParams()) + + assert excinfo.value.code == INTERNAL_ERROR, "Expected INTERNAL_ERROR" + + + def test_dispatch_converts_precheck_error(self): + """PrecheckError โ†’ HIERO_ERROR.""" + + @rpc_method("precheck") + def handler(params: DummyParams): + raise PrecheckError( + status=1, + transaction_id="0.0.1", + message="failure", + ) + + with pytest.raises(JsonRpcError) as excinfo: + dispatch("precheck", DummyParams()) + + assert excinfo.value.code == HIERO_ERROR, "Expected HIERO_ERROR" + + + def test_dispatch_converts_receipt_status_error(self): + """ReceiptStatusError โ†’ HIERO_ERROR.""" + + @rpc_method("receipt") + def handler(params: DummyParams): + raise ReceiptStatusError( + status=1, + transaction_id=None, + transaction_receipt=MagicMock(), + message="fail", + ) + + with pytest.raises(JsonRpcError) as excinfo: + dispatch("receipt", DummyParams()) + + assert excinfo.value.code == HIERO_ERROR, "Expected HIERO_ERROR" + + + def test_dispatch_converts_max_attempts_error(self): + """MaxAttemptsError โ†’ HIERO_ERROR.""" + + @rpc_method("max_attempts") + def handler(params: DummyParams): + raise MaxAttemptsError("fail", node_id="0.0.1") + + with pytest.raises(JsonRpcError) as excinfo: + dispatch("max_attempts", DummyParams()) + + assert excinfo.value.code == HIERO_ERROR, "Expected HIERO_ERROR" + + +class TestSafeDispatch: + + def test_safe_dispatch_success(self): + """safe_dispatch should return raw result.""" + + @rpc_method("success") + def handler(params: DummyParams): + return DummyResult(value=1, other="value") + + result = safe_dispatch("success", {}, 1) + + assert result == {"value":1, "other":"value"}, "Unexpected result" + + + def test_safe_dispatch_json_error(self): + """JsonRpcError should produce JSON-RPC error response.""" + + @rpc_method("json_error") + def handler(params: DummyParams): + raise JsonRpcError.invalid_params_error(data="field") + + response = safe_dispatch("json_error", {}, 10) + + assert response["error"]["code"] == INVALID_PARAMS, "Expected INVALID_PARAMS" + + + def test_safe_dispatch_generic_exception(self): + """Generic exception โ†’ INTERNAL_ERROR.""" + + @rpc_method("generic_error") + def handler(params: DummyParams): + raise RuntimeError("unexpected") + + response = safe_dispatch("generic_error", {}, 2) + + assert response["error"]["code"] == INTERNAL_ERROR, "Expected INTERNAL_ERROR" + + + +class TestParseResult: + + def test_parse_result_dataclass(self): + """Dataclass should convert to dict.""" + + result = DummyResult(value=10, other="other") + + parsed = parse_result(result) + + assert parsed == {"value": 10, "other": "other"}, "Expected filtered dataclass result" + + def test_parse_result_dataclass_ignore_none(self): + """Dataclass should convert to dict without None values.""" + + result = DummyResult(value=10, other=None) + + parsed = parse_result(result) + + assert parsed == {"value": 10}, "Expected filtered dataclass result" \ No newline at end of file diff --git a/tests/unit/tck/protocol_test.py b/tests/tck/protocol_test.py similarity index 100% rename from tests/unit/tck/protocol_test.py rename to tests/tck/protocol_test.py diff --git a/tests/unit/account_create_transaction_test.py b/tests/unit/account_create_transaction_test.py index 1c894d9a7..ac8d2365d 100644 --- a/tests/unit/account_create_transaction_test.py +++ b/tests/unit/account_create_transaction_test.py @@ -544,3 +544,32 @@ def test_set_key_with_alias_then_without_alias_overrides_alias(): tx.set_key_without_alias(account_private_key) assert tx.alias is None + +def test_set_stack_node_id_reset_stake_account_id(): + """Test set_node_id reset stake_account_id if stake account id is set.""" + tx = ( + AccountCreateTransaction() + .set_staked_account_id(AccountId(0,0,1)) + ) + + assert tx.staked_account_id == AccountId(0,0,1) + assert tx.staked_node_id == None + + tx.set_staked_node_id(1) + + assert tx.staked_node_id == 1 + assert tx.staked_account_id is None + +def test_set_stake_account_id_reset_stake_node_id(): + """Test set_account_id reset stake_node_id if node id is set.""" + tx = ( + AccountCreateTransaction() + .set_staked_node_id(1) + ) + + assert tx.staked_node_id == 1 + assert tx.staked_account_id is None + + tx.set_staked_account_id(AccountId(0,0,1)) + assert tx.staked_account_id == AccountId(0,0,1) + assert tx.staked_node_id is None diff --git a/tests/unit/account_info_test.py b/tests/unit/account_info_test.py index 4687731cc..5901b529c 100644 --- a/tests/unit/account_info_test.py +++ b/tests/unit/account_info_test.py @@ -2,6 +2,7 @@ from hiero_sdk_python.account.account_info import AccountInfo from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.staking_info import StakingInfo from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.hbar import Hbar from hiero_sdk_python.Duration import Duration @@ -9,6 +10,8 @@ from hiero_sdk_python.tokens.token_relationship import TokenRelationship from hiero_sdk_python.tokens.token_id import TokenId from hiero_sdk_python.hapi.services.crypto_get_info_pb2 import CryptoGetInfoResponse +from hiero_sdk_python.hapi.services.basic_types_pb2 import StakingInfo as StakingInfoProto +from hiero_sdk_python.hapi.services.timestamp_pb2 import Timestamp as TimestampProto pytestmark = pytest.mark.unit @@ -34,6 +37,13 @@ def account_info(): @pytest.fixture def proto_account_info(): public_key = PrivateKey.generate_ed25519().public_key() + staking_info_proto = StakingInfoProto( + decline_reward=True, + pending_reward=500, + staked_to_me=1000, + ) + staking_info_proto.staked_account_id.CopyFrom(AccountId(0, 0, 200)._to_proto()) + staking_info_proto.stake_period_start.CopyFrom(TimestampProto(seconds=1625097600, nanos=0)) proto = CryptoGetInfoResponse.AccountInfo( accountID=AccountId(0, 0, 100)._to_proto(), contractAccountID="0.0.100", @@ -48,6 +58,7 @@ def proto_account_info(): memo="Test account memo", ownedNfts=5, ) + proto.staking_info.CopyFrom(staking_info_proto) return proto @@ -100,6 +111,12 @@ def test_from_proto(proto_account_info): assert account_info.token_relationships == [] assert account_info.account_memo == "Test account memo" assert account_info.owned_nfts == 5 + assert account_info.staking_info is not None + assert account_info.staking_info.decline_reward is True + assert account_info.staking_info.pending_reward.to_tinybars() == 500 + assert account_info.staking_info.staked_to_me.to_tinybars() == 1000 + assert account_info.staking_info.staked_account_id == AccountId(0, 0, 200) + assert account_info.staking_info.stake_period_start == Timestamp(1625097600, 0) def test_from_proto_with_token_relationships(): @@ -127,6 +144,13 @@ def test_from_proto_none_raises_error(): def test_to_proto(account_info): """Test the to_proto method of the AccountInfo class""" + account_info.staking_info = StakingInfo( + pending_reward=Hbar.from_tinybars(500), + staked_to_me=Hbar.from_tinybars(1000), + stake_period_start=Timestamp(1625097600, 0), + staked_account_id=AccountId(0, 0, 200), + decline_reward=True, + ) proto = account_info._to_proto() assert proto.accountID == AccountId(0, 0, 100)._to_proto() @@ -141,6 +165,12 @@ def test_to_proto(account_info): assert proto.tokenRelationships == [] assert proto.memo == "Test account memo" assert proto.ownedNfts == 5 + assert proto.HasField('staking_info') + assert proto.staking_info.decline_reward is True + assert proto.staking_info.pending_reward == 500 + assert proto.staking_info.staked_to_me == 1000 + assert proto.staking_info.HasField('staked_account_id') + assert proto.staking_info.staked_account_id == AccountId(0, 0, 200)._to_proto() def test_to_proto_with_none_values(): @@ -192,6 +222,32 @@ def test_proto_conversion(account_info): ) assert converted_account_info.account_memo == account_info.account_memo assert converted_account_info.owned_nfts == account_info.owned_nfts + assert converted_account_info.staking_info == account_info.staking_info + + +def test_proto_conversion_with_staking_info(): + """Test converting AccountInfo with staking_info to proto and back preserves data""" + original = AccountInfo( + account_id=AccountId(0, 0, 100), + balance=Hbar.from_tinybars(5000000), + key=PrivateKey.generate_ed25519().public_key(), + staking_info=StakingInfo( + pending_reward=Hbar.from_tinybars(300), + staked_to_me=Hbar.from_tinybars(700), + stake_period_start=Timestamp(1625097600, 0), + staked_account_id=AccountId(0, 0, 200), + decline_reward=True, + ), + ) + proto = original._to_proto() + restored = AccountInfo._from_proto(proto) + + assert restored.staking_info is not None + assert restored.staking_info.decline_reward is True + assert restored.staking_info.pending_reward.to_tinybars() == 300 + assert restored.staking_info.staked_to_me.to_tinybars() == 700 + assert restored.staking_info.staked_account_id == AccountId(0, 0, 200) + assert restored.staking_info.stake_period_start == Timestamp(1625097600, 0) def test_str_and_repr(account_info): @@ -211,3 +267,131 @@ def test_str_and_repr(account_info): assert "contract_account_id='0.0.100'" in info_repr assert "account_memo='Test account memo'" in info_repr + +# --------------------------------------------------------------------------- +# Deprecated flat property tests on AccountInfo +# --------------------------------------------------------------------------- + +class TestDeprecatedProperties: + """Ensure the deprecated flat staking properties still work with warnings.""" + + def _make_info(self): + si = StakingInfo( + staked_account_id=AccountId(0, 0, 10), + decline_reward=True, + ) + return AccountInfo(staking_info=si) + + def test_staked_account_id_deprecated(self): + import warnings + info = self._make_info() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + val = info.staked_account_id + assert val == AccountId(0, 0, 10) + assert len(w) == 1 + assert issubclass(w[0].category, DeprecationWarning) + assert "staked_account_id" in str(w[0].message) + + def test_staked_node_id_deprecated(self): + import warnings + info = self._make_info() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + val = info.staked_node_id + assert val is None + assert issubclass(w[0].category, DeprecationWarning) + + def test_decline_staking_reward_deprecated(self): + import warnings + info = self._make_info() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + val = info.decline_staking_reward + assert val is True + assert issubclass(w[0].category, DeprecationWarning) + + def test_deprecated_properties_return_none_without_staking_info(self): + import warnings + info = AccountInfo() + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + assert info.staked_account_id is None + assert info.staked_node_id is None + assert info.decline_staking_reward is None + + def test_staked_account_id_setter(self): + import warnings + info = AccountInfo() + new_id = AccountId(0, 0, 50) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + info.staked_account_id = new_id + assert info.staking_info is not None + assert info.staking_info.staked_account_id == new_id + assert any(issubclass(x.category, DeprecationWarning) for x in w) + + def test_staked_node_id_setter(self): + import warnings + info = AccountInfo() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + info.staked_node_id = 42 + assert info.staking_info is not None + assert info.staking_info.staked_node_id == 42 + assert any(issubclass(x.category, DeprecationWarning) for x in w) + + def test_decline_staking_reward_setter(self): + import warnings + info = AccountInfo() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + info.decline_staking_reward = True + assert info.staking_info is not None + assert info.staking_info.decline_reward is True + assert any(issubclass(x.category, DeprecationWarning) for x in w) + + def test_setters_preserve_other_staking_fields(self): + import warnings + si = StakingInfo( + pending_reward=Hbar.from_tinybars(100), + staked_to_me=Hbar.from_tinybars(200), + staked_account_id=AccountId(0, 0, 10), + decline_reward=True, + ) + info = AccountInfo(staking_info=si) + with warnings.catch_warnings(record=True): + warnings.simplefilter("always") + # Setting staked_node_id clears staked_account_id due to oneof constraint + info.staked_node_id = 5 + # Other non-oneof fields should be preserved + assert info.staking_info.pending_reward.to_tinybars() == 100 + assert info.staking_info.staked_to_me.to_tinybars() == 200 + assert info.staking_info.decline_reward is True + # Conflicting field cleared, new value set + assert info.staking_info.staked_account_id is None + assert info.staking_info.staked_node_id == 5 + + def test_constructor_legacy_kwargs_deprecated(self): + """Legacy staking kwargs in AccountInfo() emit DeprecationWarning but still work.""" + import warnings + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + info = AccountInfo(staked_account_id=AccountId(0, 0, 1)) + assert info.staking_info is not None + assert info.staking_info.staked_account_id == AccountId(0, 0, 1) + assert any(issubclass(x.category, DeprecationWarning) for x in w) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + info = AccountInfo(staked_node_id=7) + assert info.staking_info.staked_node_id == 7 + assert any(issubclass(x.category, DeprecationWarning) for x in w) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + info = AccountInfo(decline_staking_reward=True) + assert info.staking_info.decline_reward is True + assert any(issubclass(x.category, DeprecationWarning) for x in w) + diff --git a/tests/unit/account_update_transaction_test.py b/tests/unit/account_update_transaction_test.py index 62b3c5453..b5e978c0e 100644 --- a/tests/unit/account_update_transaction_test.py +++ b/tests/unit/account_update_transaction_test.py @@ -16,6 +16,7 @@ AccountUpdateParams, AccountUpdateTransaction, ) +from hiero_sdk_python.crypto.key_list import KeyList from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.hapi.services import ( response_header_pb2, @@ -826,3 +827,44 @@ def test_build_transaction_body_with_cleared_staking(mock_account_ids): txn_body = account_tx.build_transaction_body().cryptoUpdateAccount assert txn_body.staked_node_id == -1 assert not txn_body.HasField("staked_account_id") + +def test_set_keylist_multiple_keys(): + """Verify that a generic KeyList is properly converted to protobuf in AccountUpdateTransaction.""" + tx = AccountUpdateTransaction() + tx.set_account_id(AccountId.from_string("0.0.123")) + + key1 = PrivateKey.generate_ed25519().public_key() + key2 = PrivateKey.generate_ed25519().public_key() + + # Create a KeyList with two public keys + key_list = KeyList([key1, key2]) + tx.set_key(key_list) + + proto_body = tx._build_proto_body() + update_body = proto_body + + # Assert that the protobuf key has a keyList set + assert update_body.key.HasField("keyList") + assert len(update_body.key.keyList.keys) == 2 + + +def test_set_keylist_threshold_key(): + """Verify that a KeyList with a threshold is properly converted to a thresholdKey protobuf.""" + tx = AccountUpdateTransaction() + tx.set_account_id(AccountId.from_string("0.0.123")) + + key1 = PrivateKey.generate_ed25519().public_key() + key2 = PrivateKey.generate_ed25519().public_key() + key3 = PrivateKey.generate_ed25519().public_key() + + # Create a threshold key (requires 2 of 3 signatures) + threshold_key = KeyList([key1, key2, key3], threshold=2) + tx.set_key(threshold_key) + + proto_body = tx._build_proto_body() + update_body = proto_body + + # Assert that the protobuf key has a thresholdKey set + assert update_body.key.HasField("thresholdKey") + assert update_body.key.thresholdKey.threshold == 2 + assert len(update_body.key.thresholdKey.keys.keys) == 3 diff --git a/tests/unit/contract_id_test.py b/tests/unit/contract_id_test.py index 67cec8a33..4886d569c 100644 --- a/tests/unit/contract_id_test.py +++ b/tests/unit/contract_id_test.py @@ -7,6 +7,7 @@ import pytest from hiero_sdk_python.contract.contract_id import ContractId +from hiero_sdk_python.crypto.key import Key from hiero_sdk_python.hapi.services import basic_types_pb2 pytestmark = pytest.mark.unit @@ -568,3 +569,14 @@ def test_populate_contract_num_invalid_mirror_response(client): ValueError, match="Mirror node response missing 'contract_id'" ): contract_id.populate_contract_num(client) + +def test_to_proto_key(): + """Test to_proto_key returns the Key protobuf.""" + contract_id = ContractId(shard=0, realm=0, contract=1) + key = contract_id.to_proto_key() + + assert key is not None + assert key.contractID is not None + assert key.contractID.shardNum == contract_id.shard + assert key.contractID.realmNum == contract_id.realm + assert key.contractID.contractNum == contract_id.contract diff --git a/tests/unit/delegate_contract_id_test.py b/tests/unit/delegate_contract_id_test.py new file mode 100644 index 000000000..4c14920ac --- /dev/null +++ b/tests/unit/delegate_contract_id_test.py @@ -0,0 +1,609 @@ +""" +Unit tests for the DelegateContractId class. +""" + +import struct +from unittest.mock import patch +import pytest + +from hiero_sdk_python.contract.delegate_contract_id import DelegateContractId +from hiero_sdk_python.crypto.key import Key +from hiero_sdk_python.hapi.services import basic_types_pb2 + +pytestmark = pytest.mark.unit + + +@pytest.fixture +def client(mock_client): + mock_client.network.ledger_id = bytes.fromhex("00") # mainnet ledger id + return mock_client + + +def test_default_initialization(): + """Test DelegateContractId initialization with default values.""" + contract_id = DelegateContractId() + + assert isinstance(contract_id, DelegateContractId) + assert contract_id.shard == 0 + assert contract_id.realm == 0 + assert contract_id.contract == 0 + assert contract_id.evm_address is None + assert contract_id.checksum is None + + +def test_custom_initialization(): + """Test DelegateContractId initialization with custom values.""" + contract_id = DelegateContractId(shard=1, realm=2, contract=3) + + assert isinstance(contract_id, DelegateContractId) + assert contract_id.shard == 1 + assert contract_id.realm == 2 + assert contract_id.contract == 3 + assert contract_id.evm_address is None + assert contract_id.checksum is None + + +def test_str_representation(): + """Test string representation of DelegateContractId.""" + contract_id = DelegateContractId(shard=1, realm=2, contract=3) + + assert isinstance(contract_id, DelegateContractId) + assert str(contract_id) == "1.2.3" + assert contract_id.evm_address is None + assert contract_id.checksum is None + + +def test_str_representation_default(): + """Test string representation of DelegateContractId with default values.""" + contract_id = DelegateContractId() + + assert isinstance(contract_id, DelegateContractId) + assert str(contract_id) == "0.0.0" + assert contract_id.evm_address is None + assert contract_id.checksum is None + + +@pytest.mark.parametrize( + "contract_str, expected", + [ + ("1.2.101", (1, 2, 101, None, None)), + ("0.0.0", (0, 0, 0, None, None)), + ("1.2.3-abcde", (1, 2, 3, None, "abcde")), + ( + "1.2.abcdef0123456789abcdef0123456789abcdef01", + (1, 2, 0, bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01"), None), + ), + ], +) +def test_from_string_for_valid_str(contract_str, expected): + """Test creating DelegateContractId from valid string format.""" + shard, realm, contract, evm_address, checksum = expected + + contract_id = DelegateContractId.from_string(contract_str) + + assert isinstance(contract_id, DelegateContractId) + assert contract_id.shard == shard + assert contract_id.realm == realm + assert contract_id.contract == contract + assert contract_id.evm_address == evm_address + assert contract_id.checksum == checksum + + +@pytest.mark.parametrize( + "invalid_id", + [ + "1.2", # Too few parts + "1.2.3.4", # Too many parts + "a.b.c", # Non-numeric parts + "", # Empty string + "1.a.3", # Partial numeric + "0.0.-1", + "abc.def.ghi", + "0.0.1-ad", + "0.0.1-addefgh", + "0.0.1 - abcde", + " 0.0.100 ", + " 1.2.abcdef0123456789abcdef0123456789abcdef01 ", + "1.2.0xabcdef0123456789abcdef0123456789abcdef01", + "1.2.001122334455667788990011223344556677", + "1.2.000000000000000000000000000000000000000000", + ], +) +def test_from_string_for_invalid_format(invalid_id): + """Should raise error when creating DelegateContractId from invalid string input.""" + with pytest.raises( + ValueError, + match=f"Invalid contract ID string '{invalid_id}'. Expected format 'shard.realm.contract'.", + ): + DelegateContractId.from_string(invalid_id) + + +@pytest.mark.parametrize("invalid_id", [None, 123, True, object, {}]) +def test_from_string_for_invalid_type(invalid_id): + """Should raise error when creating DelegateContractId from invalid input type.""" + with pytest.raises( + TypeError, + match=f"contract_id_str must be of type str, got {type(invalid_id).__name__}", + ): + DelegateContractId.from_string(invalid_id) + + +def test_to_proto(): + """Test converting DelegateContractId to protobuf format.""" + contract_id = DelegateContractId(shard=1, realm=2, contract=3) + proto = contract_id._to_proto() + + assert isinstance(proto, basic_types_pb2.ContractID) + assert proto.shardNum == 1 + assert proto.realmNum == 2 + assert proto.contractNum == 3 + + +def test_to_proto_default_values(): + """Test converting DelegateContractId with default values to protobuf format.""" + contract_id = DelegateContractId() + proto = contract_id._to_proto() + + assert isinstance(proto, basic_types_pb2.ContractID) + assert proto.shardNum == 0 + assert proto.realmNum == 0 + assert proto.contractNum == 0 + + +def test_from_proto(): + """Test creating DelegateContractId from protobuf format.""" + proto = basic_types_pb2.ContractID(shardNum=1, realmNum=2, contractNum=3) + + contract_id = DelegateContractId._from_proto(proto) + + assert isinstance(contract_id, DelegateContractId) + assert contract_id.shard == 1 + assert contract_id.realm == 2 + assert contract_id.contract == 3 + assert contract_id.evm_address is None + + +def test_from_proto_with_evm_address(): + """Test creating DelegateContractId from protobuf with EVM address set.""" + evm_address = bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01") + proto = basic_types_pb2.ContractID( + shardNum=1, + realmNum=2, + evm_address=evm_address, + ) + + contract_id = DelegateContractId._from_proto(proto) + + assert isinstance(contract_id, DelegateContractId) + assert contract_id.shard == 1 + assert contract_id.realm == 2 + assert contract_id.contract == 0 + assert contract_id.evm_address == evm_address + + +def test_from_proto_zero_values(): + """Test creating DelegateContractId from protobuf format with zero values.""" + proto = basic_types_pb2.ContractID(shardNum=0, realmNum=0, contractNum=0) + + contract_id = DelegateContractId._from_proto(proto) + + assert isinstance(contract_id, DelegateContractId) + assert contract_id.shard == 0 + assert contract_id.realm == 0 + assert contract_id.contract == 0 + assert contract_id.evm_address is None + + +def test_roundtrip_proto_conversion(): + """Test that converting to proto and back preserves values.""" + original = DelegateContractId(shard=5, realm=10, contract=15) + proto = original._to_proto() + reconstructed = DelegateContractId._from_proto(proto) + + assert original.shard == reconstructed.shard + assert original.realm == reconstructed.realm + assert original.contract == reconstructed.contract + + +def test_roundtrip_string_conversion(): + """Test that converting to string and back preserves values.""" + original = DelegateContractId(shard=7, realm=14, contract=21) + string_repr = str(original) + reconstructed = DelegateContractId.from_string(string_repr) + + assert original.shard == reconstructed.shard + assert original.realm == reconstructed.realm + assert original.contract == reconstructed.contract + + +def test_equality(): + """Test DelegateContractId equality comparison.""" + contract_id1 = DelegateContractId(shard=1, realm=2, contract=3) + contract_id2 = DelegateContractId(shard=1, realm=2, contract=3) + contract_id3 = DelegateContractId(shard=1, realm=2, contract=4) + + assert contract_id1 == contract_id2 + assert contract_id1 != contract_id3 + + +def test_evm_address_initialization(): + """Test DelegateContractId initialization with EVM address.""" + evm_address = bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01") + contract_id = DelegateContractId( + shard=1, realm=2, contract=3, evm_address=evm_address + ) + + assert contract_id.shard == 1 + assert contract_id.realm == 2 + assert contract_id.contract == 3 + assert contract_id.evm_address == evm_address + + +def test_evm_address_to_proto(): + """Test converting DelegateContractId with EVM address to protobuf format.""" + evm_address = bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01") + contract_id = DelegateContractId( + shard=1, realm=2, contract=3, evm_address=evm_address + ) + proto = contract_id._to_proto() + + assert isinstance(proto, basic_types_pb2.ContractID) + assert proto.shardNum == 1 + assert proto.realmNum == 2 + assert proto.contractNum == 0 + assert proto.evm_address == evm_address + + +def test_evm_address_to_proto_none(): + """Test converting DelegateContractId with None EVM address to protobuf format.""" + contract_id = DelegateContractId(shard=1, realm=2, contract=3, evm_address=None) + proto = contract_id._to_proto() + + assert isinstance(proto, basic_types_pb2.ContractID) + assert proto.shardNum == 1 + assert proto.realmNum == 2 + assert proto.contractNum == 3 + assert proto.evm_address == b"" + + +def test_evm_address_equality(): + """Test DelegateContractId equality with EVM addresses.""" + evm_address1 = bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01") + evm_address2 = bytes.fromhex("1234567890abcdef1234567890abcdef12345678") + + contract_id1 = DelegateContractId( + shard=1, realm=2, contract=3, evm_address=evm_address1 + ) + contract_id2 = DelegateContractId( + shard=1, realm=2, contract=3, evm_address=evm_address1 + ) + contract_id3 = DelegateContractId( + shard=1, realm=2, contract=3, evm_address=evm_address2 + ) + contract_id4 = DelegateContractId(shard=1, realm=2, contract=3, evm_address=None) + + # Same EVM address should be equal + assert contract_id1 == contract_id2 + + # Different EVM addresses should not be equal + assert contract_id1 != contract_id3 + + # None EVM address should not be equal to one with EVM address + assert contract_id1 != contract_id4 + + +def test_evm_address_hash(): + """Test DelegateContractId hash with EVM addresses.""" + evm_address1 = bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01") + evm_address2 = bytes.fromhex("1234567890abcdef1234567890abcdef12345678") + + contract_id1 = DelegateContractId( + shard=1, realm=2, contract=3, evm_address=evm_address1 + ) + contract_id2 = DelegateContractId( + shard=1, realm=2, contract=3, evm_address=evm_address1 + ) + contract_id3 = DelegateContractId( + shard=1, realm=2, contract=3, evm_address=evm_address2 + ) + + # Same EVM address should have same hash + assert hash(contract_id1) == hash(contract_id2) + + # Different EVM addresses should have different hashes + assert hash(contract_id1) != hash(contract_id3) + + +def test_to_evm_address(): + """Test DelegateContractId.to_evm_address() for both explicit and computed EVM addresses.""" + # Explicit EVM address + evm_address = bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01") + contract_id = DelegateContractId( + shard=1, realm=2, contract=3, evm_address=evm_address + ) + assert contract_id.to_evm_address() == evm_address.hex() + + # Computed EVM address (no explicit evm_address) + contract_id = DelegateContractId(shard=1, realm=2, contract=3) + expected_bytes = struct.pack( + ">iqq", contract_id.shard, contract_id.realm, contract_id.contract + ) + assert contract_id.to_evm_address() == expected_bytes.hex() + + # Default values + contract_id = DelegateContractId() + expected_bytes = struct.pack( + ">iqq", contract_id.shard, contract_id.realm, contract_id.contract + ) + assert contract_id.to_evm_address() == expected_bytes.hex() + + +def test_str_representation_with_checksum(client): + """Should return string representation with checksum""" + contract_id = DelegateContractId.from_string("0.0.1") + assert contract_id.to_string_with_checksum(client) == "0.0.1-dfkxr" + + +def test_str_representation_checksum_with_evm_address(client): + """Should raise error on to_string_with_checksum is called when evm_address is set""" + contract_id = DelegateContractId.from_string( + "0.0.abcdef0123456789abcdef0123456789abcdef01" + ) + + with pytest.raises( + ValueError, + match="to_string_with_checksum cannot be applied to DelegateContractId with evm_address", + ): + contract_id.to_string_with_checksum(client) + + +def test_validate_checksum_success(client): + """Should pass checksum validation when checksum is correct.""" + contract_id = DelegateContractId.from_string("0.0.1-dfkxr") + contract_id.validate_checksum(client) + + +def test_validate_checksum_failure(client): + """Should raise ValueError if checksum validation fails.""" + contract_id = DelegateContractId.from_string("0.0.1-wronx") + + with pytest.raises(ValueError, match="Checksum mismatch for 0.0.1"): + contract_id.validate_checksum(client) + + +def test_str_representation_with_evm_address(): + """Should return str representing with evm_address""" + contract_id = DelegateContractId.from_string( + "0.0.abcdef0123456789abcdef0123456789abcdef01" + ) + assert contract_id.__str__() == "0.0.abcdef0123456789abcdef0123456789abcdef01" + + +def test_contract_id_repr_numeric(): + """Test __repr__ output for numeric contract ID.""" + contract_id = DelegateContractId(0, 0, 12345) + expected = "DelegateContractId(shard=0, realm=0, contract=12345)" + assert repr(contract_id) == expected + + +def test_contract_id_repr_evm_address(): + """Test __repr__ output for EVM-based contract ID.""" + evm_bytes = bytes.fromhex("a" * 40) + contract_id = DelegateContractId(1, 2, evm_address=evm_bytes) + expected = f"DelegateContractId(shard=1, realm=2, evm_address={evm_bytes.hex()})" + assert repr(contract_id) == expected + + +def test_to_string_with_checksum_missing_ledger_id(mock_client): + """Should raise error if client has no ledger ID.""" + mock_client.network.ledger_id = None + contract_id = DelegateContractId.from_string("0.0.1") + + with pytest.raises(ValueError, match="Missing ledger ID"): + contract_id.to_string_with_checksum(mock_client) + + +@pytest.mark.parametrize( + "evm_address_str, expected", + [ + ( + "abcdef0123456789abcdef0123456789abcdef01", + (0, 0, 0, bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01")), + ), + ( + "0xabcdef0123456789abcdef0123456789abcdef01", + (0, 0, 0, bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01")), + ), + ], +) +def test_from_evm_address_valid_params(evm_address_str, expected): + """Test from_evm_address with valid EVM address strings.""" + shard, realm, contract, evm_address = expected + + contract_id = DelegateContractId.from_evm_address(0, 0, evm_address_str) + + assert isinstance(contract_id, DelegateContractId) + assert contract_id.shard == shard + assert contract_id.realm == realm + assert contract_id.contract == contract + assert contract_id.evm_address == evm_address + assert contract_id.checksum is None + + +@pytest.mark.parametrize( + "invalid_address", + [ + "abcdef0123456789abcdef0123456789abcdef", # less than 20 bytes + "abcdef0123456789abcdef0123456789abcdef1010101", # greater than 20 bytes + "abcd-123sjd", # invalid format + "0xZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ", # invalid hex + ], +) +def test_from_evm_address_invalid_evm_address_str(invalid_address): + """Test from_evm_address raise error for invalid EVM address strings.""" + with pytest.raises(ValueError, match=f"Invalid EVM address: {invalid_address}"): + DelegateContractId.from_evm_address(0, 0, invalid_address) + + +@pytest.mark.parametrize( + "invalid_address", + [None, 1234, True, object, {}], +) +def test_from_evm_address_invalid_evm_address_type(invalid_address): + """Test from_evm_address raise error for non-string EVM address inputs.""" + with pytest.raises( + TypeError, + match=f"evm_address must be of type str, got {type(invalid_address).__name__}", + ): + DelegateContractId.from_evm_address(0, 0, invalid_address) + + +@pytest.mark.parametrize( + "invalid_shard", + [None, "123", True, object, {}], +) +def test_from_evm_address_invalid_shard_type(invalid_shard): + """Test from_evm_address raise error for invalid shard types.""" + with pytest.raises( + TypeError, match=f"shard must be int, got {type(invalid_shard).__name__}" + ): + DelegateContractId.from_evm_address( + invalid_shard, 0, "abcdef0123456789abcdef0123456789abcdef01" + ) + + +def test_from_evm_address_negative_shard_value(): + """Test from_evm_address raise error for negative shard values.""" + with pytest.raises(ValueError, match="shard must be a non-negative integer"): + DelegateContractId.from_evm_address( + -1, 0, "abcdef0123456789abcdef0123456789abcdef01" + ) + + +@pytest.mark.parametrize( + "invalid_realm", + [None, "123", True, object, {}], +) +def test_from_evm_address_invalid_realm_type(invalid_realm): + """Test from_evm_address raise error for invalid realm types.""" + with pytest.raises( + TypeError, match=f"realm must be int, got {type(invalid_realm).__name__}" + ): + DelegateContractId.from_evm_address( + 0, invalid_realm, "abcdef0123456789abcdef0123456789abcdef01" + ) + + +def test_from_evm_address_negative_realm_value(): + """Test from_evm_address raise error for negative realm values.""" + with pytest.raises(ValueError, match="realm must be a non-negative integer"): + DelegateContractId.from_evm_address( + 0, -1, "abcdef0123456789abcdef0123456789abcdef01" + ) + + +def test_from_bytes_success(): + """Should deserialize DelegateContractId correctly from protobuf bytes.""" + original = DelegateContractId(shard=1, realm=2, contract=3) + data = original.to_bytes() + + reconstructed = DelegateContractId.from_bytes(data) + + assert reconstructed == original + + +@pytest.mark.parametrize("invalid_data", [None, "abc", 123, object()]) +def test_from_bytes_invalid_type(invalid_data): + """Should raise TypeError when from_bytes receives non-bytes input.""" + with pytest.raises(TypeError, match="data must be bytes"): + DelegateContractId.from_bytes(invalid_data) + + +def test_from_bytes_invalid_payload(): + """Should raise ValueError when protobuf deserialization fails.""" + with pytest.raises(ValueError, match="Failed to deserialize ContractId from bytes"): + DelegateContractId.from_bytes(b"\x00\x01\x02") + + +def test_populate_contract_num_success(client): + """Should populate contract number using mirror node response.""" + evm_address = bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01") + contract_id = DelegateContractId(shard=0, realm=0, evm_address=evm_address) + + with patch( + "hiero_sdk_python.contract.contract_id.perform_query_to_mirror_node", + return_value={"contract_id": "0.0.123"}, + ): + populated = contract_id.populate_contract_num(client) + + assert populated.contract == 123 + assert populated.evm_address == evm_address + + +def test_populate_contract_num_invalid_response(client): + """Should raise error when populating contract number invalid response.""" + evm_address = bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01") + contract_id = DelegateContractId(shard=0, realm=0, evm_address=evm_address) + + with patch( + "hiero_sdk_python.contract.contract_id.perform_query_to_mirror_node", + return_value={"contract_id": "invalid.account.format"}, + ): + with pytest.raises( + ValueError, + match="Invalid contract_id format received: invalid.account.format", + ): + contract_id.populate_contract_num(client) + + +def test_populate_contract_num_query_fails(client): + """Should raise error when populating contract number query fails.""" + evm_address = bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01") + contract_id = DelegateContractId(shard=0, realm=0, evm_address=evm_address) + + with patch( + "hiero_sdk_python.contract.contract_id.perform_query_to_mirror_node", + side_effect=RuntimeError("mirror node query error"), + ): + with pytest.raises( + RuntimeError, + match="Failed to populate contract num from mirror node for evm_address abcdef0123456789abcdef0123456789abcdef01", + ): + contract_id.populate_contract_num(client) + + +def test_populate_contract_num_without_evm_address(client): + """Should raise error when populate_contract_num is called without evm_address.""" + contract_id = DelegateContractId(shard=0, realm=0, contract=1) + + with pytest.raises( + ValueError, match="evm_address is required to populate the contract number" + ): + contract_id.populate_contract_num(client) + + +def test_populate_contract_num_invalid_mirror_response(client): + """Should raise error if mirror node response is missing contract_id.""" + evm_address = bytes.fromhex("abcdef0123456789abcdef0123456789abcdef01") + contract_id = DelegateContractId(shard=0, realm=0, evm_address=evm_address) + + with patch( + "hiero_sdk_python.contract.contract_id.perform_query_to_mirror_node", + return_value={}, + ): + with pytest.raises( + ValueError, match="Mirror node response missing 'contract_id'" + ): + contract_id.populate_contract_num(client) + + +def test_to_proto_key(): + """Test to_proto_key returns the Key protobuf.""" + contract_id = DelegateContractId(shard=0, realm=0, contract=1) + key = contract_id.to_proto_key() + + assert key is not None + assert key.delegatable_contract_id is not None + assert key.delegatable_contract_id.shardNum == contract_id.shard + assert key.delegatable_contract_id.realmNum == contract_id.realm + assert key.delegatable_contract_id.contractNum == contract_id.contract diff --git a/tests/unit/evm_address_test.py b/tests/unit/evm_address_test.py index 82cba0cf5..61c565276 100644 --- a/tests/unit/evm_address_test.py +++ b/tests/unit/evm_address_test.py @@ -1,3 +1,5 @@ +import re + import pytest from hiero_sdk_python.crypto.evm_address import EvmAddress @@ -59,3 +61,12 @@ def test_equality(): addr2 = EvmAddress.from_string("0x" + raw.hex()) assert addr1 == addr2 + + +def test_to_proto_key(): + "Test to_proto_key raises error when call." + raw = bytes(range(20)) + address = EvmAddress.from_bytes(raw) + + with pytest.raises(RuntimeError, match=re.escape("to_proto_key() not implemented for EvmAddress")): + address.to_proto_key() diff --git a/tests/unit/file_append_transaction_test.py b/tests/unit/file_append_transaction_test.py index affd517da..7b6c3cf10 100644 --- a/tests/unit/file_append_transaction_test.py +++ b/tests/unit/file_append_transaction_test.py @@ -1,7 +1,7 @@ from unittest.mock import patch, MagicMock import pytest -from hiero_sdk_python.exceptions import PrecheckError +from hiero_sdk_python.exceptions import PrecheckError, ReceiptStatusError from hiero_sdk_python.file.file_append_transaction import FileAppendTransaction from hiero_sdk_python.file.file_id import FileId from hiero_sdk_python.hapi.services import response_header_pb2, response_pb2, transaction_get_receipt_pb2, transaction_receipt_pb2, transaction_response_pb2 @@ -132,7 +132,7 @@ def test_multi_chunk_execution(): # Mock client and responses mock_client = MagicMock() - mock_receipt = MagicMock() + mock_receipt = MagicMock(spec=TransactionReceipt) mock_receipt.status = ResponseCode.SUCCESS # Mock the execute method to return our mock receipt @@ -328,3 +328,133 @@ def test_file_append_tx_execute_all_throw_error_when_transaction_fails(file_id): with pytest.raises(PrecheckError, match="Transaction failed precheck"): tx.execute_all(client) + +def test_execute_raises_error_when_validation_enabled(file_id): + """Test execute raises error for failing transactions when validate_status is True.""" + ok_response = transaction_response_pb2.TransactionResponse( + nodeTransactionPrecheckCode=ResponseCode.OK + ) + + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ), + ) + ) + + response_sequence = [[ok_response, receipt_response]] + + with mock_hedera_servers(response_sequence) as client: + tx = ( + FileAppendTransaction() + .set_file_id(file_id) + .set_contents("Hello Hiero") + .freeze_with(client) + ) + + with pytest.raises(ReceiptStatusError) as e: + tx.execute(client, validate_status=True) + + assert e.value.status == ResponseCode.INVALID_SIGNATURE + +def test_execute_returns_failed_receipt_when_validation_disabled(file_id): + """Test execute returns the failing receipt by default when validation is disabled.""" + ok_response = transaction_response_pb2.TransactionResponse( + nodeTransactionPrecheckCode=ResponseCode.OK + ) + + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ), + ) + ) + + response_sequence = [[ok_response, receipt_response]] + + with mock_hedera_servers(response_sequence) as client: + tx = ( + FileAppendTransaction() + .set_file_id(file_id) + .set_contents("Hello Hiero") + .freeze_with(client) + ) + + receipt = tx.execute(client) + + assert receipt.status == ResponseCode.INVALID_SIGNATURE + +def test_file_append_execute_all_raises_error_with_validation(file_id): + """Test execute_all raises error when validate_status is True and a chunk fails.""" + content = "A" * 1024 + + tx_response = transaction_response_pb2.TransactionResponse( + nodeTransactionPrecheckCode=ResponseCode.OK + ) + + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_FILE_ID + ) + ) + ) + + response_sequence = [tx_response, receipt_response] * 4 + + with mock_hedera_servers([response_sequence]) as client: + tx = ( + FileAppendTransaction() + .set_file_id(file_id) + .set_contents(content) + .freeze_with(client) + ) + + with pytest.raises(ReceiptStatusError) as e: + tx.execute_all(client, validate_status=True) + + assert e.value.status == ResponseCode.INVALID_FILE_ID + +def test_file_append_execute_all_returns_receipt_without_validation(file_id): + """Test execute_all returns failing receipts normally when validation is disabled.""" + content = "A" * 1024 + + tx_response = transaction_response_pb2.TransactionResponse( + nodeTransactionPrecheckCode=ResponseCode.OK + ) + + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_FILE_ID + ) + ) + ) + + response_sequence = [tx_response, receipt_response] * 4 # 4 chunks + + with mock_hedera_servers([response_sequence]) as client: + tx = ( + FileAppendTransaction() + .set_file_id(file_id) + .set_contents(content) + .freeze_with(client) + ) + + receipts = tx.execute_all(client) + + assert receipts[0].status == ResponseCode.INVALID_FILE_ID diff --git a/tests/unit/get_receipt_query_test.py b/tests/unit/get_receipt_query_test.py index 79b4d3d35..66cdc0225 100644 --- a/tests/unit/get_receipt_query_test.py +++ b/tests/unit/get_receipt_query_test.py @@ -4,7 +4,7 @@ from unittest.mock import patch from hiero_sdk_python.account.account_id import AccountId -from hiero_sdk_python.exceptions import MaxAttemptsError +from hiero_sdk_python.exceptions import MaxAttemptsError, ReceiptStatusError from hiero_sdk_python.hapi.services import ( basic_types_pb2, response_header_pb2, @@ -375,3 +375,57 @@ def test_transaction_get_receipt_query_returns_empty_duplicate_receipts_when_not assert query.include_duplicates is False assert result.status == ResponseCode.SUCCESS assert len(result.duplicates) == 0 + +def test_transaction_receipt_query_should_not_raise_receipt_error(transaction_id): + """Test receipt query should not raise error if the status is non success and non retryable when validate_status is false.""" + response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ), + ) + ) + + response_sequences = [[response]] + + with mock_hedera_servers(response_sequences) as client: + query = ( + TransactionGetReceiptQuery() + .set_transaction_id(transaction_id) + .set_validate_status(False) + ) + + receipt = query.execute(client) + + assert receipt.status == ResponseCode.INVALID_SIGNATURE + + +def test_transaction_receipt_query_should_raise_receipt_error(transaction_id): + """Test receipt query should raise error if the status is non success and non retryable when validate_status is true.""" + response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ), + ) + ) + + response_sequences = [[response]] + + with mock_hedera_servers(response_sequences) as client: + query = ( + TransactionGetReceiptQuery() + .set_transaction_id(transaction_id) + .set_validate_status(True) + ) + + with pytest.raises(ReceiptStatusError) as e: + query.execute(client) + + assert e.value.status == ResponseCode.INVALID_SIGNATURE \ No newline at end of file diff --git a/tests/unit/hbar_test.py b/tests/unit/hbar_test.py index 67553893d..99137d3f4 100644 --- a/tests/unit/hbar_test.py +++ b/tests/unit/hbar_test.py @@ -39,6 +39,13 @@ def test_constructor_non_finite_amount_value(invalid_amount): Hbar(invalid_amount) +@pytest.mark.parametrize("invalid_amount", [Decimal("Infinity"), Decimal("NaN")]) +def test_constructor_non_finite_decimal_amount_value(invalid_amount): + """Test creation raises errors for non-finite Decimal amounts.""" + with pytest.raises(ValueError, match="Hbar amount must be finite"): + Hbar(invalid_amount) + + def test_constructor_with_tinybar_unit(): """Test creation with unit set to HbarUnit.TINYBAR.""" hbar1 = Hbar(50, unit=HbarUnit.TINYBAR) @@ -83,6 +90,12 @@ def test_constructor_fractional_tinybar(): Hbar(0.1, unit=HbarUnit.TINYBAR) +def test_constructor_fractional_non_tinybar_amount(): + """Test creation rejects values that do not map to an integral tinybar amount.""" + with pytest.raises(ValueError, match="Fractional tinybar value not allowed"): + Hbar(Decimal("0.000000001"), unit=HbarUnit.HBAR) + + def test_constructor_invalid_type(): """Test creation of Hbar with invalid type.""" with pytest.raises( @@ -102,6 +115,22 @@ def test_from_string(): assert Hbar.from_string("-3").to_tinybars() == -300_000_000 +def test_to_tinybars_preserves_large_integer_values(): + """to_tinybars() should not lose precision for integers above float-safe range.""" + tinybars = 9_007_199_254_740_993 # 2^53 + 1 + hbar = Hbar.from_tinybars(tinybars) + + assert hbar.to_tinybars() == tinybars + + +def test_from_string_preserves_large_integer_hbar_values(): + """Parsing large whole hbar values should preserve the exact tinybar amount.""" + hbar_amount = "90071992.54740993" + expected_tinybars = 9_007_199_254_740_993 + + assert Hbar.from_string(hbar_amount).to_tinybars() == expected_tinybars + + @pytest.mark.parametrize( "invalid_str", [ diff --git a/tests/unit/key_list_test.py b/tests/unit/key_list_test.py new file mode 100644 index 000000000..e3b584383 --- /dev/null +++ b/tests/unit/key_list_test.py @@ -0,0 +1,213 @@ +import pytest + +from hiero_sdk_python.crypto.key_list import KeyList +from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.crypto.key import Key +from hiero_sdk_python.hapi.services import basic_types_pb2 + +pytestmark = pytest.mark.unit + + +def make_keys(n=3): + """Helper to generate a list of public keys.""" + keys = [] + for _ in range(n): + priv = PrivateKey.generate("ed25519") + keys.append(priv.public_key()) + return keys + + +def test_create_keylist_no_threshold(): + """Test create a KeyList without a threshold.""" + keys = make_keys(3) + kl = KeyList(keys) + + assert kl.threshold is None + assert len(kl.keys) == 3 + + +def test_create_keylist_with_threshold(): + """Test create a KeyList with a threshold.""" + keys = make_keys(3) + kl = KeyList(keys, threshold=2) + + assert kl.threshold == 2 + assert len(kl.keys) == 3 + + +def test_create_keylist_empty(): + """Test KeyList can be created without providing keys.""" + kl = KeyList() + + assert kl.threshold is None + assert kl.keys == [] + + +def test_constructor_type_check_keys(): + """Test constructor should reject non-list keys.""" + with pytest.raises(TypeError, match="keys must be a list"): + KeyList("not-a-list") + + +def test_constructor_type_check_key_elements(): + """Test constructor should reject non-Key objects in list.""" + with pytest.raises(TypeError, match="instances of Key"): + KeyList([1, 2, 3]) + + +def test_constructor_threshold_type_check(): + """Test constructor should reject non-int threshold.""" + with pytest.raises(TypeError, match="threshold must be an integer"): + KeyList(make_keys(2), threshold="two") + + +def test_add_key(): + """Test add_key appends a key and returns self.""" + kl = KeyList() + + key = PrivateKey.generate("ed25519").public_key() + returned = kl.add_key(key) + + assert returned is kl + assert kl.keys == [key] + + +def test_add_key_type_check(): + """Test add_key should reject non-Key objects.""" + kl = KeyList() + + with pytest.raises(TypeError, match="instances of Key"): + kl.add_key("not-a-key") + + +def test_set_keys(): + """Test set_keys replaces existing keys.""" + kl = KeyList(make_keys(2)) + + new_keys = make_keys(3) + returned = kl.set_keys(new_keys) + + assert returned is kl + assert kl.keys == new_keys + + +def test_set_keys_type_check(): + """Test set_keys should reject non-list input.""" + kl = KeyList() + + with pytest.raises(TypeError, match="keys must be a list"): + kl.set_keys("not-a-list") + + +def test_set_keys_invalid_elements(): + """Test set_keys should reject non-Key elements.""" + kl = KeyList() + + with pytest.raises(TypeError, match="instances of Key"): + kl.set_keys([1, 2]) + + +def test_set_threshold(): + """Test threshold can be updated.""" + kl = KeyList(make_keys(3)) + + returned = kl.set_threshold(2) + + assert returned is kl + assert kl.threshold == 2 + + +def test_set_threshold_none(): + """Test threshold can be reset to None.""" + kl = KeyList(make_keys(3), threshold=2) + + kl.set_threshold(None) + + assert kl.threshold is None + + +def test_set_threshold_type_check(): + """Test set_threshold should reject non-int values.""" + kl = KeyList(make_keys(2)) + + with pytest.raises(TypeError, match="threshold must be an integer"): + kl.set_threshold("bad") + + +def test_to_proto_without_threshold(): + """Test to_proto should produce a protobuf KeyList.""" + keys = make_keys(3) + kl = KeyList(keys) + + proto = kl.to_proto() + + assert isinstance(proto, basic_types_pb2.KeyList) + assert len(proto.keys) == 3 + + +def test_to_proto_with_threshold(): + """Test to_proto_key should produce a ThresholdKey if threshold is set.""" + keys = make_keys(3) + kl = KeyList(keys, threshold=2) + + proto_key = kl.to_proto_key() + + assert proto_key.HasField("thresholdKey") + assert proto_key.thresholdKey.threshold == 2 + assert len(proto_key.thresholdKey.keys.keys) == 3 + + +def test_to_proto_key_without_threshold(): + """Test to_proto_key should produce a standard KeyList if no threshold.""" + kl = KeyList(make_keys(2)) + + proto_key = kl.to_proto_key() + + assert proto_key.HasField("keyList") + assert len(proto_key.keyList.keys) == 2 + + +def test_from_proto_keylist(): + """Test from_proto correctly reconstructs a KeyList.""" + keys = make_keys(3) + kl = KeyList(keys) + + proto = kl.to_proto() + + loaded = KeyList.from_proto(proto) + + assert isinstance(loaded, KeyList) + assert len(loaded.keys) == 3 + assert loaded.threshold is None + + +def test_from_proto_threshold(): + """Test thresholdKey proto loads correctly.""" + keys = make_keys(3) + kl = KeyList(keys, threshold=2) + + proto_key = kl.to_proto_key() + + loaded = KeyList.from_proto( + proto_key.thresholdKey.keys, threshold=proto_key.thresholdKey.threshold + ) + + assert loaded.threshold == 2 + assert len(loaded.keys) == 3 + + +@pytest.mark.parametrize("num_keys", [1, 2, 5]) +def test_proto_roundtrip(num_keys): + """Test roundtrip should preserve key count.""" + kl1 = KeyList(make_keys(num_keys), threshold=None) + + proto = kl1.to_proto() + kl2 = KeyList.from_proto(proto) + + assert len(kl1.keys) == len(kl2.keys) + + +def test_keylist_is_instance_of_key(): + """Test KeyList must inherit from Key.""" + kl = KeyList() + assert isinstance(kl, Key) diff --git a/tests/unit/key_test.py b/tests/unit/key_test.py new file mode 100644 index 000000000..6e35b1bf6 --- /dev/null +++ b/tests/unit/key_test.py @@ -0,0 +1,186 @@ +import pytest + +from hiero_sdk_python.contract.delegate_contract_id import DelegateContractId +from hiero_sdk_python.crypto.key import Key +from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.crypto.public_key import PublicKey +from hiero_sdk_python.crypto.key_list import KeyList +from hiero_sdk_python.crypto.evm_address import EvmAddress +from hiero_sdk_python.contract.contract_id import ContractId +from hiero_sdk_python.hapi.services import basic_types_pb2 + +pytestmark = pytest.mark.unit + + +class DummyKey(Key): + """Helper Key implementation for testing.""" + + def to_proto_key(self) -> basic_types_pb2.Key: + return basic_types_pb2.Key() + + +def test_from_bytes_type_check(): + """Test from_bytes should reject non-bytes.""" + with pytest.raises(TypeError, match="data must be bytes"): + Key.from_bytes("not-bytes") + + +def test_to_bytes_serialization(): + """Test to_bytes should serialize the proto returned by to_proto_key.""" + key = DummyKey() + b = key.to_bytes() + + assert isinstance(b, bytes) + + +def test_from_proto_key_ed25519(): + """Test ed25519 keys are parsed into PublicKey objects.""" + priv = PrivateKey.generate("ed25519") + pub = priv.public_key() + + proto = pub.to_proto_key() + + loaded = Key.from_proto_key(proto) + + assert isinstance(loaded, PublicKey) + + +def test_from_proto_key_ecdsa(): + """Test ECDSA keys are parsed into PublicKey objects.""" + priv = PrivateKey.generate("ecdsa") + pub = priv.public_key() + + proto = pub.to_proto_key() + + loaded = Key.from_proto_key(proto) + + assert isinstance(loaded, PublicKey) + + +def test_from_proto_key_evm_address(): + """Test ECDSA_secp256k1 field is 20 bytes it should produce an EvmAddress.""" + evm = EvmAddress.from_bytes(b"\x11" * 20) + + proto = basic_types_pb2.Key(ECDSA_secp256k1=evm.address_bytes) + loaded = Key.from_proto_key(proto) + + assert isinstance(loaded, EvmAddress) + + +def test_from_proto_key_contract_id(): + """Test contractID should produce a ContractId instance.""" + cid = ContractId(0, 0, 123) + + proto = basic_types_pb2.Key(contractID=cid._to_proto()) + + loaded = Key.from_proto_key(proto) + + assert isinstance(loaded, ContractId) + + +def test_from_proto_key_delegatable_contract_id(): + """Test delegatable_contract_id should produce a ContractId instance.""" + cid = ContractId(0, 0, 123) + + proto = basic_types_pb2.Key(delegatable_contract_id=cid._to_proto()) + + loaded = Key.from_proto_key(proto) + + assert isinstance(loaded, DelegateContractId) + + +def test_from_proto_key_keylist(): + """Test keyList should produce a KeyList instance.""" + priv = PrivateKey.generate("ed25519") + + kl = KeyList([priv.public_key()]) + proto = kl.to_proto_key() + + loaded = Key.from_proto_key(proto) + + assert isinstance(loaded, KeyList) + assert len(loaded.keys) == 1 + + +def test_from_proto_key_threshold_key(): + """Test thresholdKey should produce a KeyList with threshold.""" + priv = PrivateKey.generate("ed25519") + + kl = KeyList([priv.public_key()], threshold=1) + proto = kl.to_proto_key() + + loaded = Key.from_proto_key(proto) + + assert isinstance(loaded, KeyList) + assert loaded.threshold == 1 + + +def test_from_proto_key_unknown_type(): + """Test unknown key type should be raised error.""" + proto = basic_types_pb2.Key() + + with pytest.raises(ValueError, match="Unknown key type"): + Key.from_proto_key(proto) + + +def test_from_proto_key_type_check(): + """Test from_proto_key should reject invalid proto type.""" + with pytest.raises(TypeError, match="proto must be an instance"): + Key.from_proto_key("not-a-proto") + + +def test_from_bytes_roundtrip(): + """Test a key to bytes then load with Key using from_bytes.""" + priv = PrivateKey.generate("ed25519") + pub = priv.public_key() + + b = pub.to_bytes() + + loaded = Key.from_bytes(b) + + assert isinstance(loaded, PublicKey) + + +@pytest.mark.parametrize("key_type", ["ed25519", "ecdsa"]) +def test_from_bytes_multiple_key_types(key_type): + """Test from_bytes supports multiple key algorithms.""" + priv = PrivateKey.generate(key_type) + pub = priv.public_key() + + loaded = Key.from_bytes(pub.to_bytes()) + + assert isinstance(loaded, PublicKey) + + +def test_keylist_bytes_roundtrip(): + """Test should serialize and deserialize Key using from_bytes.""" + keys = [PrivateKey.generate("ed25519").public_key() for _ in range(2)] + + kl = KeyList(keys) + + b = kl.to_bytes() + + loaded = Key.from_bytes(b) + + assert isinstance(loaded, KeyList) + assert len(loaded.keys) == 2 + + +def test_threshold_key_bytes_roundtrip(): + """Test ThresholdKey should serialize and deserialize correctly.""" + keys = [PrivateKey.generate("ed25519").public_key() for _ in range(3)] + + kl = KeyList(keys, threshold=2) + + b = kl.to_bytes() + + loaded = Key.from_bytes(b) + + assert isinstance(loaded, KeyList) + assert loaded.threshold == 2 + + +def test_key_is_abstract(): + """Test key should not be directly instantiable.""" + with pytest.raises(TypeError): + Key() diff --git a/tests/unit/keys_private_test.py b/tests/unit/keys_private_test.py index bbc8589dd..37de97668 100644 --- a/tests/unit/keys_private_test.py +++ b/tests/unit/keys_private_test.py @@ -5,6 +5,7 @@ from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives.asymmetric import ec, ed25519, rsa from cryptography.hazmat.primitives import serialization +from hiero_sdk_python.crypto.key import Key from hiero_sdk_python.crypto.public_key import PublicKey from hiero_sdk_python.crypto.private_key import PrivateKey @@ -542,3 +543,38 @@ def test_hash_algorithm_distinction(): assert ed != ec assert hash(ed) != hash(ec) + + +def test_to_proto_key_ecd25519(): + """Test to_proto_key properly convert ed25519 to Key protobuf.""" + pr_key = PrivateKey.generate_ed25519() + proto_key = pr_key.to_proto_key() + + assert proto_key is not None + assert proto_key.ed25519 is not None + assert proto_key.ed25519 == pr_key.public_key().to_bytes_raw() + +def test_to_proto_key_ecdsa(): + """Test to_proto_key properly convert ecdsa to Key protobuf.""" + pr_key = PrivateKey.generate_ecdsa() + proto_key = pr_key.to_proto_key() + + assert proto_key is not None + assert proto_key.ECDSA_secp256k1 is not None + assert proto_key.ECDSA_secp256k1 == pr_key.public_key().to_bytes_raw() + +@pytest.mark.parametrize( + "key", + [PrivateKey.generate_ed25519(), PrivateKey.generate_ecdsa()] +) +def test_protobuf_roundtrip(key): + """Test protobuf roundtrip properly convert to Key protobuf and vice versa.""" + proto = key.to_proto_key() + + pub_key = key.public_key() + loaded = Key.from_proto_key(proto) # Returns public key + + assert isinstance(loaded, PublicKey) + assert loaded.is_ed25519() == pub_key.is_ed25519() + assert loaded.is_ecdsa() == pub_key.is_ecdsa() + assert loaded.to_bytes_raw() == pub_key.to_bytes_raw() diff --git a/tests/unit/keys_public_test.py b/tests/unit/keys_public_test.py index 5b99bd321..bbf0f2049 100644 --- a/tests/unit/keys_public_test.py +++ b/tests/unit/keys_public_test.py @@ -5,8 +5,9 @@ from cryptography.hazmat.primitives.asymmetric import utils as asym_utils from cryptography.exceptions import InvalidSignature from hiero_sdk_python.crypto.evm_address import EvmAddress +from hiero_sdk_python.crypto.key import Key from hiero_sdk_python.crypto.private_key import PrivateKey -from hiero_sdk_python.hapi.services.basic_types_pb2 import Key +from hiero_sdk_python.hapi.services import basic_types_pb2 from hiero_sdk_python.crypto.public_key import PublicKey from hiero_sdk_python.utils.crypto_utils import keccak256 @@ -469,7 +470,7 @@ def test_from_proto_ecdsa(ecdsa_keypair): def test_from_proto_unsupported_type(): # Create a Key proto with an unsupported type - proto = Key() + proto = basic_types_pb2.Key() # Set some arbitrary bytes to a RSA_3072 as currently we do not support it proto.RSA_3072 = b"currently unsupported" @@ -711,3 +712,37 @@ def test_equality_with_non_publickey_returns_false(ed25519_keypair, other): assert (key == other) is False +def test_to_proto_key_ecd25519(): + """Test to_proto_key properly convert ed25519 to Key protobuf.""" + pub_key = PrivateKey.generate_ed25519().public_key() + proto_key = pub_key.to_proto_key() + + assert proto_key is not None + assert proto_key.ed25519 is not None + assert proto_key.ed25519 == pub_key.to_bytes_raw() + +def test_to_proto_key_ecdsa(): + """Test to_proto_key properly convert ecdsa to Key protobuf.""" + pub_key = PrivateKey.generate_ecdsa().public_key() + proto_key = pub_key.to_proto_key() + + assert proto_key is not None + assert proto_key.ECDSA_secp256k1 is not None + assert proto_key.ECDSA_secp256k1 == pub_key.to_bytes_raw() + +@pytest.mark.parametrize( + "key", + [PrivateKey.generate_ed25519(), PrivateKey.generate_ecdsa()] +) +def test_protobuf_roundtrip(key): + """Test protobuf roundtrip properly convert to Key protobuf and vice versa.""" + pub_key = key.public_key() + + proto = pub_key.to_proto_key() + loaded = Key.from_proto_key(proto) + + assert isinstance(loaded, PublicKey) + assert loaded.is_ed25519() == pub_key.is_ed25519() + assert loaded.is_ecdsa() == pub_key.is_ecdsa() + assert loaded.to_bytes_raw() == pub_key.to_bytes_raw() + diff --git a/tests/unit/mock_server.py b/tests/unit/mock_server.py index 50bee264d..71c8df412 100644 --- a/tests/unit/mock_server.py +++ b/tests/unit/mock_server.py @@ -1,4 +1,5 @@ import grpc +import threading from concurrent import futures from contextlib import contextmanager from hiero_sdk_python.client.network import Network @@ -6,8 +7,6 @@ from hiero_sdk_python.account.account_id import AccountId from hiero_sdk_python.crypto.private_key import PrivateKey from hiero_sdk_python.client.network import _Node -import socket -from contextlib import closing from hiero_sdk_python.hapi.services import ( crypto_service_pb2_grpc, token_service_pb2_grpc, @@ -32,14 +31,15 @@ def __init__(self, responses): responses (list): List of response objects to return in sequence """ self.responses = responses + self._lock = threading.Lock() self.server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) - self.port = _find_free_port() + + self.port = self.server.add_insecure_port('[::]:0') self.address = f"localhost:{self.port}" self._register_services() # Start the server - self.server.add_insecure_port(self.address) self.server.start() def _register_services(self): @@ -95,6 +95,7 @@ def _create_mock_servicer(self, servicer_class): A mock servicer object """ responses = self.responses + lock = self._lock; class MockServicer(servicer_class): def __getattribute__(self, name): @@ -103,12 +104,11 @@ def __getattribute__(self, name): return super().__getattribute__(name) def method_wrapper(request, context): - nonlocal responses - if not responses: - # If no more responses are available, return None - return None + with lock: + if not responses: + return None - response = responses.pop(0) + response = responses.pop(0) if isinstance(response, RealRpcError): # Abort with custom error @@ -122,15 +122,11 @@ def method_wrapper(request, context): def close(self): """Stop the server.""" - self.server.stop(0) + shutdown_event = self.server.stop(0) + success = shutdown_event.wait(timeout=2.0) - -def _find_free_port(): - """Find a free port on localhost.""" - with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: - s.bind(("", 0)) - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - return s.getsockname()[1] + if not success: + pass class RealRpcError(grpc.RpcError): @@ -166,15 +162,20 @@ def mock_hedera_servers(response_sequences): nodes = [] for i, server in enumerate(servers): node = _Node(AccountId(0, 0, 3 + i), server.address, None) - - # force insecure transport - node._apply_transport_security(False) - node._set_verify_certificates(False) - nodes.append(node) # Create network and client network = Network(nodes=nodes) + network.set_transport_security(False) + network.set_verify_certificates(False) + client = Client(network) + + # Force non-tls for channel + for node in client.network.nodes: + node._address._is_transport_security = lambda: False + node._set_verify_certificates(False) + node._close() + client = Client(network) client.logger.set_level(LogLevel.DISABLED) # Set the operator diff --git a/tests/unit/tck/handlers_test.py b/tests/unit/tck/handlers_test.py deleted file mode 100644 index 574e5419c..000000000 --- a/tests/unit/tck/handlers_test.py +++ /dev/null @@ -1,359 +0,0 @@ -"""Test cases for the Hiero SDK TCK handlers registry and dispatch functionality.""" -import pytest -from unittest.mock import MagicMock -from hiero_sdk_python.exceptions import (PrecheckError, ReceiptStatusError, MaxAttemptsError) -from tck.handlers.registry import ( - register_handler, get_handler, get_all_handlers, dispatch, safe_dispatch, - validate_request_params -) -from tck.errors import ( - JsonRpcError, - METHOD_NOT_FOUND, - INTERNAL_ERROR, - HIERO_ERROR, - INVALID_PARAMS) -from tck.handlers import registry - -pytestmark = pytest.mark.unit - -@pytest.fixture(autouse=True) -def clear_handlers(): - """Clear the handlers registry before each test.""" - registry._HANDLERS.clear() - yield - registry._HANDLERS.clear() - - -class TestHandlerRegistration: - """Test handler registration via decorator.""" - - def test_handler_registration_via_decorator(self): - """Test that @register_handler decorator adds handler to registry.""" - @register_handler("test_method") - def test_handler(_params): - return {"status": "ok"} - - handler = get_handler("test_method") - if handler is None: - raise AssertionError("Expected handler to be registered") - if handler({"test": "param"}) != {"status": "ok"}: - raise AssertionError("Expected handler to return ok status") - - def test_get_all_handlers(self): - """Test that get_all_handlers returns copy of all registered handlers.""" - @register_handler("method1") - def handler1(_params): - return "result1" - - @register_handler("method2") - def handler2(_params): - return "result2" - - all_handlers = get_all_handlers() - if len(all_handlers) != 2: - raise AssertionError("Expected two handlers to be registered") - if "method1" not in all_handlers: - raise AssertionError("Expected method1 to be registered") - if "method2" not in all_handlers: - raise AssertionError("Expected method2 to be registered") - - def test_get_nonexistent_handler_returns_none(self): - """Test that getting a non-existent handler returns None.""" - handler = get_handler("nonexistent") - if handler is not None: - raise AssertionError("Expected None for nonexistent handler") - - def test_handler_override_behavior(self): - """Test that registering the same method twice overwrites the first handler.""" - @register_handler("override_method") - def first_handler(_params): - return "first" - - @register_handler("override_method") - def second_handler(_params): - return "second" - - handler = get_handler("override_method") - if handler({}) != "second": - raise AssertionError("Second handler should overwrite first") - - def test_get_all_handlers_returns_copy(self): - """Test that get_all_handlers returns a copy that doesn't affect the registry.""" - @register_handler("protected_method") - def protected_handler(_params): - return "protected" - - all_handlers = get_all_handlers() - all_handlers["protected_method"] = lambda: "modified" - all_handlers["injected_method"] = lambda: "injected" - - # Original registry should be unchanged - if get_handler("protected_method")({}) != "protected": - raise AssertionError("Expected protected_method to be unchanged") - if get_handler("injected_method") is not None: - raise AssertionError("Expected injected_method to be absent") - - -class TestDispatch: - """Test method dispatch functionality.""" - - def test_dispatch_registered_method(self): - """Test that dispatch invokes registered handler correctly.""" - @register_handler("setup") - def setup_handler(_params): - return {"ready": True} - - result = dispatch("setup", {"key": "value"}, None) - if result != {"ready": True}: - raise AssertionError("Expected handler result to match") - - def test_dispatch_with_session_id(self): - """Test that dispatch passes session_id to handler when provided.""" - @register_handler("session_method") - def session_handler(params, session_id): - return {"session": session_id, "params": params} - - result = dispatch("session_method", {"data": "test"}, "session123") - if result != {"session": "session123", "params": {"data": "test"}}: - raise AssertionError("Expected session id and params to be passed through") - - def test_dispatch_unknown_method_raises_method_not_found(self): - """Test that dispatching unknown method raises METHOD_NOT_FOUND error.""" - with pytest.raises(JsonRpcError) as excinfo: - dispatch("unknown_method", {}, None) - - if excinfo.value.code != METHOD_NOT_FOUND: - raise AssertionError("Expected METHOD_NOT_FOUND error") - if "Method not found" not in excinfo.value.message: - raise AssertionError("Expected Method not found message") - - def test_dispatch_reraises_json_rpc_error(self): - """Test that dispatch re-raises JsonRpcError exceptions.""" - @register_handler("error_method") - def error_handler(params): - raise JsonRpcError.invalid_params_error() - - with pytest.raises(JsonRpcError) as excinfo: - dispatch("error_method", {}, None) - - if excinfo.value.code != INVALID_PARAMS: - raise AssertionError("Expected INVALID_PARAMS error") - - def test_dispatch_converts_generic_exception_to_internal_error(self): - """Test that dispatch converts generic exceptions to INTERNAL_ERROR.""" - @register_handler("crash_method") - def crash_handler(params): - raise ValueError("Something went wrong") - - with pytest.raises(JsonRpcError) as excinfo: - dispatch("crash_method", {}, None) - - if excinfo.value.code != INTERNAL_ERROR: - raise AssertionError("Expected INTERNAL_ERROR error") - if "Something went wrong" not in excinfo.value.data: - raise AssertionError("Expected error details to include message") - - def test_dispatch_converts_precheck_error_to_hiero_error(self): - """Test that dispatch converts PrecheckError to HIERO_ERROR.""" - @register_handler("precheck_method") - def precheck_handler(_params): - raise PrecheckError(status=123, transaction_id="0.0.456", message="Account does not exist") - - with pytest.raises(JsonRpcError) as excinfo: - dispatch("precheck_method", {}, None) - - if excinfo.value.code != HIERO_ERROR: - raise AssertionError("Expected HIERO_ERROR error") - if "Hiero error" not in excinfo.value.message: - raise AssertionError("Expected Hiero error message") - - def test_dispatch_converts_receipt_status_error_to_hiero_error(self): - """Test that dispatch converts ReceiptStatusError to HIERO_ERROR.""" - @register_handler("receipt_method") - def receipt_handler(_params): - mock_receipt = MagicMock() - raise ReceiptStatusError(status=21, transaction_id=None, transaction_receipt=mock_receipt, message="Transaction failed") - - with pytest.raises(JsonRpcError) as excinfo: - dispatch("receipt_method", {}, None) - - if excinfo.value.code != HIERO_ERROR: - raise AssertionError("Expected HIERO_ERROR error") - if "Hiero error" not in excinfo.value.message: - raise AssertionError("Expected Hiero error message") - - def test_dispatch_converts_max_attempts_error_to_hiero_error(self): - """Test that dispatch converts MaxAttemptsError to HIERO_ERROR.""" - @register_handler("max_attempts_method") - def max_attempts_handler(_params): - raise MaxAttemptsError("Max retries exceeded", node_id="0.0.1") - - with pytest.raises(JsonRpcError) as excinfo: - dispatch("max_attempts_method", {}, None) - - if excinfo.value.code != HIERO_ERROR: - raise AssertionError("Expected HIERO_ERROR error") - if "Hiero error" not in excinfo.value.message: - raise AssertionError("Expected Hiero error message") - - -class TestSafeDispatch: - """Test safe dispatch with error handling.""" - - def test_safe_dispatch_returns_success_response(self): - """Test that safe_dispatch returns result for successful dispatch.""" - @register_handler("success_method") - def success_handler(_params): - return {"success": True} - - # safe_dispatch should return raw result without wrapping - result = safe_dispatch("success_method", {}, None, 1) - if result != {"success": True}: - raise AssertionError("Expected raw handler result from safe_dispatch") - - def test_safe_dispatch_returns_error_response_for_json_rpc_error(self): - """Test that safe_dispatch returns error response for JsonRpcError.""" - @register_handler("json_error_method") - def error_handler(_params): - raise JsonRpcError.invalid_params_error(data="field_name") - - response = safe_dispatch("json_error_method", {}, None, 42) - - if "error" not in response: - raise AssertionError("Expected error in response") - if response["error"]["code"] != INVALID_PARAMS: - raise AssertionError("Expected INVALID_PARAMS error code") - if response["error"]["message"] != "Invalid params": - raise AssertionError("Expected Bad params message") - if response["error"]["data"] != "field_name": - raise AssertionError("Expected error data to match") - if response["id"] != 42: - raise AssertionError("Expected response id to match request id") - - def test_safe_dispatch_transforms_precheck_error(self): - """Test that safe_dispatch transforms PrecheckError to HIERO_ERROR.""" - @register_handler("precheck_method") - def precheck_handler(_params): - raise PrecheckError(status=123, transaction_id="0.0.456", message="Account does not exist") - - response = safe_dispatch("precheck_method", {}, None, 1) - - if "error" not in response: - raise AssertionError("Expected error in response") - if response["error"]["code"] != HIERO_ERROR: - raise AssertionError("Expected HIERO_ERROR error code") - if "Hiero error" not in response["error"]["message"]: - raise AssertionError("Expected Hiero error message") - - def test_safe_dispatch_transforms_receipt_status_error(self): - """Test that safe_dispatch transforms ReceiptStatusError to HIERO_ERROR.""" - @register_handler("receipt_method") - def receipt_handler(params): - mock_receipt = MagicMock() - raise ReceiptStatusError(status=21, transaction_id=None, transaction_receipt=mock_receipt, message="Transaction failed") - response = safe_dispatch("receipt_method", {}, None, 2) - - if "error" not in response: - raise AssertionError("Expected error in response") - if response["error"]["code"] != HIERO_ERROR: - raise AssertionError("Expected HIERO_ERROR error code") - - def test_safe_dispatch_transforms_max_attempts_error(self): - """Test that safe_dispatch transforms MaxAttemptsError to HIERO_ERROR.""" - @register_handler("max_attempts_method") - def max_attempts_handler(params): - raise MaxAttemptsError("Max retries exceeded", node_id="0.0.1") - response = safe_dispatch("max_attempts_method", {}, None, 3) - - if "error" not in response: - raise AssertionError("Expected error in response") - if response["error"]["code"] != HIERO_ERROR: - raise AssertionError("Expected HIERO_ERROR error code") - - def test_safe_dispatch_transforms_generic_exception(self): - """Test that safe_dispatch transforms generic Exception to INTERNAL_ERROR.""" - @register_handler("generic_error_method") - def generic_error_handler(params): - raise RuntimeError("Unexpected error") - - response = safe_dispatch("generic_error_method", {}, None, 4) - - if "error" not in response: - raise AssertionError("Expected error in response") - if response["error"]["code"] != INTERNAL_ERROR: - raise AssertionError("Expected INTERNAL_ERROR error code") - if response["id"] != 4: - raise AssertionError("Expected response id to match request id") - - def test_safe_dispatch_includes_request_id_in_response(self): - """Test that safe_dispatch returns raw result without id field (id is added at server level).""" - @register_handler("test_id_method") - def test_id_handler(_params): - return {"data": "test"} - - # safe_dispatch returns raw result; request_id is added by server layer - response = safe_dispatch("test_id_method", {}, None, "request_id_123") - if response != {"data": "test"}: - raise AssertionError("Expected raw response to be returned") - - -class TestValidateRequestParams: - """Test request parameter validation.""" - - def test_validate_valid_params(self): - """Test that validate_request_params accepts valid params.""" - params = {"memo": "Test topic", "sequence_number": 42} - required = {"memo": str, "sequence_number": int} - - # Should not raise- explicitly verify by calling and asserting no exception - try: - validate_request_params(params, required) - except JsonRpcError: - pytest.fail("validate_request_params raised JsonRpcError unexpectedly") - - def test_validate_missing_required_field(self): - """Test that missing required field raises INVALID_PARAMS.""" - params = {"memo": "Test topic"} - required = {"memo": str, "sequence_number": int} - - with pytest.raises(JsonRpcError) as excinfo: - validate_request_params(params, required) - - if excinfo.value.code != INVALID_PARAMS: - raise AssertionError("Expected INVALID_PARAMS error") - if "sequence_number" not in excinfo.value.message: - raise AssertionError("Expected missing field name in message") - - def test_validate_incorrect_field_type(self): - """Test that incorrect field type raises INVALID_PARAMS.""" - params = {"memo": "Test topic", "sequence_number": "not_a_number"} - required = {"memo": str, "sequence_number": int} - - with pytest.raises(JsonRpcError) as excinfo: - validate_request_params(params, required) - - if excinfo.value.code != INVALID_PARAMS: - raise AssertionError("Expected INVALID_PARAMS error") - if "sequence_number" not in excinfo.value.message: - raise AssertionError("Expected field name in message") - - def test_validate_non_dict_params_raises_invalid_params(self): - """Test that non-dict params raises INVALID_PARAMS.""" - with pytest.raises(JsonRpcError) as excinfo: - validate_request_params(["not", "a", "dict"], {"memo": str}) - - if excinfo.value.code != INVALID_PARAMS: - raise AssertionError("Expected INVALID_PARAMS error") - - def test_validate_none_value_for_required_field(self): - """Test that None value for required field raises INVALID_PARAMS.""" - params = {"memo": None, "sequence_number": 42} - required = {"memo": str, "sequence_number": int} - - with pytest.raises(JsonRpcError) as excinfo: - validate_request_params(params, required) - - if excinfo.value.code != INVALID_PARAMS: - raise AssertionError("Expected INVALID_PARAMS error") - if "memo" not in excinfo.value.message: - raise AssertionError("Expected field name in message") \ No newline at end of file diff --git a/tests/unit/token_association_test.py b/tests/unit/token_association_test.py new file mode 100644 index 000000000..e80b79c8f --- /dev/null +++ b/tests/unit/token_association_test.py @@ -0,0 +1,242 @@ +"""Unit tests for the TokenAssociation class.""" +import pytest + +from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.hapi.services.basic_types_pb2 import TokenAssociation as TokenAssociationProto +from hiero_sdk_python.tokens.token_association import TokenAssociation +from hiero_sdk_python.tokens.token_id import TokenId + +pytestmark = pytest.mark.unit + + +@pytest.fixture +def sample_token_id() -> TokenId: + """Return a sample TokenId for testing.""" + return TokenId(shard=0, realm=0, num=5678) + + +@pytest.fixture +def sample_account_id() -> AccountId: + """Return a sample AccountId for testing.""" + return AccountId(shard=0, realm=0, num=1234) + + +def test_default_initialization(): + """Test default initialization of TokenAssociation (both fields None).""" + assoc = TokenAssociation() + + assert assoc.token_id is None + assert assoc.account_id is None + + +def test_initialization_both_fields(sample_token_id: TokenId, sample_account_id: AccountId): + """Test initialization with both token_id and account_id provided.""" + assoc = TokenAssociation( + token_id=sample_token_id, + account_id=sample_account_id + ) + + assert assoc.token_id == sample_token_id + assert assoc.account_id == sample_account_id + + +def test_initialization_token_only(sample_token_id: TokenId): + """Test initialization with only token_id (account_id None).""" + assoc = TokenAssociation(token_id=sample_token_id) + + assert assoc.token_id == sample_token_id + assert assoc.account_id is None + + +def test_initialization_account_only(sample_account_id: AccountId): + """Test initialization with only account_id (token_id None).""" + assoc = TokenAssociation(account_id=sample_account_id) + + assert assoc.token_id is None + assert assoc.account_id == sample_account_id + + +def test_from_proto_both_fields(sample_token_id: TokenId, sample_account_id: AccountId): + """Test _from_proto with both token_id and account_id present in proto.""" + proto = TokenAssociationProto() + proto.token_id.shardNum = sample_token_id.shard + proto.token_id.realmNum = sample_token_id.realm + proto.token_id.tokenNum = sample_token_id.num + proto.account_id.shardNum = sample_account_id.shard + proto.account_id.realmNum = sample_account_id.realm + proto.account_id.accountNum = sample_account_id.num + + assoc = TokenAssociation._from_proto(proto) + + assert assoc.token_id == sample_token_id + assert assoc.account_id == sample_account_id + + +def test_from_proto_token_only(sample_token_id: TokenId): + """Test _from_proto with only token_id present (account_id absent).""" + proto = TokenAssociationProto() + proto.token_id.shardNum = sample_token_id.shard + proto.token_id.realmNum = sample_token_id.realm + proto.token_id.tokenNum = sample_token_id.num + + assoc = TokenAssociation._from_proto(proto) + + assert assoc.token_id == sample_token_id + assert assoc.account_id is None + + +def test_from_proto_account_only(sample_account_id: AccountId): + """Test _from_proto with only account_id present (token_id absent).""" + proto = TokenAssociationProto() + proto.account_id.shardNum = sample_account_id.shard + proto.account_id.realmNum = sample_account_id.realm + proto.account_id.accountNum = sample_account_id.num + + assoc = TokenAssociation._from_proto(proto) + + assert assoc.token_id is None + assert assoc.account_id == sample_account_id + + +def test_from_proto_empty(): + """Test _from_proto with completely empty protobuf message.""" + proto = TokenAssociationProto() + assoc = TokenAssociation._from_proto(proto) + + assert assoc.token_id is None + assert assoc.account_id is None + + +def test_to_proto_both_fields(sample_token_id: TokenId, sample_account_id: AccountId): + """Test _to_proto serializes both fields correctly.""" + assoc = TokenAssociation( + token_id=sample_token_id, + account_id=sample_account_id + ) + + proto = assoc._to_proto() + + assert proto.HasField("token_id") + assert proto.token_id.shardNum == sample_token_id.shard + assert proto.token_id.realmNum == sample_token_id.realm + assert proto.token_id.tokenNum == sample_token_id.num + + assert proto.HasField("account_id") + assert proto.account_id.shardNum == sample_account_id.shard + assert proto.account_id.realmNum == sample_account_id.realm + assert proto.account_id.accountNum == sample_account_id.num + + +def test_to_proto_token_only(sample_token_id: TokenId): + """Test _to_proto with only token_id (account_id None).""" + assoc = TokenAssociation(token_id=sample_token_id) + proto = assoc._to_proto() + + assert proto.HasField("token_id") + assert not proto.HasField("account_id") + + +def test_to_proto_account_only(sample_account_id: AccountId): + """Test _to_proto with only account_id (token_id None).""" + assoc = TokenAssociation(account_id=sample_account_id) + proto = assoc._to_proto() + + assert not proto.HasField("token_id") + assert proto.HasField("account_id") + + +def test_to_proto_empty(): + """Test _to_proto with empty/default instance.""" + assoc = TokenAssociation() + proto = assoc._to_proto() + + assert not proto.HasField("token_id") + assert not proto.HasField("account_id") + + +def test_round_trip_both_fields(sample_token_id: TokenId, sample_account_id: AccountId): + """Test full round-trip conversion preserves both fields.""" + original = TokenAssociation( + token_id=sample_token_id, + account_id=sample_account_id + ) + + proto = original._to_proto() + reconstructed = TokenAssociation._from_proto(proto) + + assert reconstructed.token_id == original.token_id + assert reconstructed.account_id == original.account_id + + +def test_round_trip_token_only(sample_token_id: TokenId): + """Test round-trip with only token_id.""" + original = TokenAssociation(token_id=sample_token_id) + proto = original._to_proto() + reconstructed = TokenAssociation._from_proto(proto) + + assert reconstructed.token_id == original.token_id + assert reconstructed.account_id is None + + +def test_round_trip_account_only(sample_account_id: AccountId): + """Test round-trip with only account_id.""" + original = TokenAssociation(account_id=sample_account_id) + proto = original._to_proto() + reconstructed = TokenAssociation._from_proto(proto) + + assert reconstructed.token_id is None + assert reconstructed.account_id == original.account_id + +def test_round_trip_empty(): + """Test round-trip with empty/default instance.""" + original = TokenAssociation() + proto = original._to_proto() + reconstructed = TokenAssociation._from_proto(proto) + + assert reconstructed.token_id is None + assert reconstructed.account_id is None + +def test_bytes_round_trip_both_fields( + sample_token_id: TokenId, + sample_account_id: AccountId, + ): + """Test round-trip via public to_bytes() / from_bytes() preserves both fields.""" + original = TokenAssociation( + token_id=sample_token_id, + account_id=sample_account_id, + ) + + reconstructed = TokenAssociation.from_bytes(original.to_bytes()) + + assert reconstructed == original + +def test_bytes_round_trip_empty(): + """Test round-trip via to_bytes()/from_bytes() with empty/default instance.""" + original = TokenAssociation() + reconstructed = TokenAssociation.from_bytes(original.to_bytes()) + assert reconstructed == original + assert reconstructed.token_id is None + assert reconstructed.account_id is None + +def test_repr_representation(sample_token_id: TokenId, sample_account_id: AccountId): + """Test __repr__ output for TokenAssociation.""" + assoc = TokenAssociation( + token_id=sample_token_id, + account_id=sample_account_id + ) + repr_str = repr(assoc) + + assert "TokenAssociation" in repr_str + assert "token_id=" in repr_str + assert "account_id=" in repr_str + + +def test_repr_with_none_fields(): + """Test __repr__ when some fields are None.""" + assoc = TokenAssociation(token_id=TokenId(shard=0, realm=0, num=5678)) + repr_str = repr(assoc) + assert "token_id=TokenId" in repr_str + assert "account_id=None" in repr_str + + empty = TokenAssociation() + assert "TokenAssociation(token_id=None, account_id=None)" in repr(empty) diff --git a/tests/unit/token_id_test.py b/tests/unit/token_id_test.py index 910e75da0..6b8189f88 100644 --- a/tests/unit/token_id_test.py +++ b/tests/unit/token_id_test.py @@ -69,3 +69,12 @@ def test_validate_checksum_failure(mock_client): with pytest.raises(ValueError): token_id.validate_checksum(client) + +def test_token_id_repr(): + """Should return constructor-style representation without checksum.""" + token_id = TokenId(0, 0, 123) + assert repr(token_id) == "TokenId(shard=0, realm=0, num=123)" + + # Test with different values + token_id2 = TokenId(1, 2, 456) + assert repr(token_id2) == "TokenId(shard=1, realm=2, num=456)" \ No newline at end of file diff --git a/tests/unit/topic_message_submit_transaction_test.py b/tests/unit/topic_message_submit_transaction_test.py index 2d206d3a3..bb2f75de7 100644 --- a/tests/unit/topic_message_submit_transaction_test.py +++ b/tests/unit/topic_message_submit_transaction_test.py @@ -5,7 +5,7 @@ import pytest from hiero_sdk_python.consensus.topic_message_submit_transaction import TopicMessageSubmitTransaction -from hiero_sdk_python.exceptions import PrecheckError +from hiero_sdk_python.exceptions import PrecheckError, ReceiptStatusError from hiero_sdk_python.hapi.services import ( response_header_pb2, response_pb2, @@ -504,3 +504,137 @@ def test_topic_message_submit_execute_all_throw_error_when_transaction_fails(top with pytest.raises(PrecheckError, match="Transaction failed precheck"): tx.execute_all(client) + +def test_topic_submit_execute_all_raises_error_with_validation(topic_id): + """Test execute_all raises error when validate_status is True and a chunk fails.""" + message = "Hello Hiero" + + tx_response = transaction_response_pb2.TransactionResponse( + nodeTransactionPrecheckCode=ResponseCode.OK + ) + + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ) + ) + ) + + response_sequence = [tx_response, receipt_response] + + with mock_hedera_servers([response_sequence]) as client: + tx = ( + TopicMessageSubmitTransaction() + .set_topic_id(topic_id) + .set_message(message) + .freeze_with(client) + ) + + with pytest.raises(ReceiptStatusError) as e: + tx.execute_all(client, validate_status=True) + + assert e.value.status == ResponseCode.INVALID_SIGNATURE + +def test_topic_submit_execute_all_returns_failed_receipt_by_default(topic_id): + """Test execute_all returns failing receipts normally when validation is disabled.""" + message = "A" * 1024 + + tx_response = transaction_response_pb2.TransactionResponse( + nodeTransactionPrecheckCode=ResponseCode.OK + ) + + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ) + ) + ) + + response_sequence = [tx_response, receipt_response] * 4 # 4 chunks + + with mock_hedera_servers([response_sequence]) as client: + tx = ( + TopicMessageSubmitTransaction() + .set_topic_id(topic_id) + .set_message(message) + .freeze_with(client) + ) + + receipt = tx.execute_all(client) + + assert receipt[0].status == ResponseCode.INVALID_SIGNATURE + +def test_topic_submit_execute_raises_error_with_validation(topic_id): + """Test execute raises error for failing messages when validate_status is True.""" + message = "A" * 1024 + + tx_response = transaction_response_pb2.TransactionResponse( + nodeTransactionPrecheckCode=ResponseCode.OK + ) + + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ) + ) + ) + + response_sequence = [tx_response, receipt_response] * 4 + + with mock_hedera_servers([response_sequence]) as client: + tx = ( + TopicMessageSubmitTransaction() + .set_topic_id(topic_id) + .set_message(message) + .freeze_with(client) + ) + + with pytest.raises(ReceiptStatusError) as e: + tx.execute(client, validate_status=True) + + assert e.value.status == ResponseCode.INVALID_SIGNATURE + +def test_topic_submit_execute_returns_failed_receipt_by_default(topic_id): + """Test execute returns the failing receipt by default when validation is disabled.""" + message = "Hello Hiero" + + tx_response = transaction_response_pb2.TransactionResponse( + nodeTransactionPrecheckCode=ResponseCode.OK + ) + + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ) + ) + ) + + response_sequence = [tx_response, receipt_response] + + with mock_hedera_servers([response_sequence]) as client: + tx = ( + TopicMessageSubmitTransaction() + .set_topic_id(topic_id) + .set_message(message) + .freeze_with(client) + ) + + receipt = tx.execute(client) + + assert receipt.status == ResponseCode.INVALID_SIGNATURE diff --git a/tests/unit/transaction_record_test.py b/tests/unit/transaction_record_test.py index 426379123..4e9078075 100644 --- a/tests/unit/transaction_record_test.py +++ b/tests/unit/transaction_record_test.py @@ -1,19 +1,28 @@ -from hiero_sdk_python.tokens.token_airdrop_pending_id import PendingAirdropId -from hiero_sdk_python.tokens.token_airdrop_pending_record import PendingAirdropRecord -import pytest +"""Unit tests for the TransactionRecord class functionality.""" + from collections import defaultdict + +import pytest + from hiero_sdk_python.account.account_id import AccountId -from hiero_sdk_python.tokens.token_id import TokenId -from hiero_sdk_python.tokens.token_nft_transfer import TokenNftTransfer -from hiero_sdk_python.transaction.transaction_record import TransactionRecord -from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt -from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.contract.contract_function_result import ContractFunctionResult +from hiero_sdk_python.contract.contract_id import ContractId from hiero_sdk_python.hapi.services import ( - transaction_record_pb2, transaction_receipt_pb2, + transaction_record_pb2, ) -from hiero_sdk_python.contract.contract_function_result import ContractFunctionResult -from hiero_sdk_python.contract.contract_id import ContractId +from hiero_sdk_python.response_code import ResponseCode +from hiero_sdk_python.schedule.schedule_id import ScheduleId +from hiero_sdk_python.timestamp import Timestamp +from hiero_sdk_python.tokens.assessed_custom_fee import AssessedCustomFee +from hiero_sdk_python.tokens.token_airdrop_pending_id import PendingAirdropId +from hiero_sdk_python.tokens.token_airdrop_pending_record import PendingAirdropRecord +from hiero_sdk_python.tokens.token_association import TokenAssociation +from hiero_sdk_python.tokens.token_id import TokenId +from hiero_sdk_python.tokens.token_nft_transfer import TokenNftTransfer +from hiero_sdk_python.transaction.transaction_id import TransactionId # noqa: F401 +from hiero_sdk_python.transaction.transaction_receipt import TransactionReceipt +from hiero_sdk_python.transaction.transaction_record import TransactionRecord pytestmark = pytest.mark.unit @@ -27,7 +36,10 @@ def transaction_record(transaction_id): ), transaction_id=transaction_id ) - + + ts = Timestamp(seconds=1234567890, nanos=500000000) + sched = ScheduleId(0, 0, 9999) + return TransactionRecord( transaction_id=transaction_id, transaction_hash=b'\x01\x02\x03\x04' * 12, @@ -41,12 +53,37 @@ def transaction_record(transaction_id): prng_number=100, prng_bytes=None, children=[], + consensus_timestamp=ts, + schedule_ref=sched, + assessed_custom_fees=[ + AssessedCustomFee( + amount=500000000, + fee_collector_account_id=AccountId(shard=0, realm=0, num=98) + ) + ], + automatic_token_associations=[ + TokenAssociation( + token_id=TokenId(shard=0, realm=0, num=5678), + account_id=AccountId(shard=0, realm=0, num=1234) + ) + ], + parent_consensus_timestamp=ts, + alias=b'\x12\x34\x56\x78', + ethereum_hash=b'\xab' * 32, + paid_staking_rewards=[ + (AccountId(shard=0, realm=0, num=456), 1000000) + ], + evm_address=b'\x12' * 20, + contract_create_result=ContractFunctionResult( + contract_id=ContractId(shard=0, realm=0, contract=789), + contract_call_result=b"Mock result" + ), ) @pytest.fixture def proto_transaction_record(transaction_id): """Create a mock transaction record protobuf.""" - proto = transaction_record_pb2.TransactionRecord( + return transaction_record_pb2.TransactionRecord( transactionHash=b'\x01\x02\x03\x04' * 12, memo="Test transaction memo", transactionFee=100000, @@ -54,10 +91,9 @@ def proto_transaction_record(transaction_id): transactionID=transaction_id._to_proto(), prng_number=100, ) - return proto def test_transaction_record_initialization(transaction_record, transaction_id): - """Test the initialization of the TransactionRecord class""" + """Test the initialization of the TransactionRecord class.""" assert transaction_record.transaction_id == transaction_id assert transaction_record.transaction_hash == b'\x01\x02\x03\x04' * 12 assert transaction_record.transaction_memo == "Test transaction memo" @@ -70,7 +106,7 @@ def test_transaction_record_initialization(transaction_record, transaction_id): def test_transaction_record_default_initialization(): - """Test the default initialization of the TransactionRecord class""" + """Test the default initialization of the TransactionRecord class.""" record = TransactionRecord() assert record.transaction_id is None assert record.transaction_hash is None @@ -89,8 +125,19 @@ def test_transaction_record_default_initialization(): assert record.duplicates == [] assert record.children == [] + assert record.consensus_timestamp is None + assert record.schedule_ref is None + assert record.assessed_custom_fees == [] + assert record.automatic_token_associations == [] + assert record.parent_consensus_timestamp is None + assert record.alias is None + assert record.ethereum_hash is None + assert record.paid_staking_rewards == [] + assert record.evm_address is None + assert record.contract_create_result is None + def test_from_proto(proto_transaction_record, transaction_id): - """Test the from_proto method of the TransactionRecord class""" + """Test the from_proto method of the TransactionRecord class.""" record = TransactionRecord._from_proto(proto_transaction_record, transaction_id) assert record.transaction_id == transaction_id @@ -100,10 +147,21 @@ def test_from_proto(proto_transaction_record, transaction_id): assert isinstance(record.receipt, TransactionReceipt) assert record.receipt.status == ResponseCode.SUCCESS assert record.prng_number == 100 - assert record.prng_bytes == b"" + assert record.prng_bytes is None + + assert record.consensus_timestamp is None + assert record.schedule_ref is None + assert record.assessed_custom_fees == [] + assert record.automatic_token_associations == [] + assert record.parent_consensus_timestamp is None + assert record.alias is None + assert record.ethereum_hash is None + assert record.paid_staking_rewards == [] + assert record.evm_address is None + assert record.contract_create_result is None def test_from_proto_with_transfers(transaction_id): - """Test from_proto with HBAR transfers""" + """Test from_proto with HBAR transfers.""" proto = transaction_record_pb2.TransactionRecord() transfer = proto.transferList.accountAmounts.add() transfer.accountID.CopyFrom(AccountId(0, 0, 200)._to_proto()) @@ -113,7 +171,7 @@ def test_from_proto_with_transfers(transaction_id): assert record.transfers[AccountId(0, 0, 200)] == 1000 def test_from_proto_with_token_transfers(transaction_id): - """Test from_proto with token transfers""" + """Test from_proto with token transfers.""" proto = transaction_record_pb2.TransactionRecord() token_list = proto.tokenTransferLists.add() token_list.token.CopyFrom(TokenId(0, 0, 300)._to_proto()) @@ -126,7 +184,7 @@ def test_from_proto_with_token_transfers(transaction_id): assert record.token_transfers[TokenId(0, 0, 300)][AccountId(0, 0, 200)] == 500 def test_from_proto_with_nft_transfers(transaction_id): - """Test from_proto with NFT transfers""" + """Test from_proto with NFT transfers.""" proto = transaction_record_pb2.TransactionRecord() token_list = proto.tokenTransferLists.add() token_list.token.CopyFrom(TokenId(0, 0, 300)._to_proto()) @@ -143,10 +201,10 @@ def test_from_proto_with_nft_transfers(transaction_id): assert transfer.sender_id == AccountId(0, 0, 100) assert transfer.receiver_id == AccountId(0, 0, 200) assert transfer.serial_number == 1 - assert transfer.is_approved == False + assert transfer.is_approved is False def test_from_proto_with_new_pending_airdrops(transaction_id): - """Test from_proto with Pending Airdrops""" + """Test from_proto with Pending Airdrops.""" sender = AccountId(0,0,100) receiver = AccountId(0,0,200) token_id = TokenId(0,0,1) @@ -166,27 +224,155 @@ def test_from_proto_with_new_pending_airdrops(transaction_id): def test_from_proto_with_prng_number(transaction_id): - """Test from_proto with prng_number set""" + """Test from_proto with prng_number set.""" proto = transaction_record_pb2.TransactionRecord() proto.prng_number = 42 record = TransactionRecord._from_proto(proto, transaction_id) assert record.prng_number == 42 - assert record.prng_bytes == b"" + assert record.prng_bytes is None def test_from_proto_with_prng_bytes(transaction_id): - """Test from_proto with prng_bytes set""" + """Test from_proto with prng_bytes set.""" proto = transaction_record_pb2.TransactionRecord() proto.prng_bytes = b"123" record = TransactionRecord._from_proto(proto, transaction_id) assert record.prng_bytes == b"123" - assert record.prng_number == 0 + assert record.prng_number is None + +def test_from_proto_with_consensus_timestamp(transaction_id): + """Test parsing consensus_timestamp from proto.""" + proto = transaction_record_pb2.TransactionRecord() + proto.consensusTimestamp.seconds = 1234567890 + proto.consensusTimestamp.nanos = 500000000 + + record = TransactionRecord._from_proto(proto, transaction_id) + assert record.consensus_timestamp is not None + assert record.consensus_timestamp.seconds == 1234567890 + assert record.consensus_timestamp.nanos == 500000000 + + +def test_from_proto_with_schedule_ref(transaction_id): + """Test parsing schedule_ref from proto.""" + proto = transaction_record_pb2.TransactionRecord() + proto.scheduleRef.shardNum = 0 + proto.scheduleRef.realmNum = 0 + proto.scheduleRef.scheduleNum = 9999 + + record = TransactionRecord._from_proto(proto, transaction_id) + assert record.schedule_ref is not None + assert record.schedule_ref.shard == 0 + assert record.schedule_ref.realm == 0 + assert record.schedule_ref.schedule == 9999 + + +def test_from_proto_with_assessed_custom_fees(transaction_id): + """Test parsing assessed_custom_fees from proto.""" + proto = transaction_record_pb2.TransactionRecord() + fee = proto.assessed_custom_fees.add() + fee.amount = 500000000 + fee.fee_collector_account_id.shardNum = 0 + fee.fee_collector_account_id.realmNum = 0 + fee.fee_collector_account_id.accountNum = 98 + + record = TransactionRecord._from_proto(proto, transaction_id) + assert len(record.assessed_custom_fees) == 1 + f = record.assessed_custom_fees[0] + assert isinstance(f, AssessedCustomFee) + assert f.amount == 500000000 + assert f.fee_collector_account_id.num == 98 + + +def test_from_proto_with_automatic_token_associations(transaction_id): + """Test parsing automatic_token_associations from proto.""" + proto = transaction_record_pb2.TransactionRecord() + assoc = proto.automatic_token_associations.add() + assoc.token_id.shardNum = 0 + assoc.token_id.realmNum = 0 + assoc.token_id.tokenNum = 5678 + assoc.account_id.shardNum = 0 + assoc.account_id.realmNum = 0 + assoc.account_id.accountNum = 1234 + + record = TransactionRecord._from_proto(proto, transaction_id) + assert len(record.automatic_token_associations) == 1 + a = record.automatic_token_associations[0] + assert isinstance(a, TokenAssociation) + assert a.token_id.num == 5678 + assert a.account_id.num == 1234 + + +def test_from_proto_with_parent_consensus_timestamp(transaction_id): + """Test parsing parent_consensus_timestamp from proto.""" + proto = transaction_record_pb2.TransactionRecord() + proto.parent_consensus_timestamp.seconds = 9876543210 + proto.parent_consensus_timestamp.nanos = 0 + + record = TransactionRecord._from_proto(proto, transaction_id) + assert record.parent_consensus_timestamp is not None + assert record.parent_consensus_timestamp.seconds == 9876543210 + + +def test_from_proto_with_alias(transaction_id): + """Test parsing alias bytes from proto.""" + proto = transaction_record_pb2.TransactionRecord() + proto.alias = b'\x12\x34\x56\x78' + + record = TransactionRecord._from_proto(proto, transaction_id) + assert record.alias == b'\x12\x34\x56\x78' + + +def test_from_proto_with_ethereum_hash(transaction_id): + """Test parsing ethereum_hash from proto.""" + proto = transaction_record_pb2.TransactionRecord() + proto.ethereum_hash = b'\xab' * 32 + record = TransactionRecord._from_proto(proto, transaction_id) + assert record.ethereum_hash == b'\xab' * 32 + + +def test_from_proto_with_paid_staking_rewards(transaction_id): + """Test parsing paid_staking_rewards from proto.""" + proto = transaction_record_pb2.TransactionRecord() + reward = proto.paid_staking_rewards.add() + reward.accountID.shardNum = 0 + reward.accountID.realmNum = 0 + reward.accountID.accountNum = 456 + reward.amount = 1000000 + + record = TransactionRecord._from_proto(proto, transaction_id) + assert len(record.paid_staking_rewards) == 1 + account, amount = record.paid_staking_rewards[0] + assert account.num == 456 + assert amount == 1000000 + + +def test_from_proto_with_evm_address(transaction_id): + """Test parsing evm_address from proto.""" + proto = transaction_record_pb2.TransactionRecord() + proto.evm_address = b'\x12' * 20 + + record = TransactionRecord._from_proto(proto, transaction_id) + assert record.evm_address == b'\x12' * 20 + + +def test_from_proto_with_contract_create_result(transaction_id): + """Test parsing contract_create_result from proto.""" + proto = transaction_record_pb2.TransactionRecord() + proto.contractCreateResult.contractID.shardNum = 0 + proto.contractCreateResult.contractID.realmNum = 0 + proto.contractCreateResult.contractID.contractNum = 789 + proto.contractCreateResult.contractCallResult = b"Created!" + + record = TransactionRecord._from_proto(proto, transaction_id) + assert record.contract_create_result is not None + assert record.contract_create_result.contract_id.contract == 789 + assert record.contract_create_result.contract_call_result == b"Created!" def test_to_proto(transaction_record, transaction_id): - """Test the to_proto method of the TransactionRecord class""" + """Test the to_proto method of the TransactionRecord class.""" proto = transaction_record._to_proto() assert proto.transactionHash == b'\x01\x02\x03\x04' * 12 @@ -198,7 +384,7 @@ def test_to_proto(transaction_record, transaction_id): assert proto.prng_bytes == b"" def test_proto_conversion(transaction_record): - """Test converting TransactionRecord to proto and back preserves data""" + """Test converting TransactionRecord to proto and back preserves data.""" proto = transaction_record._to_proto() converted = TransactionRecord._from_proto(proto, transaction_record.transaction_id) @@ -208,10 +394,10 @@ def test_proto_conversion(transaction_record): assert converted.transaction_fee == transaction_record.transaction_fee assert converted.receipt.status == transaction_record.receipt.status assert converted.prng_number == transaction_record.prng_number - assert converted.prng_bytes == b"" + assert converted.prng_bytes is None def test_proto_conversion_with_transfers(transaction_id): - """Test proto conversion preserves transfer data""" + """Test proto conversion preserves transfer data.""" record = TransactionRecord() record.transfers = defaultdict(int) record.transfers[AccountId(0, 0, 100)] = -1000 @@ -224,7 +410,7 @@ def test_proto_conversion_with_transfers(transaction_id): assert converted.transfers[AccountId(0, 0, 200)] == 1000 def test_proto_conversion_with_token_transfers(transaction_id): - """Test proto conversion preserves token transfer data""" + """Test proto conversion preserves token transfer data.""" record = TransactionRecord() token_id = TokenId(0, 0, 300) record.token_transfers = defaultdict(lambda: defaultdict(int)) @@ -238,7 +424,7 @@ def test_proto_conversion_with_token_transfers(transaction_id): assert converted.token_transfers[token_id][AccountId(0, 0, 200)] == 500 def test_proto_conversion_with_nft_transfers(transaction_id): - """Test proto conversion preserves NFT transfer data""" + """Test proto conversion preserves NFT transfer data.""" record = TransactionRecord() token_id = TokenId(0, 0, 300) nft_transfer = TokenNftTransfer( @@ -259,10 +445,10 @@ def test_proto_conversion_with_nft_transfers(transaction_id): assert transfer.sender_id == AccountId(0, 0, 100) assert transfer.receiver_id == AccountId(0, 0, 200) assert transfer.serial_number == 1 - assert transfer.is_approved == False + assert not transfer.is_approved def test_proto_conversion_with_new_pending_airdrops(transaction_id): - """Test proto conversion preserves PendingAirdropsRecord""" + """Test proto conversion preserves PendingAirdropsRecord.""" sender = AccountId(0,0,100) receiver = AccountId(0,0,200) token_id = TokenId(0,0,1) @@ -304,7 +490,17 @@ def test_repr_method(transaction_id): "prng_number=None, " "prng_bytes=None, " "duplicates_count=0, " - "children_count=0)" + "children_count=0, " + "consensus_timestamp=None, " + "schedule_ref=None, " + "assessed_custom_fees=[], " + "automatic_token_associations=[], " + "parent_consensus_timestamp=None, " + "alias=None, " + "ethereum_hash=None, " + "paid_staking_rewards=[], " + "evm_address=None, " + "contract_create_result=None)" ) assert repr(record_default) == expected_repr_default @@ -336,40 +532,63 @@ def test_repr_method(transaction_id): f"prng_number=None, " f"prng_bytes=None, " f"duplicates_count=0, " - f"children_count=0)" + f"children_count=0, " + f"consensus_timestamp=None, " + f"schedule_ref=None, " + f"assessed_custom_fees=[], " + f"automatic_token_associations=[], " + f"parent_consensus_timestamp=None, " + f"alias=None, " + f"ethereum_hash=None, " + f"paid_staking_rewards=[], " + f"evm_address=None, " + f"contract_create_result=None)" ) assert repr(record_with_receipt) == expected_repr_with_receipt # Test with all parameters set + ts = Timestamp(1234567890, 0) + sched = ScheduleId(0, 0, 9999) record_full = TransactionRecord( transaction_id=transaction_id, transaction_hash=b'\x01\x02\x03\x04', transaction_memo="Test memo", transaction_fee=100000, receipt=receipt, + consensus_timestamp=ts, + schedule_ref=sched, + assessed_custom_fees=[AssessedCustomFee( + amount=500000000, + fee_collector_account_id=AccountId(0, 0, 98) + )], + automatic_token_associations=[TokenAssociation(token_id=TokenId(0, 0, 5678), account_id=AccountId(0, 0, 1234))], + parent_consensus_timestamp=ts, + alias=b'\x12\x34', + ethereum_hash=b'\xab' * 32, + paid_staking_rewards=[(AccountId(0, 0, 456), 1000000)], + evm_address=b'\x12' * 20, + contract_create_result=ContractFunctionResult(contract_id=ContractId(0, 0, 789)), ) + repr_full = repr(record_full) - assert "duplicates_count=0" in repr_full assert f"transaction_id='{transaction_id}'" in repr_full assert "transaction_hash=b'\\x01\\x02\\x03\\x04'" in repr_full assert "transaction_memo='Test memo'" in repr_full assert "transaction_fee=100000" in repr_full assert "receipt_status='SUCCESS'" in repr_full - expected_repr_full = (f"TransactionRecord(transaction_id='{transaction_id}', " - f"transaction_hash=b'\\x01\\x02\\x03\\x04', " - f"transaction_memo='Test memo', " - f"transaction_fee=100000, " - f"receipt_status='SUCCESS', " - f"token_transfers={{}}, " - f"nft_transfers={{}}, " - f"transfers={{}}, " - f"new_pending_airdrops={[]}, " - f"call_result=None, " - f"prng_number=None, " - f"prng_bytes=None, " - f"duplicates_count=0, " - f"children_count=0)") - assert repr(record_full) == expected_repr_full + assert "consensus_timestamp=" in repr_full + assert f"schedule_ref={sched}" in repr_full + assert "assessed_custom_fees=" in repr_full + assert "automatic_token_associations=" in repr_full + assert "parent_consensus_timestamp=" in repr_full + assert "alias=b'\\x124'" in repr_full + assert "ethereum_hash=" in repr_full + assert "paid_staking_rewards=" in repr_full + assert "evm_address=" in repr_full + assert "contract_create_result=" in repr_full + assert "duplicates_count=0" in repr_full + assert "children_count=0" in repr_full + # Test with transfers record_with_transfers = TransactionRecord( transaction_id=transaction_id, receipt=receipt @@ -393,7 +612,17 @@ def test_repr_method(transaction_id): f"prng_number=None, " f"prng_bytes=None, " f"duplicates_count=0, " - f"children_count=0)") + f"children_count=0, " + f"consensus_timestamp=None, " + f"schedule_ref=None, " + f"assessed_custom_fees=[], " + f"automatic_token_associations=[], " + f"parent_consensus_timestamp=None, " + f"alias=None, " + f"ethereum_hash=None, " + f"paid_staking_rewards=[], " + f"evm_address=None, " + f"contract_create_result=None)") assert repr(record_with_transfers) == expected_repr_with_transfers def test_proto_conversion_with_call_result(transaction_id): @@ -441,7 +670,6 @@ def test_from_proto_without_duplicates_param_backward_compat(transaction_id): proto = transaction_record_pb2.TransactionRecord() proto.memo = "Test" - # Call without duplicates parameter - should not raise record = TransactionRecord._from_proto(proto, transaction_id) assert record.duplicates == [], "Duplicates should default to empty list when omitted" @@ -503,12 +731,11 @@ def test_repr_includes_duplicates_count(transaction_id): record.duplicates = [dup, dup] assert "duplicates_count=2" in repr(record), "duplicates_count should reflect list length" - + def test_from_proto_raises_when_no_transaction_id_available(): """Verify error is raised when neither transaction_id param nor proto.transactionID is present.""" proto = transaction_record_pb2.TransactionRecord() - # Force-clear the field (works in protobuf 3 & 4) proto.ClearField("transactionID") assert not proto.HasField("transactionID"), "Field should be absent after ClearField" @@ -518,7 +745,7 @@ def test_from_proto_raises_when_no_transaction_id_available(): def test_transaction_record_children_not_shared_between_instances(): - """children not shared between two different instances""" + """Children not shared between two different instances.""" r1 = TransactionRecord() r2 = TransactionRecord() @@ -526,3 +753,128 @@ def test_transaction_record_children_not_shared_between_instances(): assert len(r1.children) == 1 assert len(r2.children) == 0 + +def test_round_trip_consensus_timestamp(transaction_id): + """Test consensus timestamp survives protobuf round-trip.""" + ts = Timestamp(1234567890, 500000000) + original = TransactionRecord(consensus_timestamp=ts) + proto = original._to_proto() + round_tripped = TransactionRecord._from_proto(proto, transaction_id) + assert round_tripped.consensus_timestamp == ts + + +def test_round_trip_schedule_ref(transaction_id): + """Test schedule reference survives protobuf round-trip.""" + sched = ScheduleId(0, 0, 9999) + original = TransactionRecord(schedule_ref=sched) + proto = original._to_proto() + round_tripped = TransactionRecord._from_proto(proto, transaction_id) + assert round_tripped.schedule_ref == sched + + +def test_round_trip_assessed_custom_fees(transaction_id): + """Test custom fees survive protobuf round-trip.""" + fee = AssessedCustomFee( + amount=500000000, + fee_collector_account_id=AccountId(0, 0, 98) + ) + original = TransactionRecord(assessed_custom_fees=[fee]) + proto = original._to_proto() + round_tripped = TransactionRecord._from_proto(proto, transaction_id) + assert len(round_tripped.assessed_custom_fees) == 1 + assert round_tripped.assessed_custom_fees[0].amount == 500000000 + + +def test_round_trip_automatic_token_associations(transaction_id): + """Test token associations survive protobuf round-trip.""" + assoc = TokenAssociation( + token_id=TokenId(0, 0, 5678), + account_id=AccountId(0, 0, 1234) + ) + original = TransactionRecord(automatic_token_associations=[assoc]) + proto = original._to_proto() + round_tripped = TransactionRecord._from_proto(proto, transaction_id) + assert len(round_tripped.automatic_token_associations) == 1 + assert round_tripped.automatic_token_associations[0].token_id.num == 5678 + + +def test_round_trip_parent_consensus_timestamp(transaction_id): + """Test parent timestamp survives protobuf round-trip.""" + ts = Timestamp(9876543210, 0) + original = TransactionRecord(parent_consensus_timestamp=ts) + proto = original._to_proto() + round_tripped = TransactionRecord._from_proto(proto, transaction_id) + assert round_tripped.parent_consensus_timestamp == ts + + +def test_round_trip_alias(transaction_id): + """Test alias bytes survive protobuf round-trip.""" + original = TransactionRecord(alias=b'\x12\x34') + proto = original._to_proto() + round_tripped = TransactionRecord._from_proto(proto, transaction_id) + assert round_tripped.alias == b'\x12\x34' + + +def test_round_trip_ethereum_hash(transaction_id): + """Test ethereum hash survives protobuf round-trip.""" + original = TransactionRecord(ethereum_hash=b'\xab' * 32) + proto = original._to_proto() + round_tripped = TransactionRecord._from_proto(proto, transaction_id) + assert round_tripped.ethereum_hash == b'\xab' * 32 + + +def test_round_trip_paid_staking_rewards(transaction_id): + """Test staking rewards survive protobuf round-trip.""" + rewards = [(AccountId(0, 0, 456), 1000000)] + original = TransactionRecord(paid_staking_rewards=rewards) + proto = original._to_proto() + round_tripped = TransactionRecord._from_proto(proto, transaction_id) + assert round_tripped.paid_staking_rewards == rewards + + +def test_round_trip_evm_address(transaction_id): + """Test EVM address survives protobuf round-trip.""" + original = TransactionRecord(evm_address=b'\x12' * 20) + proto = original._to_proto() + round_tripped = TransactionRecord._from_proto(proto, transaction_id) + assert round_tripped.evm_address == b'\x12' * 20 + + +def test_round_trip_contract_create_result(transaction_id): + """Test contract creation result survives round-trip.""" + result = ContractFunctionResult( + contract_id=ContractId(0, 0, 789), + contract_call_result=b"Created!" + ) + original = TransactionRecord(contract_create_result=result) + proto = original._to_proto() + round_tripped = TransactionRecord._from_proto(proto, transaction_id) + assert round_tripped.contract_create_result.contract_id == result.contract_id + assert round_tripped.contract_create_result.contract_call_result == b"Created!" + +def test_to_proto_raises_when_both_call_and_create_result_set(transaction_id): + """Setting both call_result and contract_create_result must raise (protobuf oneof).""" + record = TransactionRecord( + transaction_id=transaction_id, + call_result=ContractFunctionResult( + contract_id=ContractId(0, 0, 100), + ), + contract_create_result=ContractFunctionResult( + contract_id=ContractId(0, 0, 200), + ), + ) + + with pytest.raises(ValueError, match="mutually exclusive"): + record._to_proto() + +def test_to_proto_raises_when_both_prng_fields_set(transaction_id): + """Setting both prng_number and prng_bytes must raise (entropy oneof).""" + record = TransactionRecord( + transaction_id=transaction_id, + prng_number=1, + prng_bytes=b"\x01", + ) + + with pytest.raises(ValueError, match="mutually exclusive"): + record._to_proto() + \ No newline at end of file diff --git a/tests/unit/transaction_response_test.py b/tests/unit/transaction_response_test.py index 7ebff9189..5a594b1d1 100644 --- a/tests/unit/transaction_response_test.py +++ b/tests/unit/transaction_response_test.py @@ -1,6 +1,7 @@ import pytest from hiero_sdk_python.account.account_id import AccountId +from hiero_sdk_python.exceptions import ReceiptStatusError from hiero_sdk_python.hapi.services import ( basic_types_pb2, response_header_pb2, @@ -71,6 +72,56 @@ def test_get_receipt_executes_and_returns_receipt(transaction_response): assert receipt.account_id.num == 1234 +def test_get_receipt_query_set_validate_status(transaction_response): + """Test receipt query correctly initializes with the validate_status flag.""" + query = transaction_response.get_receipt_query(validate_status=True) + + assert isinstance(query, TransactionGetReceiptQuery) + assert query.validate_status == True + assert query.transaction_id == transaction_response.transaction_id + assert len(query.node_account_ids) == 1 + assert query.node_account_ids[0] == transaction_response.node_id + + +def test_get_receipt_returns_failure_status_without_validate_status(transaction_response): + """Test failing status returns a receipt instead of raising an error when validation is disabled.""" + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ), + ) + ) + + with mock_hedera_servers([[receipt_response]]) as client: + receipt = transaction_response.get_receipt(client) + + assert isinstance(receipt, TransactionReceipt) + assert receipt.status == ResponseCode.INVALID_SIGNATURE + +def test_get_receipt_raises_exception_with_validate_status(transaction_response): + """Test get_receipt error is raised for non-success statuses when validation is enabled.""" + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ), + ) + ) + + with mock_hedera_servers([[receipt_response]]) as client: + with pytest.raises(ReceiptStatusError) as e: + transaction_response.get_receipt(client, validate_status=True) + + assert e.value.status == ResponseCode.INVALID_SIGNATURE + + def test_get_record_query_builds_query(transaction_response): """Test get_record_query builds and returns the transaction record query.""" query = transaction_response.get_record_query() diff --git a/tests/unit/transaction_test.py b/tests/unit/transaction_test.py index 3b1a3eff9..d3504e144 100644 --- a/tests/unit/transaction_test.py +++ b/tests/unit/transaction_test.py @@ -2,6 +2,7 @@ from hiero_sdk_python.account.account_create_transaction import AccountCreateTransaction from hiero_sdk_python.crypto.private_key import PrivateKey +from hiero_sdk_python.exceptions import ReceiptStatusError from hiero_sdk_python.hapi.services import ( basic_types_pb2, response_header_pb2, @@ -90,3 +91,64 @@ def test_execute_without_wait_returns_transaction_response(): assert response.transaction is tx assert response.node_id == tx.node_account_id assert response.validate_status is True + +def test_execute_raises_error_when_validation_enabled_and_transaction_fails(): + """Test execute raises error for failing transactions when validate_status is True.""" + ok_response = transaction_response_pb2.TransactionResponse( + nodeTransactionPrecheckCode=ResponseCode.OK + ) + + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ), + ) + ) + + response_sequence = [[ok_response, receipt_response]] + + with mock_hedera_servers(response_sequence) as client: + tx = ( + AccountCreateTransaction() + .set_initial_balance(1) + .set_key_without_alias(PrivateKey.generate()) + ) + + with pytest.raises(ReceiptStatusError) as e: + tx.execute(client, validate_status=True) + + assert e.value.status == ResponseCode.INVALID_SIGNATURE + +def test_execute_returns_receipt_without_error_when_validation_disabled(): + """Test execute returns a receipt normally on failure when validate_status is False.""" + ok_response = transaction_response_pb2.TransactionResponse( + nodeTransactionPrecheckCode=ResponseCode.OK + ) + + receipt_response = response_pb2.Response( + transactionGetReceipt=transaction_get_receipt_pb2.TransactionGetReceiptResponse( + header=response_header_pb2.ResponseHeader( + nodeTransactionPrecheckCode=ResponseCode.OK + ), + receipt=transaction_receipt_pb2.TransactionReceipt( + status=ResponseCode.INVALID_SIGNATURE + ), + ) + ) + + response_sequence = [[ok_response, receipt_response]] + + with mock_hedera_servers(response_sequence) as client: + tx = ( + AccountCreateTransaction() + .set_initial_balance(1) + .set_key_without_alias(PrivateKey.generate()) + ) + + receipt = tx.execute(client) + + assert receipt.status == ResponseCode.INVALID_SIGNATURE diff --git a/uv.lock b/uv.lock new file mode 100644 index 000000000..e966d6055 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1697 @@ +version = 1 +revision = 3 +requires-python = ">=3.10, <4" + +[[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 = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, +] + +[[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 = "coverage" +version = "7.13.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" }, + { url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" }, + { url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" }, + { url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" }, + { url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" }, + { url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" }, + { url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, + { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, + { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, + { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, + { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, + { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, + { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, + { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "46.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, + { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, + { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, + { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, + { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, + { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, + { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, + { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, +] + +[[package]] +name = "cytoolz" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/d4/16916f3dc20a3f5455b63c35dcb260b3716f59ce27a93586804e70e431d5/cytoolz-1.1.0.tar.gz", hash = "sha256:13a7bf254c3c0d28b12e2290b82aed0f0977a4c2a2bf84854fcdc7796a29f3b0", size = 642510, upload-time = "2025-10-19T00:44:56.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/7a/3244e6e3587be9abfee3b1c320e43a279831b3c3a31fe5d08c1ee6193e6b/cytoolz-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:72d7043a88ea5e61ba9d17ea0d1c1eff10f645d7edfcc4e56a31ef78be287644", size = 1307813, upload-time = "2025-10-19T00:39:34.198Z" }, + { url = "https://files.pythonhosted.org/packages/32/7e/eaf504ca59addce323ef4d4ffedc2913d83c121ec19f6419bc402f7702dc/cytoolz-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d759e9ed421bacfeb456d47af8d734c057b9912b5f2441f95b27ca35e5efab07", size = 985777, upload-time = "2025-10-19T00:39:36.545Z" }, + { url = "https://files.pythonhosted.org/packages/d4/a1/ec95443f0cf4cd0dbc574fa26ac85a0442d35f3b601a90a0e3dda077f614/cytoolz-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fdb5be8fbcc0396141189022724155a4c1c93712ac4aef8c03829af0c2a816d7", size = 982865, upload-time = "2025-10-19T00:39:38.19Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1b/8503604b0c0534977363fb77d371019395dfa031a216f9b1d8729d1280e4/cytoolz-1.1.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c8c0a513dc89bc05cc72893609118815bced5ef201f1a317b4cc3423b3a0e750", size = 2597969, upload-time = "2025-10-19T00:39:40.26Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e5/30748da06417cb2d4bc58e380b0c11d8c6539f4e289dc1e4f4b4fc248d0e/cytoolz-1.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce94db4f8ebe842c30c0ece42ff5de977c47859088c2c363dede5a68f6906484", size = 2692230, upload-time = "2025-10-19T00:39:42.327Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/e06580b74deb97dfd3513e4e6b660c2dedc220c7653f5bd3e4f772f4d885/cytoolz-1.1.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b622d4f54e370c853ded94a668f94fe72c6d70e06ac102f17a2746661c27ab52", size = 2565243, upload-time = "2025-10-19T00:39:44.403Z" }, + { url = "https://files.pythonhosted.org/packages/91/5e/79c0122a34c33afcb5aaee1fec35be24fe16cecefb9bb8890f2908feae56/cytoolz-1.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:375a65baa5a5b4ff6a0c5ff17e170cf23312e4c710755771ca966144c24216b5", size = 2868602, upload-time = "2025-10-19T00:39:46.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/84/404698ff02b32292db1e39cc4a2fbdabe15164b092cc364902984c3ce0f4/cytoolz-1.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c0d51bcdb3203a062a78f66bbe33db5e3123048e24a5f0e1402422d79df8ee2d", size = 2905121, upload-time = "2025-10-19T00:39:48.078Z" }, + { url = "https://files.pythonhosted.org/packages/9f/33/afad6593829ba73fc87b5ae64441e380fc937f79f24a1cda60d23cb99b8c/cytoolz-1.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1010869529bb05dc9802b6d776a34ca1b6d48b9deec70ad5e2918ae175be5c2f", size = 2684382, upload-time = "2025-10-19T00:39:49.766Z" }, + { url = "https://files.pythonhosted.org/packages/ce/86/7900013a82ca9c6cadbfb22bf50d0fbfc3b192915d2bdd9fab3f69a9afba/cytoolz-1.1.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11a8f2e83295bdb33f35454d6bafcb7845b03b5881dcaed66ecbd726c7f16772", size = 2518183, upload-time = "2025-10-19T00:39:51.433Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4b/acf9be2953fed6a6d795fb66de37c367915037a998a5b3d3b69476cf91fe/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0499c5e0a8e688ed367a2e51cc13792ae8f08226c15f7d168589fc44b9b9cada", size = 2609368, upload-time = "2025-10-19T00:39:53.458Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ec/3e30455fd526f5cc37bd3dd2a0e2aafb803ae4d271e50ce53bfc30810053/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:87d44e6033d4c5e95a7d39ba59b8e105ba1c29b1ccd1d215f26477cc1d64be39", size = 2561458, upload-time = "2025-10-19T00:39:55.493Z" }, + { url = "https://files.pythonhosted.org/packages/49/27/e5815c85bb18cdf95780f9596dcfd76dee910a4d635a1924648cb8a636c6/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a68cef396a7de237f7b97422a6a450dfb111722296ba217ba5b34551832f1f6e", size = 2578236, upload-time = "2025-10-19T00:39:57.512Z" }, + { url = "https://files.pythonhosted.org/packages/17/db/588e266eff397670398ea335a809152e77b02ee92e0ec42091115b42f09b/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:06ad4c95b258141f138a93ebfdc1d76ac087afc1a82f1401100a1f44b44ba656", size = 2770523, upload-time = "2025-10-19T00:39:59.194Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ad/82be0b999c7a0a0b362cedfc183eb090b872fd42937af2d6e97d58bc70f8/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ada59a4b3c59d4ac7162e0ed08667ffa78abf48e975c8a9f9d5b9bc50720f4fd", size = 2512909, upload-time = "2025-10-19T00:40:01.199Z" }, + { url = "https://files.pythonhosted.org/packages/25/21/45f07ab0339a20c518bc9006100922babc397ab7ea5ef40a395db83b9cdd/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a8957bcaea1ba01327a9b219d2adb84144377684f51444253890dab500ca171f", size = 2755345, upload-time = "2025-10-19T00:40:03.322Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a7/e530bf2b304206f79b36d793caba1ff9448348713a41bb1ad0197714a0f2/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6d8cdc299d67eb0f3b9ecdafeeb55eb3b7b7470e2d950ac34b05ed4c7a5572b8", size = 2617790, upload-time = "2025-10-19T00:40:05.03Z" }, + { url = "https://files.pythonhosted.org/packages/9f/77/7f53092121d7431589344c7d65c3d43c4111547aafabb21d3ca9032d126c/cytoolz-1.1.0-cp310-cp310-win32.whl", hash = "sha256:d8e08464c5cdea4f6df31e84b11ed6bfd79cedb99fbcbfdc15eb9361a6053c5a", size = 900209, upload-time = "2025-10-19T00:40:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/84/e4/902578658303b9bc76b1704d3ed85e6d307d311bd9fa0b919581bea56e62/cytoolz-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7e49922a7ed54262d41960bf3b835a7700327bf79cff1e9bfc73d79021132ff8", size = 944802, upload-time = "2025-10-19T00:40:08.983Z" }, + { url = "https://files.pythonhosted.org/packages/71/9f/56a7003617b4eabd8ddfb470aacc240425cbe6ddeb756adfbbaadaa175f1/cytoolz-1.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:943a662d2e72ffc4438d43ab5a1de8d852237775a423236594a3b3e381b8032c", size = 904835, upload-time = "2025-10-19T00:40:11.024Z" }, + { url = "https://files.pythonhosted.org/packages/69/82/edf1d0c32b6222f2c22e5618d6db855d44eb59f9b6f22436ff963c5d0a5c/cytoolz-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dba8e5a8c6e3c789d27b0eb5e7ce5ed7d032a7a9aae17ca4ba5147b871f6e327", size = 1314345, upload-time = "2025-10-19T00:40:13.273Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b5/0e3c1edaa26c2bd9db90cba0ac62c85bbca84224c7ae1c2e0072c4ea64c5/cytoolz-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:44b31c05addb0889167a720123b3b497b28dd86f8a0aeaf3ae4ffa11e2c85d55", size = 989259, upload-time = "2025-10-19T00:40:15.196Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/e2b2ee9fc684867e817640764ea5807f9d25aa1e7bdba02dd4b249aab0f7/cytoolz-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:653cb18c4fc5d8a8cfce2bce650aabcbe82957cd0536827367d10810566d5294", size = 986551, upload-time = "2025-10-19T00:40:16.831Z" }, + { url = "https://files.pythonhosted.org/packages/39/9f/4e8ee41acf6674f10a9c2c9117b2f219429a5a0f09bba6135f34ca4f08a6/cytoolz-1.1.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:853a5b4806915020c890e1ce70cc056bbc1dd8bc44f2d74d555cccfd7aefba7d", size = 2688378, upload-time = "2025-10-19T00:40:18.552Z" }, + { url = "https://files.pythonhosted.org/packages/78/94/ef006f3412bc22444d855a0fc9ecb81424237fb4e5c1a1f8f5fb79ac978f/cytoolz-1.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7b44e9de86bea013fe84fd8c399d6016bbb96c37c5290769e5c99460b9c53e5", size = 2798299, upload-time = "2025-10-19T00:40:20.191Z" }, + { url = "https://files.pythonhosted.org/packages/df/aa/365953926ee8b4f2e07df7200c0d73632155908c8867af14b2d19cc9f1f7/cytoolz-1.1.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:098d628a801dc142e9740126be5624eb7aef1d732bc7a5719f60a2095547b485", size = 2639311, upload-time = "2025-10-19T00:40:22.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ee/62beaaee7df208f22590ad07ef8875519af49c52ca39d99460b14a00f15a/cytoolz-1.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:779ee4096ed7a82cffab89372ffc339631c285079dbf33dbe7aff1f6174985df", size = 2979532, upload-time = "2025-10-19T00:40:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/c5/04/2211251e450bed111ada1194dc42c461da9aea441de62a01e4085ea6de9f/cytoolz-1.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f2ce18dd99533d077e9712f9faa852f389f560351b1efd2f2bdb193a95eddde2", size = 3018632, upload-time = "2025-10-19T00:40:26.175Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a2/4a3400e4d07d3916172bf74fede08020d7b4df01595d8a97f1e9507af5ae/cytoolz-1.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac266a34437812cf841cecbfe19f355ab9c3dd1ef231afc60415d40ff12a76e4", size = 2788579, upload-time = "2025-10-19T00:40:27.878Z" }, + { url = "https://files.pythonhosted.org/packages/fe/82/bb88caa53a41f600e7763c517d50e2efbbe6427ea395716a92b83f44882a/cytoolz-1.1.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1920b9b9c13d60d0bb6cd14594b3bce0870022eccb430618c37156da5f2b7a55", size = 2593024, upload-time = "2025-10-19T00:40:29.601Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/8b25e59570da16c7a0f173b8c6ec0aa6f3abd47fd385c007485acb459896/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47caa376dafd2bdc29f8a250acf59c810ec9105cd6f7680b9a9d070aae8490ec", size = 2715304, upload-time = "2025-10-19T00:40:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/faec7696f235521b926ffdf92c102f5b029f072d28e1020364e55b084820/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5ab2c97d8aaa522b038cca9187b1153347af22309e7c998b14750c6fdec7b1cb", size = 2654461, upload-time = "2025-10-19T00:40:32.884Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/f790ed167c04b8d2a33bed30770a9b7066fc4f573321d797190e5f05685f/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4bce006121b120e8b359244ee140bb0b1093908efc8b739db8dbaa3f8fb42139", size = 2672077, upload-time = "2025-10-19T00:40:34.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b3/80b8183e7eee44f45bfa3cdd3ebdadf3dd43ffc686f96d442a6c4dded45d/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7fc0f1e4e9bb384d26e73c6657bbc26abdae4ff66a95933c00f3d578be89181b", size = 2881589, upload-time = "2025-10-19T00:40:36.315Z" }, + { url = "https://files.pythonhosted.org/packages/8f/05/ac5ba5ddb88a3ba7ecea4bf192194a838af564d22ea7a4812cbb6bd106ce/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:dd3f894ff972da1994d06ac6157d74e40dda19eb31fe5e9b7863ca4278c3a167", size = 2589924, upload-time = "2025-10-19T00:40:38.317Z" }, + { url = "https://files.pythonhosted.org/packages/8e/cd/100483cae3849d24351c8333a815dc6adaf3f04912486e59386d86d9db9a/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0846f49cf8a4496bd42659040e68bd0484ce6af819709cae234938e039203ba0", size = 2868059, upload-time = "2025-10-19T00:40:40.025Z" }, + { url = "https://files.pythonhosted.org/packages/34/6e/3a7c56b325772d39397fc3aafb4dc054273982097178b6c3917c6dad48de/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:16a3af394ade1973226d64bb2f9eb3336adbdea03ed5b134c1bbec5a3b20028e", size = 2721692, upload-time = "2025-10-19T00:40:41.621Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ca/9fdaee32c3bc769dfb7e7991d9499136afccea67e423d097b8fb3c5acbc1/cytoolz-1.1.0-cp311-cp311-win32.whl", hash = "sha256:b786c9c8aeab76cc2f76011e986f7321a23a56d985b77d14f155d5e5514ea781", size = 899349, upload-time = "2025-10-19T00:40:43.183Z" }, + { url = "https://files.pythonhosted.org/packages/fd/04/2ab98edeea90311e4029e1643e43d2027b54da61453292d9ea51a103ee87/cytoolz-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:ebf06d1c5344fb22fee71bf664234733e55db72d74988f2ecb7294b05e4db30c", size = 945831, upload-time = "2025-10-19T00:40:44.693Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8d/777d86ea6bcc68b0fc926b0ef8ab51819e2176b37aadea072aac949d5231/cytoolz-1.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:b63f5f025fac893393b186e132e3e242de8ee7265d0cd3f5bdd4dda93f6616c9", size = 904076, upload-time = "2025-10-19T00:40:46.678Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ec/01426224f7acf60183d3921b25e1a8e71713d3d39cb464d64ac7aace6ea6/cytoolz-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:99f8e134c9be11649342853ec8c90837af4089fc8ff1e8f9a024a57d1fa08514", size = 1327800, upload-time = "2025-10-19T00:40:48.674Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/e07e8fedd332ac9626ad58bea31416dda19bfd14310731fa38b16a97e15f/cytoolz-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a6f44cf9319c30feb9a50aa513d777ef51efec16f31c404409e7deb8063df64", size = 997118, upload-time = "2025-10-19T00:40:50.919Z" }, + { url = "https://files.pythonhosted.org/packages/ab/72/c0f766d63ed2f9ea8dc8e1628d385d99b41fb834ce17ac3669e3f91e115d/cytoolz-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:945580dc158c557172fca899a35a99a16fbcebf6db0c77cb6621084bc82189f9", size = 991169, upload-time = "2025-10-19T00:40:52.887Z" }, + { url = "https://files.pythonhosted.org/packages/df/4b/1f757353d1bf33e56a7391ecc9bc49c1e529803b93a9d2f67fe5f92906fe/cytoolz-1.1.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:257905ec050d04f2f856854620d1e25556fd735064cebd81b460f54939b9f9d5", size = 2700680, upload-time = "2025-10-19T00:40:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/25/73/9b25bb7ed8d419b9d6ff2ae0b3d06694de79a3f98f5169a1293ff7ad3a3f/cytoolz-1.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82779049f352fb3ab5e8c993ab45edbb6e02efb1f17f0b50f4972c706cc51d76", size = 2824951, upload-time = "2025-10-19T00:40:56.137Z" }, + { url = "https://files.pythonhosted.org/packages/0c/93/9c787f7c909e75670fff467f2504725d06d8c3f51d6dfe22c55a08c8ccd4/cytoolz-1.1.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7d3e405e435320e08c5a1633afaf285a392e2d9cef35c925d91e2a31dfd7a688", size = 2679635, upload-time = "2025-10-19T00:40:57.799Z" }, + { url = "https://files.pythonhosted.org/packages/50/aa/9ee92c302cccf7a41a7311b325b51ebeff25d36c1f82bdc1bbe3f58dc947/cytoolz-1.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:923df8f5591e0d20543060c29909c149ab1963a7267037b39eee03a83dbc50a8", size = 2938352, upload-time = "2025-10-19T00:40:59.49Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a3/3b58c5c1692c3bacd65640d0d5c7267a7ebb76204f7507aec29de7063d2f/cytoolz-1.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:25db9e4862f22ea0ae2e56c8bec9fc9fd756b655ae13e8c7b5625d7ed1c582d4", size = 3022121, upload-time = "2025-10-19T00:41:01.209Z" }, + { url = "https://files.pythonhosted.org/packages/e1/93/c647bc3334355088c57351a536c2d4a83dd45f7de591fab383975e45bff9/cytoolz-1.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7a98deb11ccd8e5d9f9441ef2ff3352aab52226a2b7d04756caaa53cd612363", size = 2857656, upload-time = "2025-10-19T00:41:03.456Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c2/43fea146bf4141deea959e19dcddf268c5ed759dec5c2ed4a6941d711933/cytoolz-1.1.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dce4ee9fc99104bc77efdea80f32ca5a650cd653bcc8a1d984a931153d3d9b58", size = 2551284, upload-time = "2025-10-19T00:41:05.347Z" }, + { url = "https://files.pythonhosted.org/packages/6f/df/cdc7a81ce5cfcde7ef523143d545635fc37e80ccacce140ae58483a21da3/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80d6da158f7d20c15819701bbda1c041f0944ede2f564f5c739b1bc80a9ffb8b", size = 2721673, upload-time = "2025-10-19T00:41:07.528Z" }, + { url = "https://files.pythonhosted.org/packages/45/be/f8524bb9ad8812ad375e61238dcaa3177628234d1b908ad0b74e3657cafd/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3b5c5a192abda123ad45ef716ec9082b4cf7d95e9ada8291c5c2cc5558be858b", size = 2722884, upload-time = "2025-10-19T00:41:09.698Z" }, + { url = "https://files.pythonhosted.org/packages/23/e6/6bb8e4f9c267ad42d1ff77b6d2e4984665505afae50a216290e1d7311431/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5b399ce7d967b1cb6280250818b786be652aa8ddffd3c0bb5c48c6220d945ab5", size = 2685486, upload-time = "2025-10-19T00:41:11.349Z" }, + { url = "https://files.pythonhosted.org/packages/d7/dd/88619f9c8d2b682562c0c886bbb7c35720cb83fda2ac9a41bdd14073d9bd/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e7e29a1a03f00b4322196cfe8e2c38da9a6c8d573566052c586df83aacc5663c", size = 2839661, upload-time = "2025-10-19T00:41:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8d/4478ebf471ee78dd496d254dc0f4ad729cd8e6ba8257de4f0a98a2838ef2/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5291b117d71652a817ec164e7011f18e6a51f8a352cc9a70ed5b976c51102fda", size = 2547095, upload-time = "2025-10-19T00:41:16.054Z" }, + { url = "https://files.pythonhosted.org/packages/e6/68/f1dea33367b0b3f64e199c230a14a6b6f243c189020effafd31e970ca527/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8caef62f846a9011676c51bda9189ae394cdd6bb17f2946ecaedc23243268320", size = 2870901, upload-time = "2025-10-19T00:41:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/33591c09dfe799b8fb692cf2ad383e2c41ab6593cc960b00d1fc8a145655/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:de425c5a8e3be7bb3a195e19191d28d9eb3c2038046064a92edc4505033ec9cb", size = 2765422, upload-time = "2025-10-19T00:41:20.075Z" }, + { url = "https://files.pythonhosted.org/packages/60/2b/a8aa233c9416df87f004e57ae4280bd5e1f389b4943d179f01020c6ec629/cytoolz-1.1.0-cp312-cp312-win32.whl", hash = "sha256:296440a870e8d1f2e1d1edf98f60f1532b9d3ab8dfbd4b25ec08cd76311e79e5", size = 901933, upload-time = "2025-10-19T00:41:21.646Z" }, + { url = "https://files.pythonhosted.org/packages/ad/33/4c9bdf8390dc01d2617c7f11930697157164a52259b6818ddfa2f94f89f4/cytoolz-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:07156987f224c6dac59aa18fb8bf91e1412f5463961862716a3381bf429c8699", size = 947989, upload-time = "2025-10-19T00:41:23.288Z" }, + { url = "https://files.pythonhosted.org/packages/35/ac/6e2708835875f5acb52318462ed296bf94ed0cb8c7cb70e62fbd03f709e3/cytoolz-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:23e616b38f5b3160c7bb45b0f84a8f3deb4bd26b29fb2dfc716f241c738e27b8", size = 903913, upload-time = "2025-10-19T00:41:24.992Z" }, + { url = "https://files.pythonhosted.org/packages/71/4a/b3ddb3ee44fe0045e95dd973746f93f033b6f92cce1fc3cbbe24b329943c/cytoolz-1.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:76c9b58555300be6dde87a41faf1f97966d79b9a678b7a526fcff75d28ef4945", size = 976728, upload-time = "2025-10-19T00:41:26.5Z" }, + { url = "https://files.pythonhosted.org/packages/42/21/a3681434aa425875dd828bb515924b0f12c37a55c7d2bc5c0c5de3aeb0b4/cytoolz-1.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d1d638b10d3144795655e9395566ce35807df09219fd7cacd9e6acbdef67946a", size = 986057, upload-time = "2025-10-19T00:41:28.911Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cb/efc1b29e211e0670a6953222afaac84dcbba5cb940b130c0e49858978040/cytoolz-1.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:26801c1a165e84786a99e03c9c9973356caaca002d66727b761fb1042878ef06", size = 992632, upload-time = "2025-10-19T00:41:30.612Z" }, + { url = "https://files.pythonhosted.org/packages/be/b0/e50621d21e939338c97faab651f58ea7fa32101226a91de79ecfb89d71e1/cytoolz-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a9a464542912d3272f6dccc5142df057c71c6a5cbd30439389a732df401afb7", size = 1317534, upload-time = "2025-10-19T00:41:32.625Z" }, + { url = "https://files.pythonhosted.org/packages/0d/6b/25aa9739b0235a5bc4c1ea293186bc6822a4c6607acfe1422423287e7400/cytoolz-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed6104fa942aa5784bf54f339563de637557e3443b105760bc4de8f16a7fc79b", size = 992336, upload-time = "2025-10-19T00:41:34.073Z" }, + { url = "https://files.pythonhosted.org/packages/e1/53/5f4deb0ff958805309d135d899c764364c1e8a632ce4994bd7c45fb98df2/cytoolz-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56161f0ab60dc4159ec343509abaf809dc88e85c7e420e354442c62e3e7cbb77", size = 986118, upload-time = "2025-10-19T00:41:35.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e3/f6255b76c8cc0debbe1c0779130777dc0434da6d9b28a90d9f76f8cb67cd/cytoolz-1.1.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:832bd36cc9123535f1945acf6921f8a2a15acc19cfe4065b1c9b985a28671886", size = 2679563, upload-time = "2025-10-19T00:41:37.926Z" }, + { url = "https://files.pythonhosted.org/packages/59/8a/acc6e39a84e930522b965586ad3a36694f9bf247b23188ee0eb47b1c9ed1/cytoolz-1.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1842636b6e034f229bf084c2bcdcfd36c8437e752eefd2c74ce9e2f10415cb6e", size = 2813020, upload-time = "2025-10-19T00:41:39.935Z" }, + { url = "https://files.pythonhosted.org/packages/db/f5/0083608286ad1716eda7c41f868e85ac549f6fd6b7646993109fa0bdfd98/cytoolz-1.1.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:823df012ab90d2f2a0f92fea453528539bf71ac1879e518524cd0c86aa6df7b9", size = 2669312, upload-time = "2025-10-19T00:41:41.55Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/d16080b575520fe5da00cede1ece4e0a4180ec23f88dcdc6a2f5a90a7f7f/cytoolz-1.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f1fcf9e7e7b3487883ff3f815abc35b89dcc45c4cf81c72b7ee457aa72d197b", size = 2922147, upload-time = "2025-10-19T00:41:43.252Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bc/716c9c1243701e58cad511eb3937fd550e645293c5ed1907639c5d66f194/cytoolz-1.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4cdb3fa1772116827f263f25b0cdd44c663b6701346a56411960534a06c082de", size = 2981602, upload-time = "2025-10-19T00:41:45.354Z" }, + { url = "https://files.pythonhosted.org/packages/14/bc/571b232996846b27f4ac0c957dc8bf60261e9b4d0d01c8d955e82329544e/cytoolz-1.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1b5c95041741b81430454db65183e133976f45ac3c03454cfa8147952568529", size = 2830103, upload-time = "2025-10-19T00:41:47.959Z" }, + { url = "https://files.pythonhosted.org/packages/5b/55/c594afb46ecd78e4b7e1fb92c947ed041807875661ceda73baaf61baba4f/cytoolz-1.1.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b2079fd9f1a65f4c61e6278c8a6d4f85edf30c606df8d5b32f1add88cbbe2286", size = 2533802, upload-time = "2025-10-19T00:41:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/93/83/1edcf95832555a78fc43b975f3ebe8ceadcc9664dd47fd33747a14df5069/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a92a320d72bef1c7e2d4c6d875125cf57fc38be45feb3fac1bfa64ea401f54a4", size = 2706071, upload-time = "2025-10-19T00:41:51.386Z" }, + { url = "https://files.pythonhosted.org/packages/e2/df/035a408df87f25cfe3611557818b250126cd2281b2104cd88395de205583/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06d1c79aa51e6a92a90b0e456ebce2288f03dd6a76c7f582bfaa3eda7692e8a5", size = 2707575, upload-time = "2025-10-19T00:41:53.305Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a4/ef78e13e16e93bf695a9331321d75fbc834a088d941f1c19e6b63314e257/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e1d7be25f6971e986a52b6d3a0da28e1941850985417c35528f6823aef2cfec5", size = 2660486, upload-time = "2025-10-19T00:41:55.542Z" }, + { url = "https://files.pythonhosted.org/packages/30/7a/2c3d60682b26058d435416c4e90d4a94db854de5be944dfd069ed1be648a/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:964b248edc31efc50a65e9eaa0c845718503823439d2fa5f8d2c7e974c2b5409", size = 2819605, upload-time = "2025-10-19T00:41:58.257Z" }, + { url = "https://files.pythonhosted.org/packages/45/92/19b722a1d83cc443fbc0c16e0dc376f8a451437890d3d9ee370358cf0709/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c9ff2b3c57c79b65cb5be14a18c6fd4a06d5036fb3f33e973a9f70e9ac13ca28", size = 2533559, upload-time = "2025-10-19T00:42:00.324Z" }, + { url = "https://files.pythonhosted.org/packages/1d/15/fa3b7891da51115204416f14192081d3dea0eaee091f123fdc1347de8dd1/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:22290b73086af600042d99f5ce52a43d4ad9872c382610413176e19fc1d4fd2d", size = 2839171, upload-time = "2025-10-19T00:42:01.881Z" }, + { url = "https://files.pythonhosted.org/packages/46/40/d3519d5cd86eebebf1e8b7174ec32dfb6ecec67b48b0cfb92bf226659b5a/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a2ade74fccd080ea793382968913ee38d7a35c921df435bbf0a6aeecf0d17574", size = 2743379, upload-time = "2025-10-19T00:42:03.809Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/a9e7511f0a13fdbefa5bf73cf8e4763878140de9453fd3e50d6ac57b6be7/cytoolz-1.1.0-cp313-cp313-win32.whl", hash = "sha256:db5dbcfda1c00e937426cbf9bdc63c24ebbc358c3263bfcbc1ab4a88dc52aa8e", size = 900844, upload-time = "2025-10-19T00:42:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a4/fb7eb403c6a4c81e5a30363f34a71adcc8bf5292dc8ea32e2440aa5668f2/cytoolz-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9e2d3fe3b45c3eb7233746f7aca37789be3dceec3e07dcc406d3e045ea0f7bdc", size = 946461, upload-time = "2025-10-19T00:42:07.983Z" }, + { url = "https://files.pythonhosted.org/packages/93/bb/1c8c33d353548d240bc6e8677ee8c3560ce5fa2f084e928facf7c35a6dcf/cytoolz-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:32c559f95ff44a9ebcbd934acaa1e6dc8f3e6ffce4762a79a88528064873d6d5", size = 902673, upload-time = "2025-10-19T00:42:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/4a53acc60f59030fcaf48c7766e3c4c81bd997379425aa45b129396557b5/cytoolz-1.1.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9e2cd93b28f667c5870a070ab2b8bb4397470a85c4b204f2454b0ad001cd1ca3", size = 1372336, upload-time = "2025-10-19T00:42:12.104Z" }, + { url = "https://files.pythonhosted.org/packages/ac/90/f28fd8ad8319d8f5c8da69a2c29b8cf52a6d2c0161602d92b366d58926ab/cytoolz-1.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f494124e141a9361f31d79875fe7ea459a3be2b9dadd90480427c0c52a0943d4", size = 1011930, upload-time = "2025-10-19T00:42:14.231Z" }, + { url = "https://files.pythonhosted.org/packages/c9/95/4561c4e0ad1c944f7673d6d916405d68080f10552cfc5d69a1cf2475a9a1/cytoolz-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53a3262bf221f19437ed544bf8c0e1980c81ac8e2a53d87a9bc075dba943d36f", size = 1020610, upload-time = "2025-10-19T00:42:15.877Z" }, + { url = "https://files.pythonhosted.org/packages/c3/14/b2e1ffa4995ec36e1372e243411ff36325e4e6d7ffa34eb4098f5357d176/cytoolz-1.1.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:47663e57d3f3f124921f38055e86a1022d0844c444ede2e8f090d3bbf80deb65", size = 2917327, upload-time = "2025-10-19T00:42:17.706Z" }, + { url = "https://files.pythonhosted.org/packages/4a/29/7cab6c609b4514ac84cca2f7dca6c509977a8fc16d27c3a50e97f105fa6a/cytoolz-1.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5a8755c4104ee4e3d5ba434c543b5f85fdee6a1f1df33d93f518294da793a60", size = 3108951, upload-time = "2025-10-19T00:42:19.363Z" }, + { url = "https://files.pythonhosted.org/packages/9a/71/1d1103b819458679277206ad07d78ca6b31c4bb88d6463fd193e19bfb270/cytoolz-1.1.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d96ff3d381423af1b105295f97de86d1db51732c9566eb37378bab6670c5010", size = 2807149, upload-time = "2025-10-19T00:42:20.964Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d4/3d83a05a21e7d2ed2b9e6daf489999c29934b005de9190272b8a2e3735d0/cytoolz-1.1.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0ec96b3d537cdf47d4e76ded199f7440715f4c71029b45445cff92c1248808c2", size = 3111608, upload-time = "2025-10-19T00:42:22.684Z" }, + { url = "https://files.pythonhosted.org/packages/51/88/96f68354c3d4af68de41f0db4fe41a23b96a50a4a416636cea325490cfeb/cytoolz-1.1.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:208e2f2ef90a32b0acbff3303d90d89b13570a228d491d2e622a7883a3c68148", size = 3179373, upload-time = "2025-10-19T00:42:24.395Z" }, + { url = "https://files.pythonhosted.org/packages/ce/50/ed87a5cd8e6f27ffbb64c39e9730e18ec66c37631db2888ae711909f10c9/cytoolz-1.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d416a81bb0bd517558668e49d30a7475b5445f9bbafaab7dcf066f1e9adba36", size = 3003120, upload-time = "2025-10-19T00:42:26.18Z" }, + { url = "https://files.pythonhosted.org/packages/d3/a7/acde155b050d6eaa8e9c7845c98fc5fb28501568e78e83ebbf44f8855274/cytoolz-1.1.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f32e94c91ffe49af04835ee713ebd8e005c85ebe83e7e1fdcc00f27164c2d636", size = 2703225, upload-time = "2025-10-19T00:42:27.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b6/9d518597c5bdea626b61101e8d2ff94124787a42259dafd9f5fc396f346a/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15d0c6405efc040499c46df44056a5c382f551a7624a41cf3e4c84a96b988a15", size = 2956033, upload-time = "2025-10-19T00:42:29.993Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/93e5f860926165538c85e1c5e1670ad3424f158df810f8ccd269da652138/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:bf069c5381d757debae891401b88b3a346ba3a28ca45ba9251103b282463fad8", size = 2862950, upload-time = "2025-10-19T00:42:31.803Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/99d6af00487bedc27597b54c9fcbfd5c833a69c6b7a9b9f0fff777bfc7aa/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d5cf15892e63411ec1bd67deff0e84317d974e6ab2cdfefdd4a7cea2989df66", size = 2861757, upload-time = "2025-10-19T00:42:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/71/ca/adfa1fb7949478135a37755cb8e88c20cd6b75c22a05f1128f05f3ab2c60/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3e3872c21170f8341656f8692f8939e8800dcee6549ad2474d4c817bdefd62cd", size = 2979049, upload-time = "2025-10-19T00:42:35.377Z" }, + { url = "https://files.pythonhosted.org/packages/70/4c/7bf47a03a4497d500bc73d4204e2d907771a017fa4457741b2a1d7c09319/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b9ddeff8e8fd65eb1fcefa61018100b2b627e759ea6ad275d2e2a93ffac147bf", size = 2699492, upload-time = "2025-10-19T00:42:37.133Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e7/3d034b0e4817314f07aa465d5864e9b8df9d25cb260a53dd84583e491558/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:02feeeda93e1fa3b33414eb57c2b0aefd1db8f558dd33fdfcce664a0f86056e4", size = 2995646, upload-time = "2025-10-19T00:42:38.912Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/be357181c71648d9fe1d1ce91cd42c63457dcf3c158e144416fd51dced83/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d08154ad45349162b6c37f12d5d1b2e6eef338e657b85e1621e4e6a4a69d64cb", size = 2919481, upload-time = "2025-10-19T00:42:40.85Z" }, + { url = "https://files.pythonhosted.org/packages/62/d5/bf5434fde726c4f80cb99912b2d8e0afa1587557e2a2d7e0315eb942f2de/cytoolz-1.1.0-cp313-cp313t-win32.whl", hash = "sha256:10ae4718a056948d73ca3e1bb9ab1f95f897ec1e362f829b9d37cc29ab566c60", size = 951595, upload-time = "2025-10-19T00:42:42.877Z" }, + { url = "https://files.pythonhosted.org/packages/64/29/39c161e9204a9715321ddea698cbd0abc317e78522c7c642363c20589e71/cytoolz-1.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:1bb77bc6197e5cb19784b6a42bb0f8427e81737a630d9d7dda62ed31733f9e6c", size = 1004445, upload-time = "2025-10-19T00:42:44.855Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5a/7cbff5e9a689f558cb0bdf277f9562b2ac51acf7cd15e055b8c3efb0e1ef/cytoolz-1.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:563dda652c6ff52d215704fbe6b491879b78d7bbbb3a9524ec8e763483cb459f", size = 926207, upload-time = "2025-10-19T00:42:46.456Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e8/297a85ba700f437c01eba962428e6ab4572f6c3e68e8ff442ce5c9d3a496/cytoolz-1.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d542cee7c7882d2a914a33dec4d3600416fb336734df979473249d4c53d207a1", size = 980613, upload-time = "2025-10-19T00:42:47.988Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d7/2b02c9d18e9cc263a0e22690f78080809f1eafe72f26b29ccc115d3bf5c8/cytoolz-1.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31922849b701b0f24bb62e56eb2488dcd3aa6ae3057694bd6b3b7c4c2bc27c2f", size = 990476, upload-time = "2025-10-19T00:42:49.653Z" }, + { url = "https://files.pythonhosted.org/packages/89/26/b6b159d2929310fca0eff8a4989cd4b1ecbdf7c46fdff46c7a20fcae55c8/cytoolz-1.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e68308d32afd31943314735c1335e4ab5696110e96b405f6bdb8f2a8dc771a16", size = 992712, upload-time = "2025-10-19T00:42:51.306Z" }, + { url = "https://files.pythonhosted.org/packages/42/a0/f7c572aa151ed466b0fce4a327c3cc916d3ef3c82e341be59ea4b9bee9e4/cytoolz-1.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fc4bb48b3b866e1867f7c6411a4229e5b44be3989060663713e10efc24c9bd5f", size = 1322596, upload-time = "2025-10-19T00:42:52.978Z" }, + { url = "https://files.pythonhosted.org/packages/72/7c/a55d035e20b77b6725e85c8f1a418b3a4c23967288b8b0c2d1a40f158cbe/cytoolz-1.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:456f77207d1445025d7ef262b8370a05492dcb1490cb428b0f3bf1bd744a89b0", size = 992825, upload-time = "2025-10-19T00:42:55.026Z" }, + { url = "https://files.pythonhosted.org/packages/03/af/39d2d3db322136e12e9336a1f13bab51eab88b386bfb11f91d3faff8ba34/cytoolz-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:174ebc71ebb20a9baeffce6ee07ee2cd913754325c93f99d767380d8317930f7", size = 990525, upload-time = "2025-10-19T00:42:56.666Z" }, + { url = "https://files.pythonhosted.org/packages/a6/bd/65d7a869d307f9b10ad45c2c1cbb40b81a8d0ed1138fa17fd904f5c83298/cytoolz-1.1.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8b3604fef602bcd53415055a4f68468339192fd17be39e687ae24f476d23d56e", size = 2672409, upload-time = "2025-10-19T00:42:58.81Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fb/74dfd844bfd67e810bd36e8e3903a143035447245828e7fcd7c81351d775/cytoolz-1.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3604b959a01f64c366e7d10ec7634d5f5cfe10301e27a8f090f6eb3b2a628a18", size = 2808477, upload-time = "2025-10-19T00:43:00.577Z" }, + { url = "https://files.pythonhosted.org/packages/d6/1f/587686c43e31c19241ec317da66438d093523921ea7749bbc65558a30df9/cytoolz-1.1.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6db2127a3c1bc2f59f08010d2ae53a760771a9de2f67423ad8d400e9ba4276e8", size = 2636881, upload-time = "2025-10-19T00:43:02.24Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6d/90468cd34f77cb38a11af52c4dc6199efcc97a486395a21bef72e9b7602e/cytoolz-1.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56584745ac647993a016a21bc76399113b7595e312f8d0a1b140c9fcf9b58a27", size = 2937315, upload-time = "2025-10-19T00:43:03.954Z" }, + { url = "https://files.pythonhosted.org/packages/d9/50/7b92cd78c613b92e3509e6291d3fb7e0d72ebda999a8df806a96c40ca9ab/cytoolz-1.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db2c4c3a7f7bd7e03bb1a236a125c8feb86c75802f4ecda6ecfaf946610b2930", size = 2959988, upload-time = "2025-10-19T00:43:05.758Z" }, + { url = "https://files.pythonhosted.org/packages/44/d5/34b5a28a8d9bb329f984b4c2259407ca3f501d1abeb01bacea07937d85d1/cytoolz-1.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48cb8a692111a285d2b9acd16d185428176bfbffa8a7c274308525fccd01dd42", size = 2795116, upload-time = "2025-10-19T00:43:07.411Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d9/5dd829e33273ec03bdc3c812e6c3281987ae2c5c91645582f6c331544a64/cytoolz-1.1.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d2f344ba5eb17dcf38ee37fdde726f69053f54927db8f8a1bed6ac61e5b1890d", size = 2535390, upload-time = "2025-10-19T00:43:09.104Z" }, + { url = "https://files.pythonhosted.org/packages/87/1f/7f9c58068a8eec2183110df051bc6b69dd621143f84473eeb6dc1b32905a/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abf76b1c1abd031f098f293b6d90ee08bdaa45f8b5678430e331d991b82684b1", size = 2704834, upload-time = "2025-10-19T00:43:10.942Z" }, + { url = "https://files.pythonhosted.org/packages/d2/90/667def5665333575d01a65fe3ec0ca31b897895f6e3bc1a42d6ea3659369/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ddf9a38a5b686091265ff45b53d142e44a538cd6c2e70610d3bc6be094219032", size = 2658441, upload-time = "2025-10-19T00:43:12.655Z" }, + { url = "https://files.pythonhosted.org/packages/23/79/6615f9a14960bd29ac98b823777b6589357833f65cf1a11b5abc1587c120/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:946786755274f07bb2be0400f28adb31d7d85a7c7001873c0a8e24a503428fb3", size = 2654766, upload-time = "2025-10-19T00:43:14.325Z" }, + { url = "https://files.pythonhosted.org/packages/b0/99/be59c6e0ae02153ef10ae1ff0f380fb19d973c651b50cf829a731f6c9e79/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d5b8f78b9fed79cf185ad4ddec099abeef45951bdcb416c5835ba05f0a1242c7", size = 2827649, upload-time = "2025-10-19T00:43:16.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/b7/854ddcf9f9618844108677c20d48f4611b5c636956adea0f0e85e027608f/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fccde6efefdbc02e676ccb352a2ccc8a8e929f59a1c6d3d60bb78e923a49ca44", size = 2533456, upload-time = "2025-10-19T00:43:17.764Z" }, + { url = "https://files.pythonhosted.org/packages/45/66/bfe6fbb2bdcf03c8377c8c2f542576e15f3340c905a09d78a6cb3badd39a/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:717b7775313da5f51b0fbf50d865aa9c39cb241bd4cb605df3cf2246d6567397", size = 2826455, upload-time = "2025-10-19T00:43:19.561Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0c/cce4047bd927e95f59e73319c02c9bc86bd3d76392e0eb9e41a1147a479c/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5158744a09d0e0e4a4f82225e3a3c4ebf38f9ae74467aaa905467270e52f2794", size = 2714897, upload-time = "2025-10-19T00:43:21.291Z" }, + { url = "https://files.pythonhosted.org/packages/ac/9a/061323bb289b565802bad14fb7ab59fcd8713105df142bcf4dd9ff64f8ac/cytoolz-1.1.0-cp314-cp314-win32.whl", hash = "sha256:1ed534bdbbf063b2bb28fca7d0f6723a3e5a72b086e7c7fe6d74ae8c3e4d00e2", size = 901490, upload-time = "2025-10-19T00:43:22.895Z" }, + { url = "https://files.pythonhosted.org/packages/a3/20/1f3a733d710d2a25d6f10b463bef55ada52fe6392a5d233c8d770191f48a/cytoolz-1.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:472c1c9a085f5ad973ec0ad7f0b9ba0969faea6f96c9e397f6293d386f3a25ec", size = 946730, upload-time = "2025-10-19T00:43:24.838Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/2d657db4a5d1c10a152061800f812caba9ef20d7bd2406f51a5fd800c180/cytoolz-1.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:a7ad7ca3386fa86bd301be3fa36e7f0acb024f412f665937955acfc8eb42deff", size = 905722, upload-time = "2025-10-19T00:43:26.439Z" }, + { url = "https://files.pythonhosted.org/packages/19/97/b4a8c76796a9a8b9bc90c7992840fa1589a1af8e0426562dea4ce9b384a7/cytoolz-1.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:64b63ed4b71b1ba813300ad0f06b8aff19a12cf51116e0e4f1ed837cea4debcf", size = 1372606, upload-time = "2025-10-19T00:43:28.491Z" }, + { url = "https://files.pythonhosted.org/packages/08/d4/a1bb1a32b454a2d650db8374ff3bf875ba0fc1c36e6446ec02a83b9140a1/cytoolz-1.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a60ba6f2ed9eb0003a737e1ee1e9fa2258e749da6477946008d4324efa25149f", size = 1012189, upload-time = "2025-10-19T00:43:30.177Z" }, + { url = "https://files.pythonhosted.org/packages/21/4b/2f5cbbd81588918ee7dd70cffb66731608f578a9b72166aafa991071af7d/cytoolz-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1aa58e2434d732241f7f051e6f17657e969a89971025e24578b5cbc6f1346485", size = 1020624, upload-time = "2025-10-19T00:43:31.712Z" }, + { url = "https://files.pythonhosted.org/packages/f5/99/c4954dd86cd593cd776a038b36795a259b8b5c12cbab6363edf5f6d9c909/cytoolz-1.1.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6965af3fc7214645970e312deb9bd35a213a1eaabcfef4f39115e60bf2f76867", size = 2917016, upload-time = "2025-10-19T00:43:33.531Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/f1f70a17e272b433232bc8a27df97e46b202d6cc07e3b0d63f7f41ba0f2d/cytoolz-1.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddd2863f321d67527d3b67a93000a378ad6f967056f68c06467fe011278a6d0e", size = 3107634, upload-time = "2025-10-19T00:43:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/8f/bd/c3226a57474b4aef1f90040510cba30d0decd3515fed48dc229b37c2f898/cytoolz-1.1.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4e6b428e9eb5126053c2ae0efa62512ff4b38ed3951f4d0888ca7005d63e56f5", size = 2806221, upload-time = "2025-10-19T00:43:37.707Z" }, + { url = "https://files.pythonhosted.org/packages/c3/47/2f7bfe4aaa1e07dc9828bea228ed744faf73b26aee0c1bdf3b5520bf1909/cytoolz-1.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d758e5ef311d2671e0ae8c214c52e44617cf1e58bef8f022b547b9802a5a7f30", size = 3107671, upload-time = "2025-10-19T00:43:39.401Z" }, + { url = "https://files.pythonhosted.org/packages/4d/12/6ff3b04fbd1369d0fcd5f8b5910ba6e427e33bf113754c4c35ec3f747924/cytoolz-1.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a95416eca473e6c1179b48d86adcf528b59c63ce78f4cb9934f2e413afa9b56b", size = 3176350, upload-time = "2025-10-19T00:43:41.148Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/6691d986b728e77b5d2872743ebcd962d37a2d0f7e9ad95a81b284fbf905/cytoolz-1.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36c8ede93525cf11e2cc787b7156e5cecd7340193ef800b816a16f1404a8dc6d", size = 3001173, upload-time = "2025-10-19T00:43:42.923Z" }, + { url = "https://files.pythonhosted.org/packages/7a/cb/f59d83a5058e1198db5a1f04e4a124c94d60390e4fa89b6d2e38ee8288a0/cytoolz-1.1.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c949755b6d8a649c5fbc888bc30915926f1b09fe42fea9f289e297c2f6ddd3", size = 2701374, upload-time = "2025-10-19T00:43:44.716Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f0/1ae6d28df503b0bdae094879da2072b8ba13db5919cd3798918761578411/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1b6d37545816905a76d9ed59fa4e332f929e879f062a39ea0f6f620405cdc27", size = 2953081, upload-time = "2025-10-19T00:43:47.103Z" }, + { url = "https://files.pythonhosted.org/packages/f4/06/d86fe811c6222dc32d3e08f5d88d2be598a6055b4d0590e7c1428d55c386/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:05332112d4087904842b36954cd1d3fc0e463a2f4a7ef9477bd241427c593c3b", size = 2862228, upload-time = "2025-10-19T00:43:49.353Z" }, + { url = "https://files.pythonhosted.org/packages/ae/32/978ef6f42623be44a0a03ae9de875ab54aa26c7e38c5c4cd505460b0927d/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:31538ca2fad2d688cbd962ccc3f1da847329e2258a52940f10a2ac0719e526be", size = 2861971, upload-time = "2025-10-19T00:43:51.028Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f7/74c69497e756b752b359925d1feef68b91df024a4124a823740f675dacd3/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:747562aa70abf219ea16f07d50ac0157db856d447f7f498f592e097cbc77df0b", size = 2975304, upload-time = "2025-10-19T00:43:52.99Z" }, + { url = "https://files.pythonhosted.org/packages/5b/2b/3ce0e6889a6491f3418ad4d84ae407b8456b02169a5a1f87990dbba7433b/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:3dc15c48b20c0f467e15e341e102896c8422dccf8efc6322def5c1b02f074629", size = 2697371, upload-time = "2025-10-19T00:43:55.312Z" }, + { url = "https://files.pythonhosted.org/packages/15/87/c616577f0891d97860643c845f7221e95240aa589586de727e28a5eb6e52/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3c03137ee6103ba92d5d6ad6a510e86fded69cd67050bd8a1843f15283be17ac", size = 2992436, upload-time = "2025-10-19T00:43:57.253Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9f/490c81bffb3428ab1fa114051fbb5ba18aaa2e2fe4da5bf4170ca524e6b3/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be8e298d88f88bd172b59912240558be3b7a04959375646e7fd4996401452941", size = 2917612, upload-time = "2025-10-19T00:43:59.423Z" }, + { url = "https://files.pythonhosted.org/packages/66/35/0fec2769660ca6472bbf3317ab634675827bb706d193e3240aaf20eab961/cytoolz-1.1.0-cp314-cp314t-win32.whl", hash = "sha256:3d407140f5604a89578285d4aac7b18b8eafa055cf776e781aabb89c48738fad", size = 960842, upload-time = "2025-10-19T00:44:01.143Z" }, + { url = "https://files.pythonhosted.org/packages/46/b4/b7ce3d3cd20337becfec978ecfa6d0ef64884d0cf32d44edfed8700914b9/cytoolz-1.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:56e5afb69eb6e1b3ffc34716ee5f92ffbdb5cb003b3a5ca4d4b0fe700e217162", size = 1020835, upload-time = "2025-10-19T00:44:03.246Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1f/0498009aa563a9c5d04f520aadc6e1c0942434d089d0b2f51ea986470f55/cytoolz-1.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:27b19b4a286b3ff52040efa42dbe403730aebe5fdfd2def704eb285e2125c63e", size = 927963, upload-time = "2025-10-19T00:44:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/84/32/0522207170294cf691112a93c70a8ef942f60fa9ff8e793b63b1f09cedc0/cytoolz-1.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f32e93a55681d782fc6af939f6df36509d65122423cbc930be39b141064adff8", size = 922014, upload-time = "2025-10-19T00:44:44.911Z" }, + { url = "https://files.pythonhosted.org/packages/4c/49/9be2d24adaa18fa307ff14e3e43f02b2ae4b69c4ce51cee6889eb2114990/cytoolz-1.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5d9bc596751cbda8073e65be02ca11706f00029768fbbbc81e11a8c290bb41aa", size = 918134, upload-time = "2025-10-19T00:44:47.122Z" }, + { url = "https://files.pythonhosted.org/packages/5c/b3/6a76c3b94c6c87c72ea822e7e67405be6b649c2e37778eeac7c0c0c69de8/cytoolz-1.1.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b16660d01c3931951fab49db422c627897c38c1a1f0393a97582004019a4887", size = 981970, upload-time = "2025-10-19T00:44:48.906Z" }, + { url = "https://files.pythonhosted.org/packages/f6/8a/606e4c7ed14aa6a86aee6ca84a2cb804754dc6c4905b8f94e09e49f1ce60/cytoolz-1.1.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b7de5718e2113d4efccea3f06055758cdbc17388ecc3341ba4d1d812837d7c1a", size = 978877, upload-time = "2025-10-19T00:44:50.819Z" }, + { url = "https://files.pythonhosted.org/packages/97/ec/ad474dcb1f6c1ebfdda3c2ad2edbb1af122a0e79c9ff2cb901ffb5f59662/cytoolz-1.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a12a2a1a6bc44099491c05a12039efa08cc33a3d0f8c7b0566185e085e139283", size = 964279, upload-time = "2025-10-19T00:44:52.476Z" }, + { url = "https://files.pythonhosted.org/packages/68/8c/d245fd416c69d27d51f14d5ad62acc4ee5971088ee31c40ffe1cc109af68/cytoolz-1.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:047defa7f5f9a32f82373dbc3957289562e8a3fa58ae02ec8e4dca4f43a33a21", size = 916630, upload-time = "2025-10-19T00:44:54.059Z" }, +] + +[[package]] +name = "eth-abi" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-typing" }, + { name = "eth-utils" }, + { name = "parsimonious" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/71/d9e1380bd77fd22f98b534699af564f189b56d539cc2b9dab908d4e4c242/eth_abi-5.2.0.tar.gz", hash = "sha256:178703fa98c07d8eecd5ae569e7e8d159e493ebb6eeb534a8fe973fbc4e40ef0", size = 49797, upload-time = "2025-01-14T16:29:34.629Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/b4/2f3982c4cbcbf5eeb6aec62df1533c0e63c653b3021ff338d44944405676/eth_abi-5.2.0-py3-none-any.whl", hash = "sha256:17abe47560ad753f18054f5b3089fcb588f3e3a092136a416b6c1502cb7e8877", size = 28511, upload-time = "2025-01-14T16:29:31.862Z" }, +] + +[[package]] +name = "eth-hash" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/38/577b7bc9380ef9dff0f1dffefe0c9a1ded2385e7a06c306fd95afb6f9451/eth_hash-0.7.1.tar.gz", hash = "sha256:d2411a403a0b0a62e8247b4117932d900ffb4c8c64b15f92620547ca5ce46be5", size = 12227, upload-time = "2025-01-13T21:29:21.765Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/db/f8775490669d28aca24871c67dd56b3e72105cb3bcae9a4ec65dd70859b3/eth_hash-0.7.1-py3-none-any.whl", hash = "sha256:0fb1add2adf99ef28883fd6228eb447ef519ea72933535ad1a0b28c6f65f868a", size = 8028, upload-time = "2025-01-13T21:29:19.365Z" }, +] + +[[package]] +name = "eth-keys" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-typing" }, + { name = "eth-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/11/1ed831c50bd74f57829aa06e58bd82a809c37e070ee501c953b9ac1f1552/eth_keys-0.7.0.tar.gz", hash = "sha256:79d24fd876201df67741de3e3fefb3f4dbcbb6ace66e47e6fe662851a4547814", size = 30166, upload-time = "2025-04-07T17:40:21.697Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/25/0ae00f2b0095e559d61ad3dc32171bd5a29dfd95ab04b4edd641f7c75f72/eth_keys-0.7.0-py3-none-any.whl", hash = "sha256:b0cdda8ffe8e5ba69c7c5ca33f153828edcace844f67aabd4542d7de38b159cf", size = 20656, upload-time = "2025-04-07T17:40:20.441Z" }, +] + +[[package]] +name = "eth-typing" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/54/62aa24b9cc708f06316167ee71c362779c8ed21fc8234a5cd94a8f53b623/eth_typing-5.2.1.tar.gz", hash = "sha256:7557300dbf02a93c70fa44af352b5c4a58f94e997a0fd6797fb7d1c29d9538ee", size = 21806, upload-time = "2025-04-14T20:39:28.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/72/c370bbe4c53da7bf998d3523f5a0f38867654923a82192df88d0705013d3/eth_typing-5.2.1-py3-none-any.whl", hash = "sha256:b0c2812ff978267563b80e9d701f487dd926f1d376d674f3b535cfe28b665d3d", size = 19163, upload-time = "2025-04-14T20:39:26.571Z" }, +] + +[[package]] +name = "eth-utils" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cytoolz", marker = "implementation_name == 'cpython'" }, + { name = "eth-hash" }, + { name = "eth-typing" }, + { name = "pydantic" }, + { name = "toolz", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/e1/ee3a8728227c3558853e63ff35bd4c449abdf5022a19601369400deacd39/eth_utils-5.3.1.tar.gz", hash = "sha256:c94e2d2abd024a9a42023b4ddc1c645814ff3d6a737b33d5cfd890ebf159c2d1", size = 123506, upload-time = "2025-08-27T16:37:17.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/4d/257cdc01ada430b8e84b9f2385c2553f33218f5b47da9adf0a616308d4b7/eth_utils-5.3.1-py3-none-any.whl", hash = "sha256:1f5476d8f29588d25b8ae4987e1ffdfae6d4c09026e476c4aad13b32dda3ead0", size = 102529, upload-time = "2025-08-27T16:37:15.449Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "flask" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, +] + +[[package]] +name = "grpcio" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/a8/690a085b4d1fe066130de97a87de32c45062cf2ecd218df9675add895550/grpcio-1.78.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5", size = 5946986, upload-time = "2026-02-06T09:54:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1b/e5213c5c0ced9d2d92778d30529ad5bb2dcfb6c48c4e2d01b1f302d33d64/grpcio-1.78.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2", size = 11816533, upload-time = "2026-02-06T09:54:37.04Z" }, + { url = "https://files.pythonhosted.org/packages/18/37/1ba32dccf0a324cc5ace744c44331e300b000a924bf14840f948c559ede7/grpcio-1.78.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d", size = 6519964, upload-time = "2026-02-06T09:54:40.268Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f5/c0e178721b818072f2e8b6fde13faaba942406c634009caf065121ce246b/grpcio-1.78.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb", size = 7198058, upload-time = "2026-02-06T09:54:42.389Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b2/40d43c91ae9cd667edc960135f9f08e58faa1576dc95af29f66ec912985f/grpcio-1.78.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7", size = 6727212, upload-time = "2026-02-06T09:54:44.91Z" }, + { url = "https://files.pythonhosted.org/packages/ed/88/9da42eed498f0efcfcd9156e48ae63c0cde3bea398a16c99fb5198c885b6/grpcio-1.78.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec", size = 7300845, upload-time = "2026-02-06T09:54:47.562Z" }, + { url = "https://files.pythonhosted.org/packages/23/3f/1c66b7b1b19a8828890e37868411a6e6925df5a9030bfa87ab318f34095d/grpcio-1.78.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a", size = 8284605, upload-time = "2026-02-06T09:54:50.475Z" }, + { url = "https://files.pythonhosted.org/packages/94/c4/ca1bd87394f7b033e88525384b4d1e269e8424ab441ea2fba1a0c5b50986/grpcio-1.78.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813", size = 7726672, upload-time = "2026-02-06T09:54:53.11Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/f16e487d4cc65ccaf670f6ebdd1a17566b965c74fc3d93999d3b2821e052/grpcio-1.78.0-cp310-cp310-win32.whl", hash = "sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de", size = 4076715, upload-time = "2026-02-06T09:54:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/2a/32/4ce60d94e242725fd3bcc5673c04502c82a8e87b21ea411a63992dc39f8f/grpcio-1.78.0-cp310-cp310-win_amd64.whl", hash = "sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf", size = 4799157, upload-time = "2026-02-06T09:54:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/86/c7/d0b780a29b0837bf4ca9580904dfb275c1fc321ded7897d620af7047ec57/grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", size = 5951525, upload-time = "2026-02-06T09:55:01.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/96920bf2ee61df85a9503cb6f733fe711c0ff321a5a697d791b075673281/grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", size = 11830418, upload-time = "2026-02-06T09:55:04.462Z" }, + { url = "https://files.pythonhosted.org/packages/83/0c/7c1528f098aeb75a97de2bae18c530f56959fb7ad6c882db45d9884d6edc/grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", size = 6524477, upload-time = "2026-02-06T09:55:07.111Z" }, + { url = "https://files.pythonhosted.org/packages/8d/52/e7c1f3688f949058e19a011c4e0dec973da3d0ae5e033909677f967ae1f4/grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", size = 7198266, upload-time = "2026-02-06T09:55:10.016Z" }, + { url = "https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", size = 6730552, upload-time = "2026-02-06T09:55:12.207Z" }, + { url = "https://files.pythonhosted.org/packages/bd/98/b8ee0158199250220734f620b12e4a345955ac7329cfd908d0bf0fda77f0/grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", size = 7304296, upload-time = "2026-02-06T09:55:15.044Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0f/7b72762e0d8840b58032a56fdbd02b78fc645b9fa993d71abf04edbc54f4/grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", size = 8288298, upload-time = "2026-02-06T09:55:17.276Z" }, + { url = "https://files.pythonhosted.org/packages/24/ae/ae4ce56bc5bb5caa3a486d60f5f6083ac3469228faa734362487176c15c5/grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", size = 7730953, upload-time = "2026-02-06T09:55:19.545Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6e/8052e3a28eb6a820c372b2eb4b5e32d195c661e137d3eca94d534a4cfd8a/grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", size = 4076503, upload-time = "2026-02-06T09:55:21.521Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/f22c98c5265dfad327251fa2f840b591b1df5f5e15d88b19c18c86965b27/grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", size = 4799767, upload-time = "2026-02-06T09:55:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, + { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, + { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, + { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, + { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, + { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, + { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, + { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" }, + { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" }, + { url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591, upload-time = "2026-02-06T09:56:20.758Z" }, + { url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685, upload-time = "2026-02-06T09:56:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803, upload-time = "2026-02-06T09:56:27.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206, upload-time = "2026-02-06T09:56:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826, upload-time = "2026-02-06T09:56:32.305Z" }, + { url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897, upload-time = "2026-02-06T09:56:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404, upload-time = "2026-02-06T09:56:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837, upload-time = "2026-02-06T09:56:40.173Z" }, + { url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439, upload-time = "2026-02-06T09:56:43.258Z" }, + { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" }, +] + +[[package]] +name = "grpcio-tools" +version = "1.78.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "protobuf" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/d1/cbefe328653f746fd319c4377836a25ba64226e41c6a1d7d5cdbc87a459f/grpcio_tools-1.78.0.tar.gz", hash = "sha256:4b0dd86560274316e155d925158276f8564508193088bc43e20d3f5dff956b2b", size = 5393026, upload-time = "2026-02-06T09:59:59.53Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/70/2118a814a62ab205c905d221064bc09021db83fceeb84764d35c00f0f633/grpcio_tools-1.78.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:ea64e38d1caa2b8468b08cb193f5a091d169b6dbfe1c7dac37d746651ab9d84e", size = 2545568, upload-time = "2026-02-06T09:57:30.308Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a9/68134839dd1a00f964185ead103646d6dd6a396b92ed264eaf521431b793/grpcio_tools-1.78.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:4003fcd5cbb5d578b06176fd45883a72a8f9203152149b7c680ce28653ad9e3a", size = 5708704, upload-time = "2026-02-06T09:57:33.512Z" }, + { url = "https://files.pythonhosted.org/packages/36/1b/b6135aa9534e22051c53e5b9c0853d18024a41c50aaff464b7b47c1ed379/grpcio_tools-1.78.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe6b0081775394c61ec633c9ff5dbc18337100eabb2e946b5c83967fe43b2748", size = 2591905, upload-time = "2026-02-06T09:57:35.338Z" }, + { url = "https://files.pythonhosted.org/packages/41/2b/6380df1390d62b1d18ae18d4d790115abf4997fa29498aa50ba644ecb9d8/grpcio_tools-1.78.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:7e989ad2cd93db52d7f1a643ecaa156ac55bf0484f1007b485979ce8aef62022", size = 2905271, upload-time = "2026-02-06T09:57:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/3a/07/9b369f37c8f4956b68778c044d57390a8f0f3b1cca590018809e75a4fce2/grpcio_tools-1.78.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b874991797e96c41a37e563236c3317ed41b915eff25b292b202d6277d30da85", size = 2656234, upload-time = "2026-02-06T09:57:41.157Z" }, + { url = "https://files.pythonhosted.org/packages/51/61/40eee40e7a54f775a0d4117536532713606b6b177fff5e327f33ad18746e/grpcio_tools-1.78.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:daa8c288b728228377aaf758925692fc6068939d9fa32f92ca13dedcbeb41f33", size = 3105770, upload-time = "2026-02-06T09:57:43.373Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ac/81ee4b728e70e8ba66a589f86469925ead02ed6f8973434e4a52e3576148/grpcio_tools-1.78.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:87e648759b06133199f4bc0c0053e3819f4ec3b900dc399e1097b6065db998b5", size = 3654896, upload-time = "2026-02-06T09:57:45.402Z" }, + { url = "https://files.pythonhosted.org/packages/be/b9/facb3430ee427c800bb1e39588c85685677ea649491d6e0874bd9f3a1c0e/grpcio_tools-1.78.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f3d3ced52bfe39eba3d24f5a8fab4e12d071959384861b41f0c52ca5399d6920", size = 3322529, upload-time = "2026-02-06T09:57:47.292Z" }, + { url = "https://files.pythonhosted.org/packages/c7/de/d7a011df9abfed8c30f0d2077b0562a6e3edc57cb3e5514718e2a81f370a/grpcio_tools-1.78.0-cp310-cp310-win32.whl", hash = "sha256:4bb6ed690d417b821808796221bde079377dff98fdc850ac157ad2f26cda7a36", size = 993518, upload-time = "2026-02-06T09:57:48.836Z" }, + { url = "https://files.pythonhosted.org/packages/c8/5e/f7f60c3ae2281c6b438c3a8455f4a5d5d2e677cf20207864cbee3763da22/grpcio_tools-1.78.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c676d8342fd53bd85a5d5f0d070cd785f93bc040510014708ede6fcb32fada1", size = 1158505, upload-time = "2026-02-06T09:57:50.633Z" }, + { url = "https://files.pythonhosted.org/packages/75/78/280184d19242ed6762bf453c47a70b869b3c5c72a24dc5bf2bf43909faa3/grpcio_tools-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:6a8b8b7b49f319d29dbcf507f62984fa382d1d10437d75c3f26db5f09c4ac0af", size = 2545904, upload-time = "2026-02-06T09:57:52.769Z" }, + { url = "https://files.pythonhosted.org/packages/5b/51/3c46dea5113f68fe879961cae62d34bb7a3c308a774301b45d614952ee98/grpcio_tools-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:d62cf3b68372b0c6d722a6165db41b976869811abeabc19c8522182978d8db10", size = 5709078, upload-time = "2026-02-06T09:57:56.389Z" }, + { url = "https://files.pythonhosted.org/packages/e0/2c/dc1ae9ec53182c96d56dfcbf3bcd3e55a8952ad508b188c75bf5fc8993d4/grpcio_tools-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fa9056742efeaf89d5fe14198af71e5cbc4fbf155d547b89507e19d6025906c6", size = 2591744, upload-time = "2026-02-06T09:57:58.341Z" }, + { url = "https://files.pythonhosted.org/packages/04/63/9b53fc9a9151dd24386785171a4191ee7cb5afb4d983b6a6a87408f41b28/grpcio_tools-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e3191af125dcb705aa6bc3856ba81ba99b94121c1b6ebee152e66ea084672831", size = 2905113, upload-time = "2026-02-06T09:58:00.38Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/0ad8d789f3a2a00893131c140865605fa91671a6e6fcf9da659e1fabba10/grpcio_tools-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:283239ddbb67ae83fac111c61b25d8527a1dbd355b377cbc8383b79f1329944d", size = 2656436, upload-time = "2026-02-06T09:58:03.038Z" }, + { url = "https://files.pythonhosted.org/packages/09/4d/580f47ce2fc61b093ade747b378595f51b4f59972dd39949f7444b464a03/grpcio_tools-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac977508c0db15301ef36d6c79769ec1a6cc4e3bc75735afca7fe7e360cead3a", size = 3106128, upload-time = "2026-02-06T09:58:05.064Z" }, + { url = "https://files.pythonhosted.org/packages/c9/29/d83b2d89f8d10e438bad36b1eb29356510fb97e81e6a608b22ae1890e8e6/grpcio_tools-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4ff605e25652a0bd13aa8a73a09bc48669c68170902f5d2bf1468a57d5e78771", size = 3654953, upload-time = "2026-02-06T09:58:07.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/71/917ce85633311e54fefd7e6eb1224fb780ef317a4d092766f5630c3fc419/grpcio_tools-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0197d7b561c79be78ab93d0fe2836c8def470683df594bae3ac89dd8e5c821b2", size = 3322630, upload-time = "2026-02-06T09:58:10.305Z" }, + { url = "https://files.pythonhosted.org/packages/b2/55/3fbf6b26ab46fc79e1e6f7f4e0993cf540263dad639290299fad374a0829/grpcio_tools-1.78.0-cp311-cp311-win32.whl", hash = "sha256:28f71f591f7f39555863ced84fcc209cbf4454e85ef957232f43271ee99af577", size = 993804, upload-time = "2026-02-06T09:58:13.698Z" }, + { url = "https://files.pythonhosted.org/packages/73/86/4affe006d9e1e9e1c6653d6aafe2f8b9188acb2b563cd8ed3a2c7c0e8aec/grpcio_tools-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:5a6de495dabf86a3b40b9a7492994e1232b077af9d63080811838b781abbe4e8", size = 1158566, upload-time = "2026-02-06T09:58:15.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ae/5b1fa5dd8d560a6925aa52de0de8731d319f121c276e35b9b2af7cc220a2/grpcio_tools-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:9eb122da57d4cad7d339fc75483116f0113af99e8d2c67f3ef9cae7501d806e4", size = 2546823, upload-time = "2026-02-06T09:58:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ed/d33ccf7fa701512efea7e7e23333b748848a123e9d3bbafde4e126784546/grpcio_tools-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d0c501b8249940b886420e6935045c44cb818fa6f265f4c2b97d5cff9cb5e796", size = 5706776, upload-time = "2026-02-06T09:58:20.944Z" }, + { url = "https://files.pythonhosted.org/packages/c6/69/4285583f40b37af28277fc6b867d636e3b10e1b6a7ebd29391a856e1279b/grpcio_tools-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:77e5aa2d2a7268d55b1b113f958264681ef1994c970f69d48db7d4683d040f57", size = 2593972, upload-time = "2026-02-06T09:58:23.29Z" }, + { url = "https://files.pythonhosted.org/packages/d7/eb/ecc1885bd6b3147f0a1b7dff5565cab72f01c8f8aa458f682a1c77a9fb08/grpcio_tools-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8e3c0b0e6ba5275322ba29a97bf890565a55f129f99a21b121145e9e93a22525", size = 2905531, upload-time = "2026-02-06T09:58:25.406Z" }, + { url = "https://files.pythonhosted.org/packages/ae/a9/511d0040ced66960ca10ba0f082d6b2d2ee6dd61837b1709636fdd8e23b4/grpcio_tools-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975d4cb48694e20ebd78e1643e5f1cd94cdb6a3d38e677a8e84ae43665aa4790", size = 2656909, upload-time = "2026-02-06T09:58:28.022Z" }, + { url = "https://files.pythonhosted.org/packages/06/a3/3d2c707e7dee8df842c96fbb24feb2747e506e39f4a81b661def7fed107c/grpcio_tools-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:553ff18c5d52807dedecf25045ae70bad7a3dbba0b27a9a3cdd9bcf0a1b7baec", size = 3109778, upload-time = "2026-02-06T09:58:30.091Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4b/646811ba241bf05da1f0dc6f25764f1c837f78f75b4485a4210c84b79eae/grpcio_tools-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8c7f5e4af5a84d2e96c862b1a65e958a538237e268d5f8203a3a784340975b51", size = 3658763, upload-time = "2026-02-06T09:58:32.875Z" }, + { url = "https://files.pythonhosted.org/packages/45/de/0a5ef3b3e79d1011375f5580dfee3a9c1ccb96c5f5d1c74c8cee777a2483/grpcio_tools-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:96183e2b44afc3f9a761e9d0f985c3b44e03e8bb98e626241a6cbfb3b6f7e88f", size = 3325116, upload-time = "2026-02-06T09:58:34.894Z" }, + { url = "https://files.pythonhosted.org/packages/95/d2/6391b241ad571bc3e71d63f957c0b1860f0c47932d03c7f300028880f9b8/grpcio_tools-1.78.0-cp312-cp312-win32.whl", hash = "sha256:2250e8424c565a88573f7dc10659a0b92802e68c2a1d57e41872c9b88ccea7a6", size = 993493, upload-time = "2026-02-06T09:58:37.242Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8f/7d0d3a39ecad76ccc136be28274daa660569b244fa7d7d0bbb24d68e5ece/grpcio_tools-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:217d1fa29de14d9c567d616ead7cb0fef33cde36010edff5a9390b00d52e5094", size = 1158423, upload-time = "2026-02-06T09:58:40.072Z" }, + { url = "https://files.pythonhosted.org/packages/53/ce/17311fb77530420e2f441e916b347515133e83d21cd6cc77be04ce093d5b/grpcio_tools-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:2d6de1cc23bdc1baafc23e201b1e48c617b8c1418b4d8e34cebf72141676e5fb", size = 2546284, upload-time = "2026-02-06T09:58:43.073Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d3/79e101483115f0e78223397daef71751b75eba7e92a32060c10aae11ca64/grpcio_tools-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2afeaad88040894c76656202ff832cb151bceb05c0e6907e539d129188b1e456", size = 5705653, upload-time = "2026-02-06T09:58:45.533Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a7/52fa3ccb39ceeee6adc010056eadfbca8198651c113e418dafebbdf2b306/grpcio_tools-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:33cc593735c93c03d63efe7a8ba25f3c66f16c52f0651910712490244facad72", size = 2592788, upload-time = "2026-02-06T09:58:48.918Z" }, + { url = "https://files.pythonhosted.org/packages/68/08/682ff6bb548225513d73dc9403742d8975439d7469c673bc534b9bbc83a7/grpcio_tools-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2921d7989c4d83b71f03130ab415fa4d66e6693b8b8a1fcbb7a1c67cff19b812", size = 2905157, upload-time = "2026-02-06T09:58:51.478Z" }, + { url = "https://files.pythonhosted.org/packages/b2/66/264f3836a96423b7018e5ada79d62576a6401f6da4e1f4975b18b2be1265/grpcio_tools-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6a0df438e82c804c7b95e3f311c97c2f876dcc36376488d5b736b7bcf5a9b45", size = 2656166, upload-time = "2026-02-06T09:58:54.117Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/f108276611522e03e98386b668cc7e575eff6952f2db9caa15b2a3b3e883/grpcio_tools-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e9c6070a9500798225191ef25d0055a15d2c01c9c8f2ee7b681fffa99c98c822", size = 3109110, upload-time = "2026-02-06T09:58:56.891Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c7/cf048dbcd64b3396b3c860a2ffbcc67a8f8c87e736aaa74c2e505a7eee4c/grpcio_tools-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:394e8b57d85370a62e5b0a4d64c96fcf7568345c345d8590c821814d227ecf1d", size = 3657863, upload-time = "2026-02-06T09:58:59.176Z" }, + { url = "https://files.pythonhosted.org/packages/b6/37/e2736912c8fda57e2e57a66ea5e0bc8eb9a5fb7ded00e866ad22d50afb08/grpcio_tools-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3ef700293ab375e111a2909d87434ed0a0b086adf0ce67a8d9cf12ea7765e63", size = 3324748, upload-time = "2026-02-06T09:59:01.242Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/726abc75bb5bfc2841e88ea05896e42f51ca7c30cb56da5c5b63058b3867/grpcio_tools-1.78.0-cp313-cp313-win32.whl", hash = "sha256:6993b960fec43a8d840ee5dc20247ef206c1a19587ea49fe5e6cc3d2a09c1585", size = 993074, upload-time = "2026-02-06T09:59:03.085Z" }, + { url = "https://files.pythonhosted.org/packages/c5/68/91b400bb360faf9b177ffb5540ec1c4d06ca923691ddf0f79e2c9683f4da/grpcio_tools-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:275ce3c2978842a8cf9dd88dce954e836e590cf7029649ad5d1145b779039ed5", size = 1158185, upload-time = "2026-02-06T09:59:05.036Z" }, + { url = "https://files.pythonhosted.org/packages/cf/5e/278f3831c8d56bae02e3acc570465648eccf0a6bbedcb1733789ac966803/grpcio_tools-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:8b080d0d072e6032708a3a91731b808074d7ab02ca8fb9847b6a011fdce64cd9", size = 2546270, upload-time = "2026-02-06T09:59:07.426Z" }, + { url = "https://files.pythonhosted.org/packages/a3/d9/68582f2952b914b60dddc18a2e3f9c6f09af9372b6f6120d6cf3ec7f8b4e/grpcio_tools-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8c0ad8f8f133145cd7008b49cb611a5c6a9d89ab276c28afa17050516e801f79", size = 5705731, upload-time = "2026-02-06T09:59:09.856Z" }, + { url = "https://files.pythonhosted.org/packages/70/68/feb0f9a48818ee1df1e8b644069379a1e6ef5447b9b347c24e96fd258e5d/grpcio_tools-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2f8ea092a7de74c6359335d36f0674d939a3c7e1a550f4c2c9e80e0226de8fe4", size = 2593896, upload-time = "2026-02-06T09:59:12.23Z" }, + { url = "https://files.pythonhosted.org/packages/1f/08/a430d8d06e1b8d33f3e48d3f0cc28236723af2f35e37bd5c8db05df6c3aa/grpcio_tools-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:da422985e0cac822b41822f43429c19ecb27c81ffe3126d0b74e77edec452608", size = 2905298, upload-time = "2026-02-06T09:59:14.458Z" }, + { url = "https://files.pythonhosted.org/packages/71/0a/348c36a3eae101ca0c090c9c3bc96f2179adf59ee0c9262d11cdc7bfe7db/grpcio_tools-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4fab1faa3fbcb246263e68da7a8177d73772283f9db063fb8008517480888d26", size = 2656186, upload-time = "2026-02-06T09:59:16.949Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3f/18219f331536fad4af6207ade04142292faa77b5cb4f4463787988963df8/grpcio_tools-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dd9c094f73f734becae3f20f27d4944d3cd8fb68db7338ee6c58e62fc5c3d99f", size = 3109859, upload-time = "2026-02-06T09:59:19.202Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d9/341ea20a44c8e5a3a18acc820b65014c2e3ea5b4f32a53d14864bcd236bc/grpcio_tools-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2ed51ce6b833068f6c580b73193fc2ec16468e6bc18354bc2f83a58721195a58", size = 3657915, upload-time = "2026-02-06T09:59:21.839Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f4/5978b0f91611a64371424c109dd0027b247e5b39260abad2eaee66b6aa37/grpcio_tools-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:05803a5cdafe77c8bdf36aa660ad7a6a1d9e49bc59ce45c1bade2a4698826599", size = 3324724, upload-time = "2026-02-06T09:59:24.402Z" }, + { url = "https://files.pythonhosted.org/packages/b2/80/96a324dba99cfbd20e291baf0b0ae719dbb62b76178c5ce6c788e7331cb1/grpcio_tools-1.78.0-cp314-cp314-win32.whl", hash = "sha256:f7c722e9ce6f11149ac5bddd5056e70aaccfd8168e74e9d34d8b8b588c3f5c7c", size = 1015505, upload-time = "2026-02-06T09:59:26.3Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d1/909e6a05bfd44d46327dc4b8a78beb2bae4fb245ffab2772e350081aaf7e/grpcio_tools-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d58ade518b546120ec8f0a8e006fc8076ae5df151250ebd7e82e9b5e152c229", size = 1190196, upload-time = "2026-02-06T09:59:28.359Z" }, +] + +[[package]] +name = "hiero-sdk-python" +source = { editable = "." } +dependencies = [ + { name = "cryptography" }, + { name = "eth-abi" }, + { name = "grpcio" }, + { name = "protobuf" }, + { name = "pycryptodome" }, + { name = "python-dotenv" }, + { name = "requests" }, +] + +[package.optional-dependencies] +eth = [ + { name = "eth-keys" }, + { name = "rlp" }, +] +tck = [ + { name = "flask" }, +] + +[package.dev-dependencies] +dev = [ + { name = "grpcio-tools" }, + { name = "hypothesis" }, + { name = "pytest" }, + { name = "pytest-cov" }, +] +lint = [ + { name = "mypy" }, + { name = "ruff" }, + { name = "typing-extensions" }, +] + +[package.metadata] +requires-dist = [ + { name = "cryptography", specifier = ">=44.0.1,<47" }, + { name = "eth-abi", specifier = ">=5.1.0,<6" }, + { name = "eth-keys", marker = "extra == 'eth'", specifier = ">=0.7.0,<0.8" }, + { name = "flask", marker = "extra == 'tck'", specifier = ">=3.0.0,<4" }, + { name = "grpcio", specifier = ">=1.76.0,<2" }, + { name = "protobuf", specifier = ">=4.21.12,<8" }, + { name = "pycryptodome", specifier = ">=3.18.0,<4" }, + { name = "python-dotenv", specifier = ">=1.2.1,<3" }, + { name = "requests", specifier = ">=2.31.0,<3" }, + { name = "rlp", marker = "extra == 'eth'", specifier = ">=4.0.1,<5" }, +] +provides-extras = ["eth", "tck"] + +[package.metadata.requires-dev] +dev = [ + { name = "grpcio-tools", specifier = ">=1.76.0,<2" }, + { name = "hypothesis", specifier = ">=6.137.2" }, + { name = "pytest", specifier = ">=8.3.4,<10" }, + { name = "pytest-cov", specifier = ">=7.0.0,<8" }, +] +lint = [ + { name = "mypy", specifier = ">=1.18.2,<2" }, + { name = "ruff", specifier = ">=0.14.9,<1" }, + { name = "typing-extensions", specifier = ">=4.15.0,<5" }, +] + +[[package]] +name = "hypothesis" +version = "6.151.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/58/41af0d539b3c95644d1e4e353cbd6ac9473e892ea21802546a8886b79078/hypothesis-6.151.11.tar.gz", hash = "sha256:f33dcb68b62c7b07c9ac49664989be898fa8ce57583f0dc080259a197c6c7ff1", size = 463779, upload-time = "2026-04-05T17:35:55.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/06/f49393eca84b87b17a67aaebf9f6251190ba1e9fe9f2236504049fc43fee/hypothesis-6.151.11-py3-none-any.whl", hash = "sha256:7ac05173206746cec8312f95164a30a4eb4916815413a278922e63ff1e404648", size = 529572, upload-time = "2026-04-05T17:35:53.438Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[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 = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[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 = "librt" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/3f/4ca7dd7819bf8ff303aca39c3c60e5320e46e766ab7f7dd627d3b9c11bdf/librt-0.8.0.tar.gz", hash = "sha256:cb74cdcbc0103fc988e04e5c58b0b31e8e5dd2babb9182b6f9490488eb36324b", size = 177306, upload-time = "2026-02-12T14:53:54.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/e9/018cfd60629e0404e6917943789800aa2231defbea540a17b90cc4547b97/librt-0.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db63cf3586a24241e89ca1ce0b56baaec9d371a328bd186c529b27c914c9a1ef", size = 65690, upload-time = "2026-02-12T14:51:57.761Z" }, + { url = "https://files.pythonhosted.org/packages/b5/80/8d39980860e4d1c9497ee50e5cd7c4766d8cfd90d105578eae418e8ffcbc/librt-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ba9d9e60651615bc614be5e21a82cdb7b1769a029369cf4b4d861e4f19686fb6", size = 68373, upload-time = "2026-02-12T14:51:59.013Z" }, + { url = "https://files.pythonhosted.org/packages/2d/76/6e6f7a443af63977e421bd542551fec4072d9eaba02e671b05b238fe73bc/librt-0.8.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb4b3ad543084ed79f186741470b251b9d269cd8b03556f15a8d1a99a64b7de5", size = 197091, upload-time = "2026-02-12T14:52:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/14/40/fa064181c231334c9f4cb69eb338132d39510c8928e84beba34b861d0a71/librt-0.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d2720335020219197380ccfa5c895f079ac364b4c429e96952cd6509934d8eb", size = 207350, upload-time = "2026-02-12T14:52:02.32Z" }, + { url = "https://files.pythonhosted.org/packages/50/49/e7f8438dd226305e3e5955d495114ad01448e6a6ffc0303289b4153b5fc5/librt-0.8.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726305d3e53419d27fc8cdfcd3f9571f0ceae22fa6b5ea1b3662c2e538f833e", size = 219962, upload-time = "2026-02-12T14:52:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/1f/2c/74086fc5d52e77107a3cc80a9a3209be6ad1c9b6bc99969d8d9bbf9fdfe4/librt-0.8.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3d107f603b5ee7a79b6aa6f166551b99b32fb4a5303c4dfcb4222fc6a0335e", size = 212939, upload-time = "2026-02-12T14:52:05.537Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ae/d6917c0ebec9bc2e0293903d6a5ccc7cdb64c228e529e96520b277318f25/librt-0.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41064a0c07b4cc7a81355ccc305cb097d6027002209ffca51306e65ee8293630", size = 221393, upload-time = "2026-02-12T14:52:07.164Z" }, + { url = "https://files.pythonhosted.org/packages/04/97/15df8270f524ce09ad5c19cbbe0e8f95067582507149a6c90594e7795370/librt-0.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c6e4c10761ddbc0d67d2f6e2753daf99908db85d8b901729bf2bf5eaa60e0567", size = 216721, upload-time = "2026-02-12T14:52:08.857Z" }, + { url = "https://files.pythonhosted.org/packages/c4/52/17cbcf9b7a1bae5016d9d3561bc7169b32c3bd216c47d934d3f270602c0c/librt-0.8.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ba581acad5ac8f33e2ff1746e8a57e001b47c6721873121bf8bbcf7ba8bd3aa4", size = 214790, upload-time = "2026-02-12T14:52:10.033Z" }, + { url = "https://files.pythonhosted.org/packages/2a/2d/010a236e8dc4d717dd545c46fd036dcced2c7ede71ef85cf55325809ff92/librt-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bdab762e2c0b48bab76f1a08acb3f4c77afd2123bedac59446aeaaeed3d086cf", size = 237384, upload-time = "2026-02-12T14:52:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/38/14/f1c0eff3df8760dee761029efb72991c554d9f3282f1048e8c3d0eb60997/librt-0.8.0-cp310-cp310-win32.whl", hash = "sha256:6a3146c63220d814c4a2c7d6a1eacc8d5c14aed0ff85115c1dfea868080cd18f", size = 54289, upload-time = "2026-02-12T14:52:12.798Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0b/2684d473e64890882729f91866ed97ccc0a751a0afc3b4bf1a7b57094dbb/librt-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:bbebd2bba5c6ae02907df49150e55870fdd7440d727b6192c46b6f754723dde9", size = 61347, upload-time = "2026-02-12T14:52:13.793Z" }, + { url = "https://files.pythonhosted.org/packages/51/e9/42af181c89b65abfd557c1b017cba5b82098eef7bf26d1649d82ce93ccc7/librt-0.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ce33a9778e294507f3a0e3468eccb6a698b5166df7db85661543eca1cfc5369", size = 65314, upload-time = "2026-02-12T14:52:14.778Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4a/15a847fca119dc0334a4b8012b1e15fdc5fc19d505b71e227eaf1bcdba09/librt-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8070aa3368559de81061ef752770d03ca1f5fc9467d4d512d405bd0483bfffe6", size = 68015, upload-time = "2026-02-12T14:52:15.797Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/ffc8dbd6ab68dd91b736c88529411a6729649d2b74b887f91f3aaff8d992/librt-0.8.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:20f73d4fecba969efc15cdefd030e382502d56bb6f1fc66b580cce582836c9fa", size = 194508, upload-time = "2026-02-12T14:52:16.835Z" }, + { url = "https://files.pythonhosted.org/packages/89/92/a7355cea28d6c48ff6ff5083ac4a2a866fb9b07b786aa70d1f1116680cd5/librt-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a512c88900bdb1d448882f5623a0b1ad27ba81a9bd75dacfe17080b72272ca1f", size = 205630, upload-time = "2026-02-12T14:52:18.58Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5e/54509038d7ac527828db95b8ba1c8f5d2649bc32fd8f39b1718ec9957dce/librt-0.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:015e2dde6e096d27c10238bf9f6492ba6c65822dfb69d2bf74c41a8e88b7ddef", size = 218289, upload-time = "2026-02-12T14:52:20.134Z" }, + { url = "https://files.pythonhosted.org/packages/6d/17/0ee0d13685cefee6d6f2d47bb643ddad3c62387e2882139794e6a5f1288a/librt-0.8.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c25a131013eadd3c600686a0c0333eb2896483cbc7f65baa6a7ee761017aef9", size = 211508, upload-time = "2026-02-12T14:52:21.413Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a8/1714ef6e9325582e3727de3be27e4c1b2f428ea411d09f1396374180f130/librt-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:21b14464bee0b604d80a638cf1ee3148d84ca4cc163dcdcecb46060c1b3605e4", size = 219129, upload-time = "2026-02-12T14:52:22.61Z" }, + { url = "https://files.pythonhosted.org/packages/89/d3/2d9fe353edff91cdc0ece179348054a6fa61f3de992c44b9477cb973509b/librt-0.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:05a3dd3f116747f7e1a2b475ccdc6fb637fd4987126d109e03013a79d40bf9e6", size = 213126, upload-time = "2026-02-12T14:52:23.819Z" }, + { url = "https://files.pythonhosted.org/packages/ad/8e/9f5c60444880f6ad50e3ff7475e5529e787797e7f3ad5432241633733b92/librt-0.8.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fa37f99bff354ff191c6bcdffbc9d7cdd4fc37faccfc9be0ef3a4fd5613977da", size = 212279, upload-time = "2026-02-12T14:52:25.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/eb/d4a2cfa647da3022ae977f50d7eda1d91f70d7d1883cf958a4b6ef689eab/librt-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1566dbb9d1eb0987264c9b9460d212e809ba908d2f4a3999383a84d765f2f3f1", size = 234654, upload-time = "2026-02-12T14:52:26.204Z" }, + { url = "https://files.pythonhosted.org/packages/6a/31/26b978861c7983b036a3aea08bdbb2ec32bbaab1ad1d57c5e022be59afc1/librt-0.8.0-cp311-cp311-win32.whl", hash = "sha256:70defb797c4d5402166787a6b3c66dfb3fa7f93d118c0509ffafa35a392f4258", size = 54603, upload-time = "2026-02-12T14:52:27.342Z" }, + { url = "https://files.pythonhosted.org/packages/d0/78/f194ed7c48dacf875677e749c5d0d1d69a9daa7c994314a39466237fb1be/librt-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:db953b675079884ffda33d1dca7189fb961b6d372153750beb81880384300817", size = 61730, upload-time = "2026-02-12T14:52:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/97/ee/ad71095478d02137b6f49469dc808c595cfe89b50985f6b39c5345f0faab/librt-0.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:75d1a8cab20b2043f03f7aab730551e9e440adc034d776f15f6f8d582b0a5ad4", size = 52274, upload-time = "2026-02-12T14:52:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fb/53/f3bc0c4921adb0d4a5afa0656f2c0fbe20e18e3e0295e12985b9a5dc3f55/librt-0.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:17269dd2745dbe8e42475acb28e419ad92dfa38214224b1b01020b8cac70b645", size = 66511, upload-time = "2026-02-12T14:52:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/89/4b/4c96357432007c25a1b5e363045373a6c39481e49f6ba05234bb59a839c1/librt-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f4617cef654fca552f00ce5ffdf4f4b68770f18950e4246ce94629b789b92467", size = 68628, upload-time = "2026-02-12T14:52:31.491Z" }, + { url = "https://files.pythonhosted.org/packages/47/16/52d75374d1012e8fc709216b5eaa25f471370e2a2331b8be00f18670a6c7/librt-0.8.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5cb11061a736a9db45e3c1293cfcb1e3caf205912dfa085734ba750f2197ff9a", size = 198941, upload-time = "2026-02-12T14:52:32.489Z" }, + { url = "https://files.pythonhosted.org/packages/fc/11/d5dd89e5a2228567b1228d8602d896736247424484db086eea6b8010bcba/librt-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb00bd71b448f16749909b08a0ff16f58b079e2261c2e1000f2bbb2a4f0a45", size = 210009, upload-time = "2026-02-12T14:52:33.634Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/fc1a92a77c3020ee08ce2dc48aed4b42ab7c30fb43ce488d388673b0f164/librt-0.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95a719a049f0eefaf1952673223cf00d442952273cbd20cf2ed7ec423a0ef58d", size = 224461, upload-time = "2026-02-12T14:52:34.868Z" }, + { url = "https://files.pythonhosted.org/packages/7f/98/eb923e8b028cece924c246104aa800cf72e02d023a8ad4ca87135b05a2fe/librt-0.8.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bd32add59b58fba3439d48d6f36ac695830388e3da3e92e4fc26d2d02670d19c", size = 217538, upload-time = "2026-02-12T14:52:36.078Z" }, + { url = "https://files.pythonhosted.org/packages/fd/67/24e80ab170674a1d8ee9f9a83081dca4635519dbd0473b8321deecddb5be/librt-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f764b2424cb04524ff7a486b9c391e93f93dc1bd8305b2136d25e582e99aa2f", size = 225110, upload-time = "2026-02-12T14:52:37.301Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c7/6fbdcbd1a6e5243c7989c21d68ab967c153b391351174b4729e359d9977f/librt-0.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f04ca50e847abc486fa8f4107250566441e693779a5374ba211e96e238f298b9", size = 217758, upload-time = "2026-02-12T14:52:38.89Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bd/4d6b36669db086e3d747434430073e14def032dd58ad97959bf7e2d06c67/librt-0.8.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9ab3a3475a55b89b87ffd7e6665838e8458e0b596c22e0177e0f961434ec474a", size = 218384, upload-time = "2026-02-12T14:52:40.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/2d/afe966beb0a8f179b132f3e95c8dd90738a23e9ebdba10f89a3f192f9366/librt-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e36a8da17134ffc29373775d88c04832f9ecfab1880470661813e6c7991ef79", size = 241187, upload-time = "2026-02-12T14:52:43.55Z" }, + { url = "https://files.pythonhosted.org/packages/02/d0/6172ea4af2b538462785ab1a68e52d5c99cfb9866a7caf00fdf388299734/librt-0.8.0-cp312-cp312-win32.whl", hash = "sha256:4eb5e06ebcc668677ed6389164f52f13f71737fc8be471101fa8b4ce77baeb0c", size = 54914, upload-time = "2026-02-12T14:52:44.676Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cb/ceb6ed6175612a4337ad49fb01ef594712b934b4bc88ce8a63554832eb44/librt-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:0a33335eb59921e77c9acc05d0e654e4e32e45b014a4d61517897c11591094f8", size = 62020, upload-time = "2026-02-12T14:52:45.676Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7e/61701acbc67da74ce06ddc7ba9483e81c70f44236b2d00f6a4bfee1aacbf/librt-0.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:24a01c13a2a9bdad20997a4443ebe6e329df063d1978bbe2ebbf637878a46d1e", size = 52443, upload-time = "2026-02-12T14:52:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/6d/32/3edb0bcb4113a9c8bdcd1750663a54565d255027657a5df9d90f13ee07fa/librt-0.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7f820210e21e3a8bf8fde2ae3c3d10106d4de9ead28cbfdf6d0f0f41f5b12fa1", size = 66522, upload-time = "2026-02-12T14:52:48.219Z" }, + { url = "https://files.pythonhosted.org/packages/30/ab/e8c3d05e281f5d405ebdcc5bc8ab36df23e1a4b40ac9da8c3eb9928b72b9/librt-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4831c44b8919e75ca0dfb52052897c1ef59fdae19d3589893fbd068f1e41afbf", size = 68658, upload-time = "2026-02-12T14:52:50.351Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d3/74a206c47b7748bbc8c43942de3ed67de4c231156e148b4f9250869593df/librt-0.8.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:88c6e75540f1f10f5e0fc5e87b4b6c290f0e90d1db8c6734f670840494764af8", size = 199287, upload-time = "2026-02-12T14:52:51.938Z" }, + { url = "https://files.pythonhosted.org/packages/fa/29/ef98a9131cf12cb95771d24e4c411fda96c89dc78b09c2de4704877ebee4/librt-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9646178cd794704d722306c2c920c221abbf080fede3ba539d5afdec16c46dad", size = 210293, upload-time = "2026-02-12T14:52:53.128Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3e/89b4968cb08c53d4c2d8b02517081dfe4b9e07a959ec143d333d76899f6c/librt-0.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e1af31a710e17891d9adf0dbd9a5fcd94901a3922a96499abdbf7ce658f4e01", size = 224801, upload-time = "2026-02-12T14:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/6d/28/f38526d501f9513f8b48d78e6be4a241e15dd4b000056dc8b3f06ee9ce5d/librt-0.8.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:507e94f4bec00b2f590fbe55f48cd518a208e2474a3b90a60aa8f29136ddbada", size = 218090, upload-time = "2026-02-12T14:52:55.758Z" }, + { url = "https://files.pythonhosted.org/packages/02/ec/64e29887c5009c24dc9c397116c680caffc50286f62bd99c39e3875a2854/librt-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f1178e0de0c271231a660fbef9be6acdfa1d596803464706862bef6644cc1cae", size = 225483, upload-time = "2026-02-12T14:52:57.375Z" }, + { url = "https://files.pythonhosted.org/packages/ee/16/7850bdbc9f1a32d3feff2708d90c56fc0490b13f1012e438532781aa598c/librt-0.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:71fc517efc14f75c2f74b1f0a5d5eb4a8e06aa135c34d18eaf3522f4a53cd62d", size = 218226, upload-time = "2026-02-12T14:52:58.534Z" }, + { url = "https://files.pythonhosted.org/packages/1c/4a/166bffc992d65ddefa7c47052010a87c059b44a458ebaf8f5eba384b0533/librt-0.8.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0583aef7e9a720dd40f26a2ad5a1bf2ccbb90059dac2b32ac516df232c701db3", size = 218755, upload-time = "2026-02-12T14:52:59.701Z" }, + { url = "https://files.pythonhosted.org/packages/da/5d/9aeee038bcc72a9cfaaee934463fe9280a73c5440d36bd3175069d2cb97b/librt-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d0f76fc73480d42285c609c0ea74d79856c160fa828ff9aceab574ea4ecfd7b", size = 241617, upload-time = "2026-02-12T14:53:00.966Z" }, + { url = "https://files.pythonhosted.org/packages/64/ff/2bec6b0296b9d0402aa6ec8540aa19ebcb875d669c37800cb43d10d9c3a3/librt-0.8.0-cp313-cp313-win32.whl", hash = "sha256:e79dbc8f57de360f0ed987dc7de7be814b4803ef0e8fc6d3ff86e16798c99935", size = 54966, upload-time = "2026-02-12T14:53:02.042Z" }, + { url = "https://files.pythonhosted.org/packages/08/8d/bf44633b0182996b2c7ea69a03a5c529683fa1f6b8e45c03fe874ff40d56/librt-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:25b3e667cbfc9000c4740b282df599ebd91dbdcc1aa6785050e4c1d6be5329ab", size = 62000, upload-time = "2026-02-12T14:53:03.822Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fd/c6472b8e0eac0925001f75e366cf5500bcb975357a65ef1f6b5749389d3a/librt-0.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:e9a3a38eb4134ad33122a6d575e6324831f930a771d951a15ce232e0237412c2", size = 52496, upload-time = "2026-02-12T14:53:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/13/79ebfe30cd273d7c0ce37a5f14dc489c5fb8b722a008983db2cfd57270bb/librt-0.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:421765e8c6b18e64d21c8ead315708a56fc24f44075059702e421d164575fdda", size = 66078, upload-time = "2026-02-12T14:53:06.085Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8f/d11eca40b62a8d5e759239a80636386ef88adecb10d1a050b38cc0da9f9e/librt-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:48f84830a8f8ad7918afd743fd7c4eb558728bceab7b0e38fd5a5cf78206a556", size = 68309, upload-time = "2026-02-12T14:53:07.121Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b4/f12ee70a3596db40ff3c88ec9eaa4e323f3b92f77505b4d900746706ec6a/librt-0.8.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9f09d4884f882baa39a7e36bbf3eae124c4ca2a223efb91e567381d1c55c6b06", size = 196804, upload-time = "2026-02-12T14:53:08.164Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7e/70dbbdc0271fd626abe1671ad117bcd61a9a88cdc6a10ccfbfc703db1873/librt-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:693697133c3b32aa9b27f040e3691be210e9ac4d905061859a9ed519b1d5a376", size = 206915, upload-time = "2026-02-12T14:53:09.333Z" }, + { url = "https://files.pythonhosted.org/packages/79/13/6b9e05a635d4327608d06b3c1702166e3b3e78315846373446cf90d7b0bf/librt-0.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5512aae4648152abaf4d48b59890503fcbe86e85abc12fb9b096fe948bdd816", size = 221200, upload-time = "2026-02-12T14:53:10.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/6c/e19a3ac53e9414de43a73d7507d2d766cd22d8ca763d29a4e072d628db42/librt-0.8.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:995d24caa6bbb34bcdd4a41df98ac6d1af637cfa8975cb0790e47d6623e70e3e", size = 214640, upload-time = "2026-02-12T14:53:12.342Z" }, + { url = "https://files.pythonhosted.org/packages/30/f0/23a78464788619e8c70f090cfd099cce4973eed142c4dccb99fc322283fd/librt-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b9aef96d7593584e31ef6ac1eb9775355b0099fee7651fae3a15bc8657b67b52", size = 221980, upload-time = "2026-02-12T14:53:13.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/32/38e21420c5d7aa8a8bd2c7a7d5252ab174a5a8aaec8b5551968979b747bf/librt-0.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4f6e975377fbc4c9567cb33ea9ab826031b6c7ec0515bfae66a4fb110d40d6da", size = 215146, upload-time = "2026-02-12T14:53:14.8Z" }, + { url = "https://files.pythonhosted.org/packages/bb/00/bd9ecf38b1824c25240b3ad982fb62c80f0a969e6679091ba2b3afb2b510/librt-0.8.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:daae5e955764be8fd70a93e9e5133c75297f8bce1e802e1d3683b98f77e1c5ab", size = 215203, upload-time = "2026-02-12T14:53:16.087Z" }, + { url = "https://files.pythonhosted.org/packages/b9/60/7559bcc5279d37810b98d4a52616febd7b8eef04391714fd6bdf629598b1/librt-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7bd68cebf3131bb920d5984f75fe302d758db33264e44b45ad139385662d7bc3", size = 237937, upload-time = "2026-02-12T14:53:17.236Z" }, + { url = "https://files.pythonhosted.org/packages/41/cc/be3e7da88f1abbe2642672af1dc00a0bccece11ca60241b1883f3018d8d5/librt-0.8.0-cp314-cp314-win32.whl", hash = "sha256:1e6811cac1dcb27ca4c74e0ca4a5917a8e06db0d8408d30daee3a41724bfde7a", size = 50685, upload-time = "2026-02-12T14:53:18.888Z" }, + { url = "https://files.pythonhosted.org/packages/38/27/e381d0df182a8f61ef1f6025d8b138b3318cc9d18ad4d5f47c3bf7492523/librt-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:178707cda89d910c3b28bf5aa5f69d3d4734e0f6ae102f753ad79edef83a83c7", size = 57872, upload-time = "2026-02-12T14:53:19.942Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0c/ca9dfdf00554a44dea7d555001248269a4bab569e1590a91391feb863fa4/librt-0.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3e8b77b5f54d0937b26512774916041756c9eb3e66f1031971e626eea49d0bf4", size = 48056, upload-time = "2026-02-12T14:53:21.473Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ed/6cc9c4ad24f90c8e782193c7b4a857408fd49540800613d1356c63567d7b/librt-0.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:789911e8fa40a2e82f41120c936b1965f3213c67f5a483fc5a41f5839a05dcbb", size = 68307, upload-time = "2026-02-12T14:53:22.498Z" }, + { url = "https://files.pythonhosted.org/packages/84/d8/0e94292c6b3e00b6eeea39dd44d5703d1ec29b6dafce7eea19dc8f1aedbd/librt-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2b37437e7e4ef5e15a297b36ba9e577f73e29564131d86dd75875705e97402b5", size = 70999, upload-time = "2026-02-12T14:53:23.603Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f4/6be1afcbdeedbdbbf54a7c9d73ad43e1bf36897cebf3978308cd64922e02/librt-0.8.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:671a6152edf3b924d98a5ed5e6982ec9cb30894085482acadce0975f031d4c5c", size = 220782, upload-time = "2026-02-12T14:53:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8d/f306e8caa93cfaf5c6c9e0d940908d75dc6af4fd856baa5535c922ee02b1/librt-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8992ca186a1678107b0af3d0c9303d8c7305981b9914989b9788319ed4d89546", size = 235420, upload-time = "2026-02-12T14:53:27.047Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f2/65d86bd462e9c351326564ca805e8457442149f348496e25ccd94583ffa2/librt-0.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:001e5330093d887b8b9165823eca6c5c4db183fe4edea4fdc0680bbac5f46944", size = 246452, upload-time = "2026-02-12T14:53:28.341Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/39c88b503b4cb3fcbdeb3caa29672b6b44ebee8dcc8a54d49839ac280f3f/librt-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d920789eca7ef71df7f31fd547ec0d3002e04d77f30ba6881e08a630e7b2c30e", size = 238891, upload-time = "2026-02-12T14:53:29.625Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c6/6c0d68190893d01b71b9569b07a1c811e280c0065a791249921c83dc0290/librt-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:82fb4602d1b3e303a58bfe6165992b5a78d823ec646445356c332cd5f5bbaa61", size = 250249, upload-time = "2026-02-12T14:53:30.93Z" }, + { url = "https://files.pythonhosted.org/packages/52/7a/f715ed9e039035d0ea637579c3c0155ab3709a7046bc408c0fb05d337121/librt-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4d3e38797eb482485b486898f89415a6ab163bc291476bd95712e42cf4383c05", size = 240642, upload-time = "2026-02-12T14:53:32.174Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3c/609000a333debf5992efe087edc6467c1fdbdddca5b610355569bbea9589/librt-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a905091a13e0884701226860836d0386b88c72ce5c2fdfba6618e14c72be9f25", size = 239621, upload-time = "2026-02-12T14:53:33.39Z" }, + { url = "https://files.pythonhosted.org/packages/b9/df/87b0673d5c395a8f34f38569c116c93142d4dc7e04af2510620772d6bd4f/librt-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:375eda7acfce1f15f5ed56cfc960669eefa1ec8732e3e9087c3c4c3f2066759c", size = 262986, upload-time = "2026-02-12T14:53:34.617Z" }, + { url = "https://files.pythonhosted.org/packages/09/7f/6bbbe9dcda649684773aaea78b87fff4d7e59550fbc2877faa83612087a3/librt-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:2ccdd20d9a72c562ffb73098ac411de351b53a6fbb3390903b2d33078ef90447", size = 51328, upload-time = "2026-02-12T14:53:36.15Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f3/e1981ab6fa9b41be0396648b5850267888a752d025313a9e929c4856208e/librt-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:25e82d920d4d62ad741592fcf8d0f3bda0e3fc388a184cb7d2f566c681c5f7b9", size = 58719, upload-time = "2026-02-12T14:53:37.183Z" }, + { url = "https://files.pythonhosted.org/packages/94/d1/433b3c06e78f23486fe4fdd19bc134657eb30997d2054b0dbf52bbf3382e/librt-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:92249938ab744a5890580d3cb2b22042f0dce71cdaa7c1369823df62bedf7cbc", size = 48753, upload-time = "2026-02-12T14:53:38.539Z" }, +] + +[[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/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[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.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "parsimonious" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/91/abdc50c4ef06fdf8d047f60ee777ca9b2a7885e1a9cea81343fbecda52d7/parsimonious-0.10.0.tar.gz", hash = "sha256:8281600da180ec8ae35427a4ab4f7b82bfec1e3d1e52f80cb60ea82b9512501c", size = 52172, upload-time = "2022-09-03T17:01:17.004Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/0f/c8b64d9b54ea631fcad4e9e3c8dbe8c11bb32a623be94f22974c88e71eaf/parsimonious-0.10.0-py3-none-any.whl", hash = "sha256:982ab435fabe86519b57f6b35610aa4e4e977e9f02a14353edf4bbc75369fc0f", size = 48427, upload-time = "2022-09-03T17:01:13.814Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, +] + +[[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 = "protobuf" +version = "6.33.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, + { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pycryptodome" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" }, + { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" }, + { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" }, + { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" }, + { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" }, + { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, + { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, + { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, + { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, + { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/d9/12/e33935a0709c07de084d7d58d330ec3f4daf7910a18e77937affdb728452/pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379", size = 1623886, upload-time = "2025-05-17T17:21:20.614Z" }, + { url = "https://files.pythonhosted.org/packages/22/0b/aa8f9419f25870889bebf0b26b223c6986652bdf071f000623df11212c90/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4", size = 1672151, upload-time = "2025-05-17T17:21:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5e/63f5cbde2342b7f70a39e591dbe75d9809d6338ce0b07c10406f1a140cdc/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630", size = 1664461, upload-time = "2025-05-17T17:21:25.225Z" }, + { url = "https://files.pythonhosted.org/packages/d6/92/608fbdad566ebe499297a86aae5f2a5263818ceeecd16733006f1600403c/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353", size = 1702440, upload-time = "2025-05-17T17:21:27.991Z" }, + { url = "https://files.pythonhosted.org/packages/d1/92/2eadd1341abd2989cce2e2740b4423608ee2014acb8110438244ee97d7ff/pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5", size = 1803005, upload-time = "2025-05-17T17:21:31.37Z" }, +] + +[[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/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { 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/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { 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" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "regex" +version = "2026.1.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/d2/e6ee96b7dff201a83f650241c52db8e5bd080967cb93211f57aa448dc9d6/regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e", size = 488166, upload-time = "2026-01-14T23:13:46.408Z" }, + { url = "https://files.pythonhosted.org/packages/23/8a/819e9ce14c9f87af026d0690901b3931f3101160833e5d4c8061fa3a1b67/regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f", size = 290632, upload-time = "2026-01-14T23:13:48.688Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c3/23dfe15af25d1d45b07dfd4caa6003ad710dcdcb4c4b279909bdfe7a2de8/regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b", size = 288500, upload-time = "2026-01-14T23:13:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/c6/31/1adc33e2f717df30d2f4d973f8776d2ba6ecf939301efab29fca57505c95/regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c", size = 781670, upload-time = "2026-01-14T23:13:52.453Z" }, + { url = "https://files.pythonhosted.org/packages/23/ce/21a8a22d13bc4adcb927c27b840c948f15fc973e21ed2346c1bd0eae22dc/regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9", size = 850820, upload-time = "2026-01-14T23:13:54.894Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/3eeacdf587a4705a44484cd0b30e9230a0e602811fb3e2cc32268c70d509/regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c", size = 898777, upload-time = "2026-01-14T23:13:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/79/a9/1898a077e2965c35fc22796488141a22676eed2d73701e37c73ad7c0b459/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106", size = 791750, upload-time = "2026-01-14T23:13:58.527Z" }, + { url = "https://files.pythonhosted.org/packages/4c/84/e31f9d149a178889b3817212827f5e0e8c827a049ff31b4b381e76b26e2d/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618", size = 782674, upload-time = "2026-01-14T23:13:59.874Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ff/adf60063db24532add6a1676943754a5654dcac8237af024ede38244fd12/regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4", size = 767906, upload-time = "2026-01-14T23:14:01.298Z" }, + { url = "https://files.pythonhosted.org/packages/af/3e/e6a216cee1e2780fec11afe7fc47b6f3925d7264e8149c607ac389fd9b1a/regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79", size = 774798, upload-time = "2026-01-14T23:14:02.715Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/23a4a8378a9208514ed3efc7e7850c27fa01e00ed8557c958df0335edc4a/regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9", size = 845861, upload-time = "2026-01-14T23:14:04.824Z" }, + { url = "https://files.pythonhosted.org/packages/f8/57/d7605a9d53bd07421a8785d349cd29677fe660e13674fa4c6cbd624ae354/regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220", size = 755648, upload-time = "2026-01-14T23:14:06.371Z" }, + { url = "https://files.pythonhosted.org/packages/6f/76/6f2e24aa192da1e299cc1101674a60579d3912391867ce0b946ba83e2194/regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13", size = 836250, upload-time = "2026-01-14T23:14:08.343Z" }, + { url = "https://files.pythonhosted.org/packages/11/3a/1f2a1d29453299a7858eab7759045fc3d9d1b429b088dec2dc85b6fa16a2/regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3", size = 779919, upload-time = "2026-01-14T23:14:09.954Z" }, + { url = "https://files.pythonhosted.org/packages/c0/67/eab9bc955c9dcc58e9b222c801e39cff7ca0b04261792a2149166ce7e792/regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218", size = 265888, upload-time = "2026-01-14T23:14:11.35Z" }, + { url = "https://files.pythonhosted.org/packages/1d/62/31d16ae24e1f8803bddb0885508acecaec997fcdcde9c243787103119ae4/regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a", size = 277830, upload-time = "2026-01-14T23:14:12.908Z" }, + { url = "https://files.pythonhosted.org/packages/e5/36/5d9972bccd6417ecd5a8be319cebfd80b296875e7f116c37fb2a2deecebf/regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3", size = 270376, upload-time = "2026-01-14T23:14:14.782Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e1/8f43b03a4968c748858ec77f746c286d81f896c2e437ccf050ebc5d3128c/regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b", size = 793503, upload-time = "2026-01-14T23:14:20.922Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4e/a39a5e8edc5377a46a7c875c2f9a626ed3338cb3bb06931be461c3e1a34a/regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8", size = 860535, upload-time = "2026-01-14T23:14:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1c/9dce667a32a9477f7a2869c1c767dc00727284a9fa3ff5c09a5c6c03575e/regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413", size = 907225, upload-time = "2026-01-14T23:14:23.897Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3c/87ca0a02736d16b6262921425e84b48984e77d8e4e572c9072ce96e66c30/regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026", size = 800526, upload-time = "2026-01-14T23:14:26.039Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/647d5715aeea7c87bdcbd2f578f47b415f55c24e361e639fe8c0cc88878f/regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785", size = 773446, upload-time = "2026-01-14T23:14:28.109Z" }, + { url = "https://files.pythonhosted.org/packages/af/89/bf22cac25cb4ba0fe6bff52ebedbb65b77a179052a9d6037136ae93f42f4/regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e", size = 783051, upload-time = "2026-01-14T23:14:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f4/6ed03e71dca6348a5188363a34f5e26ffd5db1404780288ff0d79513bce4/regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763", size = 854485, upload-time = "2026-01-14T23:14:31.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/8e8560bd78caded8eb137e3e47612430a05b9a772caf60876435192d670a/regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb", size = 762195, upload-time = "2026-01-14T23:14:32.802Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61fc710f9aa8dfcd764fe27d37edfaa023b1a23305a0d84fccd5adb346ea/regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2", size = 845986, upload-time = "2026-01-14T23:14:34.898Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2e/fbee4cb93f9d686901a7ca8d94285b80405e8c34fe4107f63ffcbfb56379/regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1", size = 788992, upload-time = "2026-01-14T23:14:37.116Z" }, + { url = "https://files.pythonhosted.org/packages/ed/14/3076348f3f586de64b1ab75a3fbabdaab7684af7f308ad43be7ef1849e55/regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569", size = 265893, upload-time = "2026-01-14T23:14:38.426Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/772cf8b5fc803f5c89ba85d8b1870a1ca580dc482aa030383a9289c82e44/regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7", size = 277840, upload-time = "2026-01-14T23:14:39.785Z" }, + { url = "https://files.pythonhosted.org/packages/78/84/d05f61142709474da3c0853222d91086d3e1372bcdab516c6fd8d80f3297/regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec", size = 270374, upload-time = "2026-01-14T23:14:41.592Z" }, + { url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" }, + { url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" }, + { url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" }, + { url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" }, + { url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" }, + { url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" }, + { url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" }, + { url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2e/6870bb16e982669b674cce3ee9ff2d1d46ab80528ee6bcc20fb2292efb60/regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e", size = 489164, upload-time = "2026-01-14T23:15:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/9774542e203849b0286badf67199970a44ebdb0cc5fb739f06e47ada72f8/regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10", size = 291218, upload-time = "2026-01-14T23:15:15.647Z" }, + { url = "https://files.pythonhosted.org/packages/b2/87/b0cda79f22b8dee05f774922a214da109f9a4c0eca5da2c9d72d77ea062c/regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc", size = 288895, upload-time = "2026-01-14T23:15:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/3b/6a/0041f0a2170d32be01ab981d6346c83a8934277d82c780d60b127331f264/regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599", size = 798680, upload-time = "2026-01-14T23:15:19.342Z" }, + { url = "https://files.pythonhosted.org/packages/58/de/30e1cfcdbe3e891324aa7568b7c968771f82190df5524fabc1138cb2d45a/regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae", size = 864210, upload-time = "2026-01-14T23:15:22.005Z" }, + { url = "https://files.pythonhosted.org/packages/64/44/4db2f5c5ca0ccd40ff052ae7b1e9731352fcdad946c2b812285a7505ca75/regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5", size = 912358, upload-time = "2026-01-14T23:15:24.569Z" }, + { url = "https://files.pythonhosted.org/packages/79/b6/e6a5665d43a7c42467138c8a2549be432bad22cbd206f5ec87162de74bd7/regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6", size = 803583, upload-time = "2026-01-14T23:15:26.526Z" }, + { url = "https://files.pythonhosted.org/packages/e7/53/7cd478222169d85d74d7437e74750005e993f52f335f7c04ff7adfda3310/regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788", size = 775782, upload-time = "2026-01-14T23:15:29.352Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b5/75f9a9ee4b03a7c009fe60500fe550b45df94f0955ca29af16333ef557c5/regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714", size = 787978, upload-time = "2026-01-14T23:15:31.295Z" }, + { url = "https://files.pythonhosted.org/packages/72/b3/79821c826245bbe9ccbb54f6eadb7879c722fd3e0248c17bfc90bf54e123/regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d", size = 858550, upload-time = "2026-01-14T23:15:33.558Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/2ab5f77a1c465745bfbfcb3ad63178a58337ae8d5274315e2cc623a822fa/regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3", size = 763747, upload-time = "2026-01-14T23:15:35.206Z" }, + { url = "https://files.pythonhosted.org/packages/6d/84/c27df502d4bfe2873a3e3a7cf1bdb2b9cc10284d1a44797cf38bed790470/regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31", size = 850615, upload-time = "2026-01-14T23:15:37.523Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b7/658a9782fb253680aa8ecb5ccbb51f69e088ed48142c46d9f0c99b46c575/regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3", size = 789951, upload-time = "2026-01-14T23:15:39.582Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2a/5928af114441e059f15b2f63e188bd00c6529b3051c974ade7444b85fcda/regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f", size = 266275, upload-time = "2026-01-14T23:15:42.108Z" }, + { url = "https://files.pythonhosted.org/packages/4f/16/5bfbb89e435897bff28cf0352a992ca719d9e55ebf8b629203c96b6ce4f7/regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e", size = 277145, upload-time = "2026-01-14T23:15:44.244Z" }, + { url = "https://files.pythonhosted.org/packages/56/c1/a09ff7392ef4233296e821aec5f78c51be5e91ffde0d163059e50fd75835/regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337", size = 270411, upload-time = "2026-01-14T23:15:45.858Z" }, + { url = "https://files.pythonhosted.org/packages/3c/38/0cfd5a78e5c6db00e6782fdae70458f89850ce95baa5e8694ab91d89744f/regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be", size = 492068, upload-time = "2026-01-14T23:15:47.616Z" }, + { url = "https://files.pythonhosted.org/packages/50/72/6c86acff16cb7c959c4355826bbf06aad670682d07c8f3998d9ef4fee7cd/regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8", size = 292756, upload-time = "2026-01-14T23:15:49.307Z" }, + { url = "https://files.pythonhosted.org/packages/4e/58/df7fb69eadfe76526ddfce28abdc0af09ffe65f20c2c90932e89d705153f/regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd", size = 291114, upload-time = "2026-01-14T23:15:51.484Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6c/a4011cd1cf96b90d2cdc7e156f91efbd26531e822a7fbb82a43c1016678e/regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a", size = 807524, upload-time = "2026-01-14T23:15:53.102Z" }, + { url = "https://files.pythonhosted.org/packages/1d/25/a53ffb73183f69c3e9f4355c4922b76d2840aee160af6af5fac229b6201d/regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93", size = 873455, upload-time = "2026-01-14T23:15:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/66/0b/8b47fc2e8f97d9b4a851736f3890a5f786443aa8901061c55f24c955f45b/regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af", size = 915007, upload-time = "2026-01-14T23:15:57.041Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fa/97de0d681e6d26fabe71968dbee06dd52819e9a22fdce5dac7256c31ed84/regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09", size = 812794, upload-time = "2026-01-14T23:15:58.916Z" }, + { url = "https://files.pythonhosted.org/packages/22/38/e752f94e860d429654aa2b1c51880bff8dfe8f084268258adf9151cf1f53/regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5", size = 781159, upload-time = "2026-01-14T23:16:00.817Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a7/d739ffaef33c378fc888302a018d7f81080393d96c476b058b8c64fd2b0d/regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794", size = 795558, upload-time = "2026-01-14T23:16:03.267Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c4/542876f9a0ac576100fc73e9c75b779f5c31e3527576cfc9cb3009dcc58a/regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a", size = 868427, upload-time = "2026-01-14T23:16:05.646Z" }, + { url = "https://files.pythonhosted.org/packages/fc/0f/d5655bea5b22069e32ae85a947aa564912f23758e112cdb74212848a1a1b/regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80", size = 769939, upload-time = "2026-01-14T23:16:07.542Z" }, + { url = "https://files.pythonhosted.org/packages/20/06/7e18a4fa9d326daeda46d471a44ef94201c46eaa26dbbb780b5d92cbfdda/regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2", size = 854753, upload-time = "2026-01-14T23:16:10.395Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/dc8946ef3965e166f558ef3b47f492bc364e96a265eb4a2bb3ca765c8e46/regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60", size = 799559, upload-time = "2026-01-14T23:16:12.347Z" }, + { url = "https://files.pythonhosted.org/packages/a5/61/1bba81ff6d50c86c65d9fd84ce9699dd106438ee4cdb105bf60374ee8412/regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952", size = 268879, upload-time = "2026-01-14T23:16:14.049Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/cef7d4c5fb0ea3ac5c775fd37db5747f7378b29526cc83f572198924ff47/regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10", size = 280317, upload-time = "2026-01-14T23:16:15.718Z" }, + { url = "https://files.pythonhosted.org/packages/b4/52/4317f7a5988544e34ab57b4bde0f04944c4786128c933fb09825924d3e82/regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829", size = 271551, upload-time = "2026-01-14T23:16:17.533Z" }, + { url = "https://files.pythonhosted.org/packages/52/0a/47fa888ec7cbbc7d62c5f2a6a888878e76169170ead271a35239edd8f0e8/regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac", size = 489170, upload-time = "2026-01-14T23:16:19.835Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/d000e9b7296c15737c9301708e9e7fbdea009f8e93541b6b43bdb8219646/regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6", size = 291146, upload-time = "2026-01-14T23:16:21.541Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b6/921cc61982e538682bdf3bdf5b2c6ab6b34368da1f8e98a6c1ddc503c9cf/regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2", size = 288986, upload-time = "2026-01-14T23:16:23.381Z" }, + { url = "https://files.pythonhosted.org/packages/ca/33/eb7383dde0bbc93f4fb9d03453aab97e18ad4024ac7e26cef8d1f0a2cff0/regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846", size = 799098, upload-time = "2026-01-14T23:16:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/b664dccae898fc8d8b4c23accd853f723bde0f026c747b6f6262b688029c/regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b", size = 864980, upload-time = "2026-01-14T23:16:27.297Z" }, + { url = "https://files.pythonhosted.org/packages/16/40/0999e064a170eddd237bae9ccfcd8f28b3aa98a38bf727a086425542a4fc/regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e", size = 911607, upload-time = "2026-01-14T23:16:29.235Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/c77f644b68ab054e5a674fb4da40ff7bffb2c88df58afa82dbf86573092d/regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde", size = 803358, upload-time = "2026-01-14T23:16:31.369Z" }, + { url = "https://files.pythonhosted.org/packages/27/31/d4292ea8566eaa551fafc07797961c5963cf5235c797cc2ae19b85dfd04d/regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5", size = 775833, upload-time = "2026-01-14T23:16:33.141Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b2/cff3bf2fea4133aa6fb0d1e370b37544d18c8350a2fa118c7e11d1db0e14/regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34", size = 788045, upload-time = "2026-01-14T23:16:35.005Z" }, + { url = "https://files.pythonhosted.org/packages/8d/99/2cb9b69045372ec877b6f5124bda4eb4253bc58b8fe5848c973f752bc52c/regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75", size = 859374, upload-time = "2026-01-14T23:16:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/09/16/710b0a5abe8e077b1729a562d2f297224ad079f3a66dce46844c193416c8/regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e", size = 763940, upload-time = "2026-01-14T23:16:38.685Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/7585c8e744e40eb3d32f119191969b91de04c073fca98ec14299041f6e7e/regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160", size = 850112, upload-time = "2026-01-14T23:16:40.646Z" }, + { url = "https://files.pythonhosted.org/packages/af/d6/43e1dd85df86c49a347aa57c1f69d12c652c7b60e37ec162e3096194a278/regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1", size = 789586, upload-time = "2026-01-14T23:16:42.799Z" }, + { url = "https://files.pythonhosted.org/packages/93/38/77142422f631e013f316aaae83234c629555729a9fbc952b8a63ac91462a/regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1", size = 271691, upload-time = "2026-01-14T23:16:44.671Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a9/ab16b4649524ca9e05213c1cdbb7faa85cc2aa90a0230d2f796cbaf22736/regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903", size = 280422, upload-time = "2026-01-14T23:16:46.607Z" }, + { url = "https://files.pythonhosted.org/packages/be/2a/20fd057bf3521cb4791f69f869635f73e0aaf2b9ad2d260f728144f9047c/regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705", size = 273467, upload-time = "2026-01-14T23:16:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/ad/77/0b1e81857060b92b9cad239104c46507dd481b3ff1fa79f8e7f865aae38a/regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8", size = 492073, upload-time = "2026-01-14T23:16:51.154Z" }, + { url = "https://files.pythonhosted.org/packages/70/f3/f8302b0c208b22c1e4f423147e1913fd475ddd6230565b299925353de644/regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf", size = 292757, upload-time = "2026-01-14T23:16:53.08Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f0/ef55de2460f3b4a6da9d9e7daacd0cb79d4ef75c64a2af316e68447f0df0/regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d", size = 291122, upload-time = "2026-01-14T23:16:55.383Z" }, + { url = "https://files.pythonhosted.org/packages/cf/55/bb8ccbacabbc3a11d863ee62a9f18b160a83084ea95cdfc5d207bfc3dd75/regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84", size = 807761, upload-time = "2026-01-14T23:16:57.251Z" }, + { url = "https://files.pythonhosted.org/packages/8f/84/f75d937f17f81e55679a0509e86176e29caa7298c38bd1db7ce9c0bf6075/regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df", size = 873538, upload-time = "2026-01-14T23:16:59.349Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d9/0da86327df70349aa8d86390da91171bd3ca4f0e7c1d1d453a9c10344da3/regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434", size = 915066, upload-time = "2026-01-14T23:17:01.607Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5e/f660fb23fc77baa2a61aa1f1fe3a4eea2bbb8a286ddec148030672e18834/regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a", size = 812938, upload-time = "2026-01-14T23:17:04.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/a47a29bfecebbbfd1e5cd3f26b28020a97e4820f1c5148e66e3b7d4b4992/regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10", size = 781314, upload-time = "2026-01-14T23:17:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/65/ec/7ec2bbfd4c3f4e494a24dec4c6943a668e2030426b1b8b949a6462d2c17b/regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac", size = 795652, upload-time = "2026-01-14T23:17:08.521Z" }, + { url = "https://files.pythonhosted.org/packages/46/79/a5d8651ae131fe27d7c521ad300aa7f1c7be1dbeee4d446498af5411b8a9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea", size = 868550, upload-time = "2026-01-14T23:17:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/06/b7/25635d2809664b79f183070786a5552dd4e627e5aedb0065f4e3cf8ee37d/regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e", size = 769981, upload-time = "2026-01-14T23:17:12.871Z" }, + { url = "https://files.pythonhosted.org/packages/16/8b/fc3fcbb2393dcfa4a6c5ffad92dc498e842df4581ea9d14309fcd3c55fb9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521", size = 854780, upload-time = "2026-01-14T23:17:14.837Z" }, + { url = "https://files.pythonhosted.org/packages/d0/38/dde117c76c624713c8a2842530be9c93ca8b606c0f6102d86e8cd1ce8bea/regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db", size = 799778, upload-time = "2026-01-14T23:17:17.369Z" }, + { url = "https://files.pythonhosted.org/packages/e3/0d/3a6cfa9ae99606afb612d8fb7a66b245a9d5ff0f29bb347c8a30b6ad561b/regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e", size = 274667, upload-time = "2026-01-14T23:17:19.301Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b2/297293bb0742fd06b8d8e2572db41a855cdf1cae0bf009b1cb74fe07e196/regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf", size = 284386, upload-time = "2026-01-14T23:17:21.231Z" }, + { url = "https://files.pythonhosted.org/packages/95/e4/a3b9480c78cf8ee86626cb06f8d931d74d775897d44201ccb813097ae697/regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70", size = 274837, upload-time = "2026-01-14T23:17:23.146Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rlp" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/2d/439b0728a92964a04d9c88ea1ca9ebb128893fbbd5834faa31f987f2fd4c/rlp-4.1.0.tar.gz", hash = "sha256:be07564270a96f3e225e2c107db263de96b5bc1f27722d2855bd3459a08e95a9", size = 33429, upload-time = "2025-02-04T22:05:59.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/fb/e4c0ced9893b84ac95b7181d69a9786ce5879aeb3bbbcbba80a164f85d6a/rlp-4.1.0-py3-none-any.whl", hash = "sha256:8eca394c579bad34ee0b937aecb96a57052ff3716e19c7a578883e767bc5da6f", size = 19973, upload-time = "2025-02-04T22:05:57.05Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz", hash = "sha256:6bdea47cdbea30d40f8f8d7d69c0854ba7c15420ec75a26f463290949d7f7e9a", size = 4524893, upload-time = "2026-02-03T17:53:35.357Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/88/3fd1b0aa4b6330d6aaa63a285bc96c9f71970351579152d231ed90914586/ruff-0.15.0-py3-none-linux_armv6l.whl", hash = "sha256:aac4ebaa612a82b23d45964586f24ae9bc23ca101919f5590bdb368d74ad5455", size = 10354332, upload-time = "2026-02-03T17:52:54.892Z" }, + { url = "https://files.pythonhosted.org/packages/72/f6/62e173fbb7eb75cc29fe2576a1e20f0a46f671a2587b5f604bfb0eaf5f6f/ruff-0.15.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dcd4be7cc75cfbbca24a98d04d0b9b36a270d0833241f776b788d59f4142b14d", size = 10767189, upload-time = "2026-02-03T17:53:19.778Z" }, + { url = "https://files.pythonhosted.org/packages/99/e4/968ae17b676d1d2ff101d56dc69cf333e3a4c985e1ec23803df84fc7bf9e/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d747e3319b2bce179c7c1eaad3d884dc0a199b5f4d5187620530adf9105268ce", size = 10075384, upload-time = "2026-02-03T17:53:29.241Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bf/9843c6044ab9e20af879c751487e61333ca79a2c8c3058b15722386b8cae/ruff-0.15.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:650bd9c56ae03102c51a5e4b554d74d825ff3abe4db22b90fd32d816c2e90621", size = 10481363, upload-time = "2026-02-03T17:52:43.332Z" }, + { url = "https://files.pythonhosted.org/packages/55/d9/4ada5ccf4cd1f532db1c8d44b6f664f2208d3d93acbeec18f82315e15193/ruff-0.15.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6664b7eac559e3048223a2da77769c2f92b43a6dfd4720cef42654299a599c9", size = 10187736, upload-time = "2026-02-03T17:53:00.522Z" }, + { url = "https://files.pythonhosted.org/packages/86/e2/f25eaecd446af7bb132af0a1d5b135a62971a41f5366ff41d06d25e77a91/ruff-0.15.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f811f97b0f092b35320d1556f3353bf238763420ade5d9e62ebd2b73f2ff179", size = 10968415, upload-time = "2026-02-03T17:53:15.705Z" }, + { url = "https://files.pythonhosted.org/packages/e7/dc/f06a8558d06333bf79b497d29a50c3a673d9251214e0d7ec78f90b30aa79/ruff-0.15.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:761ec0a66680fab6454236635a39abaf14198818c8cdf691e036f4bc0f406b2d", size = 11809643, upload-time = "2026-02-03T17:53:23.031Z" }, + { url = "https://files.pythonhosted.org/packages/dd/45/0ece8db2c474ad7df13af3a6d50f76e22a09d078af63078f005057ca59eb/ruff-0.15.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:940f11c2604d317e797b289f4f9f3fa5555ffe4fb574b55ed006c3d9b6f0eb78", size = 11234787, upload-time = "2026-02-03T17:52:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d9/0e3a81467a120fd265658d127db648e4d3acfe3e4f6f5d4ea79fac47e587/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcbca3d40558789126da91d7ef9a7c87772ee107033db7191edefa34e2c7f1b4", size = 11112797, upload-time = "2026-02-03T17:52:49.274Z" }, + { url = "https://files.pythonhosted.org/packages/b2/cb/8c0b3b0c692683f8ff31351dfb6241047fa873a4481a76df4335a8bff716/ruff-0.15.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9a121a96db1d75fa3eb39c4539e607f628920dd72ff1f7c5ee4f1b768ac62d6e", size = 11033133, upload-time = "2026-02-03T17:53:33.105Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5e/23b87370cf0f9081a8c89a753e69a4e8778805b8802ccfe175cc410e50b9/ruff-0.15.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5298d518e493061f2eabd4abd067c7e4fb89e2f63291c94332e35631c07c3662", size = 10442646, upload-time = "2026-02-03T17:53:06.278Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9a/3c94de5ce642830167e6d00b5c75aacd73e6347b4c7fc6828699b150a5ee/ruff-0.15.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afb6e603d6375ff0d6b0cee563fa21ab570fd15e65c852cb24922cef25050cf1", size = 10195750, upload-time = "2026-02-03T17:53:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/30/15/e396325080d600b436acc970848d69df9c13977942fb62bb8722d729bee8/ruff-0.15.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:77e515f6b15f828b94dc17d2b4ace334c9ddb7d9468c54b2f9ed2b9c1593ef16", size = 10676120, upload-time = "2026-02-03T17:53:09.363Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c9/229a23d52a2983de1ad0fb0ee37d36e0257e6f28bfd6b498ee2c76361874/ruff-0.15.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6f6e80850a01eb13b3e42ee0ebdf6e4497151b48c35051aab51c101266d187a3", size = 11201636, upload-time = "2026-02-03T17:52:57.281Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b0/69adf22f4e24f3677208adb715c578266842e6e6a3cc77483f48dd999ede/ruff-0.15.0-py3-none-win32.whl", hash = "sha256:238a717ef803e501b6d51e0bdd0d2c6e8513fe9eec14002445134d3907cd46c3", size = 10465945, upload-time = "2026-02-03T17:53:12.591Z" }, + { url = "https://files.pythonhosted.org/packages/51/ad/f813b6e2c97e9b4598be25e94a9147b9af7e60523b0cb5d94d307c15229d/ruff-0.15.0-py3-none-win_amd64.whl", hash = "sha256:dd5e4d3301dc01de614da3cdffc33d4b1b96fb89e45721f1598e5532ccf78b18", size = 11564657, upload-time = "2026-02-03T17:52:51.893Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b0/2d823f6e77ebe560f4e397d078487e8d52c1516b331e3521bc75db4272ca/ruff-0.15.0-py3-none-win_arm64.whl", hash = "sha256:c480d632cc0ca3f0727acac8b7d053542d9e114a462a145d0b00e7cd658c515a", size = 10865753, upload-time = "2026-02-03T17:53:03.014Z" }, +] + +[[package]] +name = "setuptools" +version = "82.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "toolz" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +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 = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, +]