Compare commits

...

29 Commits

Author SHA1 Message Date
Jonathon Broughton d41dacf13b Update README with clearer instructions
- Updated the template spreadsheet link.
- Changed steps for rule publishing and automation creation.
- Improved formatting of supported predicates table.
- Added a new support contact method via community forum.
2025-03-01 10:21:17 +00:00
Jonathon Broughton 0e81ac07f0 Merge remote-tracking branch 'origin/main' 2025-02-28 17:35:43 +00:00
Jonathon Broughton 1fa7bcb31a Comprehensive Documentation Update for User and Developer Guides (#59)
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
* Added over the top levels of documentation for future developers

* Update README for Speckle Checker functionality

Expanded the overview of the Speckle Checker, detailing its purpose and how it simplifies validation through spreadsheets. Updated usage instructions to include step-by-step guidance on preparing rule spreadsheets and creating automations. Added sections on rule definition format, supported predicates, and example rules for clarity. Enhanced support information at the end.

* Update developer guide for Checker function

Expanded the developer guide with detailed setup instructions, project overview, and testing procedures. Added sections on test automation environment, integration tests, and TDD workflow for rule development. Included troubleshooting tips and future development ideas to enhance functionality.
2025-02-28 16:05:21 +00:00
Jonathon Broughton 7d3afe4f9f Merge remote-tracking branch 'origin/main' 2025-02-28 15:00:09 +00:00
Jonathon Broughton 66312e1cdd Added over the top levels of documentation for future developers (#58) 2025-02-28 14:59:25 +00:00
Jonathon Broughton 997b1049aa Added over the top levels of documentation for future developers 2025-02-28 14:54:46 +00:00
Jonathon Broughton 38d2073dbb Refactor footgun (#57)
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
- Added traceback import for better error logging.
- Enhanced exception handling to include traceback details.
- Commented out non-integer rule number checks for now.
2025-02-21 13:21:08 +00:00
Jonathon Broughton 091a272185 Add new predicates for value checks (#54)
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
- Removed "matches" and "not equal" predicates.
- Added "not equal to" and "does not contain" predicates.
- Introduced a method to check if a parameter value does not contain a substring.
2025-02-20 18:30:05 +00:00
Jonathon Broughton 0e95f3998a Update rules.py (#53)
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
2025-02-20 17:32:29 +00:00
Jonathon Broughton 05a5383060 Add 'contains' predicate method (#52)
- Introduced a new predicate for checking if a parameter value contains a substring.
- Added the corresponding method to handle the logic and error management.
2025-02-20 17:22:02 +00:00
Jonathon Broughton f3c56a48b5 Rule numbers and metdata validation (#51)
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
* Update Python version and clean up dependencies

- Bumped required Python version from 3.12 to 3.13.
- Removed the lock file for dependency management.
- Cleaned up unnecessary dependency groups in the configuration.

* Refine rule number processing and type conversion

- Updated rule number handling to preserve original identifiers.
- Improved logic for generating missing rule numbers.
- Changed duplicate handling to keep original values intact.
- Adjusted column type conversion to replace NaN with empty strings.

* Refactor message handling in rule processor

- Introduced a new function to format rule messages.
- Replaced direct access to the message with the new formatting function.
- Cleaned up metadata creation for better readability.

* Enhance metadata generation with JSON validation

- Added JSON serialisability check for generated metadata
- Included error handling to log issues during creation
- Updated docstring to clarify function purpose and arguments
2025-02-20 15:41:33 +00:00
Jonathon Broughton a704aded80 Refactor parameter value checks to use float conversion (#49)
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
- Updated methods to convert parameter values to float.
- Removed type checks and exceptions for non-numeric types.
- Simplified return logic for invalid conversions.
2025-02-20 12:23:43 +00:00
Jonathon Broughton 90c5051fc6 Update test_user_entry.py (#44) 2025-02-18 20:37:06 +00:00
Jonathon Broughton ec6bdf3485 Update requirements.txt (#43)
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
2025-02-18 19:15:21 +00:00
Jonathon Broughton ceaa75d40a Update requirements.txt (#41) 2025-02-18 19:12:26 +00:00
renovate[bot] 0566f7d890 Update python Docker tag to v3.13 (#38)
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 19:09:55 +00:00
Jonathon Broughton b431662031 Update rule_processor.py (#39) 2025-02-18 19:08:25 +00:00
renovate[bot] e520d9bc91 Update python Docker tag to v3.13 (#37)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 18:59:18 +00:00
renovate[bot] b6dcfe57df Update dependency httpx to v0.28.1 (#26)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 18:58:52 +00:00
renovate[bot] ba8443ce92 Update dependency pydantic-core to v2.29.0 (#27)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 18:58:36 +00:00
renovate[bot] 0bab18d2f2 Update python Docker tag to v3.13 (#28)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 18:58:21 +00:00
renovate[bot] dffb7ea7ba Update dependency attrs to v25 (#29)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 18:57:59 +00:00
renovate[bot] 4420fd31f4 Update dependency websockets to v15 (#30)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 18:57:42 +00:00
Jonathon Broughton 168a1f517a Robust rule validation and UX (#36)
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
* Add minimum severity and hide skipped options

- Introduced `minimum_severity` to filter reported results.
- Added `hide_skipped` option to control visibility of skipped tests.
- Updated rule application logic to respect new parameters.

* Improve rule processing and validation

- Enhanced rule reading to group rules and handle validation messages.
- Added functions to process and validate rule numbers in the spreadsheet.
- Improved error handling when reading TSV files.

* Improve docstrings and clean up code

- Added docstring for `PropertyMatchMode` enum.
- Removed unnecessary blank lines in `FunctionInputs`.
- Commented out unused `property_match_mode` field with explanation.

* Improve rule validation logic

Added a new function to validate the structure of rule groups. Key updates include:
- Checks for required columns and conditions.
- Ensures rules start with "WHERE" and have at most one "CHECK".
- Validates that "CHECK" is the last condition if present.
- Raises descriptive errors for invalid structures.

* Refactor rule processing logic

- Added a new function to separate filters and final checks.
- Simplified condition evaluation by using the new function.
- Improved handling of empty rule groups and speckle objects.
- Enhanced clarity in filtering logic for better maintainability.

* Refactor number conversion logic in comparison

- Updated boolean comparison method call for clarity.
- Introduced a helper function to safely convert strings to numbers, handling whitespace and negative values.
- Simplified the type-checking process for string-to-float conversion.

* Update comparison logic in parameter checks

Flipped the comparison logic to match user expectations for parameter value checks. Updated docstrings for better clarity on UX perspective. Added error handling for non-numeric values to improve robustness.

* Refactor method names for clarity

Updated method names to remove leading underscores for better readability and consistency. Added a docstring to describe the purpose of the rules module.

* Add tests for comparison and rule processing

- Introduced a new test suite for value comparisons, covering numeric strings, boolean strings, case sensitivity in string comparisons, and float comparison with tolerance.
- Created a test suite for parameter handling functionality to validate the existence and retrieval of parameters in v2 and v3 objects.
- Added tests for rule processing functionality using both explicit CHECK format and legacy format rules.
2025-02-18 18:56:02 +00:00
Jonathon Broughton e49bf225ec Update main.yml (#35) 2025-02-18 09:23:57 +00:00
Jonathon Broughton f3987fced9 Update main.yml (#34) 2025-02-18 08:21:30 +00:00
Jonathon Broughton 1ae3372f42 Update main.yml (#33)
* Update main.yml

* Update Dockerfile
2025-02-18 08:07:09 +00:00
Jonathon Broughton b071380a4f Update main.yml (#32) 2025-02-18 00:07:30 +00:00
Jonathon Broughton 460b21772a Update Dockerfile (#31)
build and deploy Speckle functions / publish-automate-function-version (push) Has been cancelled
2025-02-17 23:57:56 +00:00
19 changed files with 2020 additions and 2369 deletions
+3 -1
View File
@@ -31,7 +31,9 @@ jobs:
- name: Extract functionInputSchema
id: extract_schema
run: |
python main.py generate_schema "${{ env.FUNCTION_SCHEMA_FILE_NAME }}"
python main.py generate_schema "${HOME}/${{ env.FUNCTION_SCHEMA_FILE_NAME }}"
echo "Checking if functionSchema.json exists after generation..."
ls -lah "${HOME}/${{ env.FUNCTION_SCHEMA_FILE_NAME }}"
# Step 5: Build and publish the Speckle function
- name: Speckle Automate Function - Build and Publish
+1 -1
View File
@@ -1 +1 @@
3.12
3.13
+349 -28
View File
@@ -1,56 +1,377 @@
# Checker Function Development Guide
# Checker Function Developer Guide
## Setup
This document provides technical details for developers working on the Speckle Checker Automate function.
1. Install dependencies:
## Project Overview
The Checker function enables validation of Speckle objects against user-defined rules in a spreadsheet. It's designed to
be flexible, supporting various object schemas including both v2 and v3 Speckle APIs.
## Setup Development Environment
### Prerequisites
- Python 3.10+
- Poetry for dependency management
### Installation
1. Clone the repository
2. Install dependencies:
```bash
poetry install
```
3. Activate the virtual environment:
```bash
poetry shell
```
### Test Automation Environment
The project uses Speckle's [Test Automation feature](https://speckle.guide/automate/function-testing.html) to run
integration tests against real Speckle data. This provides a sandboxed environment to validate the function's business
logic without triggering actual automations.
#### Setting Up a Test Automation
1. Navigate to your Speckle project
2. Go to the **Automations** tab
3. Click **New Automation**
4. Select **Create Test Automation** in the bottom left
5. Follow the configuration steps
Note: To create a test automation, you must:
- Be an owner of the Speckle project
- Have published this function to the Function Library
- Have at least one release for the function
#### Environment Configuration
For local integration testing, create a `.env` file in the project root with these variables:
```
# Your Personal Access Token from Speckle
SPECKLE_TOKEN=your_speckle_token
# The Speckle server URL
SPECKLE_SERVER_URL=https://app.speckle.systems
# From the test automation URL: /projects/[project-id]/automations/[automation-id]
SPECKLE_PROJECT_ID=your_project_id
SPECKLE_AUTOMATION_ID=your_automation_id
```
This configuration allows the test suite to:
1. Connect to your test automation via the Speckle API
2. Run the function locally against real Speckle data
3. Submit results to the test automation for validation
For detailed instructions, refer to
the [official documentation on function testing](https://speckle.guide/automate/function-testing.html#how-to-create-a-test-automation).
#### Running Integration Tests
With the `.env` file configured:
```bash
poetry shell && poetry install
# Run the integration tests
pytest test_function.py
```
2. Configure `.env`:
The SDK utilities will automatically:
```
SPECKLE_TOKEN=your_speckle_token
SPECKLE_SERVER_URL=app.speckle.systems
- Connect to your test automation
- Execute your function with the specified test data
- Submit results back to Speckle
Test results will be visible on the automation page in the Speckle UI.
#### Unit Tests
For unit tests that don't require a full Speckle connection, you can run:
```bash
# Run unit tests only
pytest test_comparisons.py test_rule_processing.py
```
Get test automation details from app.speckle.systems
Note: The `.env` file should never be committed to version control (it's included in .gitignore)
## Project Structure
- `function.py`: Main business logic
- `rules.py`: Rule definitions and processing
- `inputs.py`: Function input schema
- `helpers.py`: Utility functions
- `spreadsheet.py`: TSV handling
```
├── main.py # Entry point for Automate
├── src/
│ ├── function.py # Main function logic
│ ├── inputs.py # Function input schema
│ ├── helpers.py # Utility functions
│ ├── filters.py # Object filtering functions
│ ├── rules.py # Rule definitions and property handling
│ ├── predicates.py # Predicate mapping for spreadsheet values
│ ├── rule_processor.py # Rule application and result handling
│ └── spreadsheet.py # TSV file parsing
├── tests/
│ ├── conftest.py # Test fixtures
│ ├── test_function.py # Main function tests
│ ├── test_comparisons.py # Value comparison tests
│ ├── test_parameters.py # Parameter handling tests
│ └── test_rule_processing.py # Rule processing tests
├── pyproject.toml # Project dependencies
└── poetry.lock # Locked dependencies
```
## Core Components
### 1. Function Execution Flow
The main execution flow is defined in `function.py`:
1. `automate_function()` receives context and inputs from Automate
2. Retrieves Speckle objects via `automate_context.receive_version()`
3. Flattens the object tree using `flatten_base()`
4. Loads rules from the spreadsheet URL via `read_rules_from_spreadsheet()`
5. Applies rules to objects using `apply_rules_to_objects()`
6. Reports results via the Automate context
### 2. Rule Processing
Rules are processed through several stages:
1. **Spreadsheet Parsing** (`spreadsheet.py`):
- Reads TSV data
- Groups rules by rule number
- Validates rule structure
2. **Rule Application** (`rule_processor.py`):
- Processes rule logic (WHERE, AND, CHECK)
- Evaluates conditions against objects
- Attaches results to objects in Automate context
3. **Property Rules** (`rules.py`):
- Handles property lookups in objects
- Implements comparison logic
- Supports both v2 and v3 Speckle schemas
### 3. Property Access System
The system uses a flexible property access mechanism that works with different Speckle schemas:
- **V2 Schema**: Properties in `parameters` dictionary with internal definition names
- **V3 Schema**: Properties in nested `properties.Parameters` structure
The `PropertyRules` class provides methods to:
- Find properties by path or name
- Extract values with appropriate type conversion
- Perform comparisons with tolerance and type handling
## Test-Driven Development for Rules
The test infrastructure is designed to support Test-Driven Development (TDD) when creating new rules or extending
functionality. This approach is especially powerful for rule development as it allows you to verify behavior against
known test objects.
### Using Test Fixtures for Rule Development
The `conftest.py` file contains test fixtures that provide sample Speckle objects for testing:
```python
@pytest.fixture
def v2_wall():
"""Creates a v2-style Speckle wall object"""
wall = Base()
wall.id = "cdb18060dc48281909e94f0f1d8d3cc0"
wall.type = "W30(Fc24)"
wall.units = "mm"
wall.family = "Basic Wall"
wall.height = 1400
wall.flipped = False
wall.category = "Walls"
# ... more properties
return wall
@pytest.fixture
def v3_wall():
"""Creates a v3-style Speckle wall object"""
wall = Base()
wall.id = "46f06fef727d64a0bbcbd7ced51e0cd2"
wall.name = "Walls - W30(Fc24)"
wall.type = "W30(Fc24)"
wall.units = "mm"
wall.family = "Basic Wall"
# ... more properties
return wall
```
These fixtures create standardized test objects that represent different Speckle schema versions, allowing you to test
rule behavior consistently.
### TDD Workflow for New Rules
When developing a new rule or predicate, follow this TDD approach:
1. **Add test fixtures**: First, expand `conftest.py` with representative objects that your rule will process
2. **Write tests first**: Create test cases in a test file (e.g., `test_my_rule.py`):
```python
def test_new_wall_rule(v2_wall, v3_wall):
"""Test a new rule that checks wall thickness requirements"""
# Test with v2 schema
assert PropertyRules.is_new_wall_check(v2_wall, "width", "300")
# Test with v3 schema
assert PropertyRules.is_new_wall_check(v3_wall, "Width", "300")
# Test failure case
v2_wall.parameters["WALL_ATTR_WIDTH_PARAM"].value = 200
assert not PropertyRules.is_new_wall_check(v2_wall, "width", "300")
```
3. **Implement the rule**: Add the new rule method to the `PropertyRules` class in `rules.py`:
```python
@staticmethod
def is_new_wall_check(speckle_object: Base, parameter_name: str, expected_value: str) -> bool:
"""Checks if a wall meets specific thickness requirements"""
parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
# Implement rule logic
return result
```
4. **Add to predicate mapping**: Register your new rule in `predicates.py`:
```python
PREDICATE_METHOD_MAP = {
# Existing predicates...
"new_wall_check": PropertyRules.is_new_wall_check.__name__,
}
```
5. **Run tests to verify**:
```bash
pytest test_my_rule.py -v
```
### Creating Comprehensive Test Objects
For the most effective testing, your test objects in `conftest.py` should:
1. **Include diverse objects**: Walls, columns, beams, etc.
2. **Cover edge cases**: Null values, missing properties, special characters
3. **Represent both schemas**: Include both v2 and v3 format objects
4. **Include real-world examples**: Extract sample objects from actual projects
You can extract real objects for testing using:
```python
# Example code to extract and save real objects for test fixtures
from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.transports.server import ServerTransport
client = SpeckleClient(host="app.speckle.systems")
client.authenticate_with_token(token)
transport = ServerTransport(client=client, stream_id="stream_id")
obj = operations.receive("object_id", transport)
# Print structure to help with fixture creation
print(obj.get_member_names())
print(obj.get_dynamic_member_names())
```
By following this TDD approach and maintaining comprehensive test fixtures, you can develop robust rules that work
reliably across different object schemas and handle edge cases appropriately.
## Testing
### Running Tests
```bash
poetry run pytest
# Run all tests
pytest
# Run specific test file
pytest tests/test_parameters.py
# Run with coverage
pytest --cov=src
```
## Extending Rules
### Test Data
1. Add new predicate to `input_predicate_mapping` in `rules.py`
2. Create corresponding method in `PropertyRules` class
3. Update tests
Test fixtures in `conftest.py` provide sample objects:
## Building
- `v2_wall`: Wall object in v2 schema
- `v3_wall`: Wall object in v3 schema
The function is packaged as a Docker container:
### Manual Testing with Real Data
```bash
docker build -f ./Dockerfile -t checker .
```
For testing with real Speckle data:
## Local Testing
```python
from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import get_account_from_token
from specklepy.transports.server import ServerTransport
```bash
docker run --rm checker python -u main.py run [automation_data] [parameters] [token]
client = SpeckleClient(host="app.speckle.systems")
account = get_account_from_token(token, "app.speckle.systems")
client.authenticate_with_account(account)
transport = ServerTransport(client=client, stream_id="your_stream_id")
commit = client.commit.get(stream_id="your_stream_id", commit_id="your_commit_id")
obj = operations.receive(commit.referencedObject, transport)
```
## Deployment
Create a GitHub release to trigger deployment to Speckle Automate.
The function is deployed through GitHub Actions:
1. Create a GitHub release to trigger the build workflow
2. The workflow builds the necessary artifacts and pushes them to the Speckle Automate registry
3. The function becomes available in the Speckle Automate UI
## Performance Considerations
- **Large Object Trees**: When processing large models, use aggressive filtering with WHERE clauses
- **Rule Complexity**: Minimize the number of nested property lookups
- **Memory Usage**: Be aware of object reference handling and avoid deep copies
## Troubleshooting
### Common Issues
1. **Rule not matching expected objects**:
- Check property paths for the specific object type
- Verify data types (strings vs. numbers)
- Enable debug logging
2. **Slow performance**:
- Check for inefficient property lookups
- Add more specific WHERE filters to reduce object set
3. **Docker build failures**:
- Check dependency compatibility
- Verify Python version requirements
## Contributing
1. Create a branch for your feature or fix
2. Add tests for new functionality
3. Update documentation
4. Submit a pull request
5. Ensure CI tests pass
## Future Development
Potential improvements:
- Support for more complex rule logic (OR conditions)
- UI-based rule editor
- Result visualization tools
- Performance optimizations for large models
- Support for referencing other objects in rules
+10 -11
View File
@@ -1,16 +1,15 @@
# We use the official Python 3.11 image as our base image and will add our code to it. For more details, see https://hub.docker.com/_/python
FROM python:3.13-slim
# Use the official Python 3.11 slim image as the base
FROM python:3.11-slim
# We install poetry to generate a list of dependencies which will be required by our application
RUN pip install poetry==1.8.4
# We set the working directory to be the /home/speckle directory; all of our files will be copied here.
# Set the working directory inside the container
WORKDIR /home/speckle
# Copy all of our code and assets from the local directory into the /home/speckle directory of the container.
# We also ensure that the user 'speckle' owns these files, so it can access them
# This assumes that the Dockerfile is in the same directory as the rest of the code
# Copy the application files to the working directory
COPY . /home/speckle
# Using poetry, we generate a list of requirements, save them to requirements.txt, and then use pip to install them
RUN poetry export --format requirements.txt --output /home/speckle/requirements.txt && pip install --requirement /home/speckle/requirements.txt
# Upgrade pip and install dependencies using requirements.txt
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r /home/speckle/requirements.txt
# Set the entrypoint for running the Speckle function
CMD ["python", "-u", "main.py", "run"]
+108 -23
View File
@@ -1,36 +1,121 @@
# Public Function: Checker
# Speckle Checker
Validate Speckle objects against configurable rules using spreadsheet definitions.
Speckle Checker is an Automate function that validates Speckle objects against configurable rules defined in a
spreadsheet. This approach provides a flexible way to implement quality checks without coding, making it accessible to
all team members.
## Usage
## Overview
1. Access the template Google Sheet [link needed]
2. Make a copy to your Google Drive using File > Make a copy
3. Define your rules in your sheet
4. Click "Speckle" menu > "Publish Rules" to get your TSV URL
5. Create an Automation in Speckle Automate using the Checker function
6. Paste your TSV URL into the function configuration
7. Run your automation
The Checker function allows you to:
## Rule Types
- Define validation rules in a spreadsheet
- Configure severity levels for issues
- Check properties across different types of objects
- Generate reports of validation results
- Apply consistent standards across projects
- Property existence
- Value matching
- Numeric comparisons
- Range checks
- List membership
- Pattern matching
- Boolean checks
## Getting Started
## Severity Levels
### 1. Prepare Your Rule Spreadsheet
- WARNING: Issues that should be reviewed
- ERROR: Critical issues requiring attention
1. Access
the [template spreadsheet](https://docs.google.com/spreadsheets/d/1eB0RVuOXdjLyn4_GAPSahV05p1lqfSGQbH8WWijnkkA/edit?gid=0#gid=0)
2. Use the Speckle menu to launch the Speckle sidebar and make a copy.
3. Define your rules using the format explained below
4. Publish your rules by clicking "Publish Rules". Copy the resultant URL.
### 2. Create an Automation
1. Go to your workspace project in [Speckle](https://app.speckle.systems/)
2. Create a new Automation
3. Select the Checker function
4. Configure the function:
- Paste your published rules URL
- Set minimum severity level to report
- Configure other options as needed
5. Save and run your automation
## Rule Definition Format
Rules are defined in a spreadsheet with the following columns:
| Rule Number | Logic | Property Name | Predicate | Value | Message | Report Severity |
|-------------|-------|---------------|--------------|-----------|----------------------|-----------------|
| 1 | WHERE | category | matches | Walls | Wall thickness check | ERROR |
| 1 | AND | Width | greater than | 200 | | |
| 2 | WHERE | category | matches | Columns | Column height check | WARNING |
| 2 | AND | height | in range | 2500,4000 | | |
### Column Explanation
- **Rule Number**: Groups conditions that belong to the same rule
- **Logic**: Defines how conditions are combined (WHERE, AND, CHECK)
- **Property Name**: The object property or parameter to check
- **Predicate**: Comparison operation (equals, greater than, etc.)
- **Value**: Reference value for comparison
- **Message**: Description shown in validation results
- **Report Severity**: ERROR, WARNING, or INFO
### Supported Predicates
| Predicate | Description | Example |
|------------------|-----------------------------|---------------------------------------|
| exists | Checks if a property exists | `height` exists |
| equal to | Exact value match | `width` equal to `300` |
| not equal to | Value doesn't match | `material` not equal to `Concrete` |
| greater than | Value exceeds threshold | `height` greater than `3000` |
| less than | Value below threshold | `thickness` less than `50` |
| in range | Value within bounds | `elevation` in range `0,10000` |
| in list | Value in allowed set | `type` in list `W1,W2,W3` |
| contains | Property contains substring | `name` contains `Beam` |
| does not contain | Property doesn't contain | `name` does not contain `temp` |
| is true | Boolean property is true | `is_structural` is true |
| is false | Boolean property is false | `is_placeholder` is false |
| is like | Loose text matching | `name` is like `Wall` matches `Walls` |
## Rule Logic
- **WHERE**: Filters objects to check (like SELECT WHERE in SQL)
- **AND**: Additional filter conditions
- **CHECK**: Final check condition (optional, defaults to last AND)
Objects pass a rule when they match all conditions. Objects that match WHERE/AND filters but fail the CHECK condition
are reported as issues.
## Working with Object Properties
The Checker understands properties in Speckle objects regardless of schema:
- Direct properties: `category`, `name`, `id`
- Nested properties: `parameters.WIDTH.value`
- Revit parameters: Use parameter names like `Mark`, `Width`, `Assembly Code`
## Example Rules
[Screenshot or example table to be added]
### Wall Thickness Check
```
Rule 1: WHERE category equals "Walls" AND width less than "200"
Message: "Wall too thin - minimum thickness is 200mm"
Severity: ERROR
```
### Door Naming Convention
```
Rule 2: WHERE category equals "Doors" AND name is not like "^D\d{3}$"
Message: "Door name must follow pattern D followed by 3 digits"
Severity: WARNING
```
### Structural Column Height Range
```
Rule 3: WHERE category equals "Columns" AND is_structural is true AND height not in range "2400,4000"
Message: "Structural column height outside acceptable range (2400-4000mm)"
Severity: ERROR
```
## Support
For issues or questions, please open a GitHub issue.
For issues or questions, please let us know on the [Speckle Community Forum](https://speckle.community/).
+2 -7
View File
@@ -4,19 +4,14 @@ version = "0.1.0"
description = "Allows for QAQC property checking with Speckle"
authors = ["Jonathon Broughton <jonathon@speckle.systems>"]
readme = "README.md"
requires-python = ">=3.12"
requires-python = ">=3.13"
dependencies = [
"more-itertools>=10.6.0",
"pandas>=2.2.3",
"pydantic==2.10.6",
"python-dotenv>=1.0.1",
"python-levenshtein>=0.26.1",
"specklepy>=2.21.2",
]
[dependency-groups]
dev = [
"specklepy>=2.21.3",
"pytest-assertcount>=1.0.0",
"black>=25.1.0",
"mypy>=1.15.0",
+89 -24
View File
@@ -1,12 +1,23 @@
# This is the main function that will be executed when the automation is triggered.
# It will receive the inputs from the user, and the context of the run.
# It will then apply the rules to the objects in the model, and report back the results.
"""This is the main entry point for the Speckle Automate function.
The Speckle Automate system works as follows:
1. When a model is committed to Speckle, it triggers automations associated with the project
2. For each automation, Speckle Automate prepares a runtime environment and context
3. The automation context includes the model data and function inputs
4. This function is executed to process the model and provide results
5. Results are attached to objects in the model, creating an annotated view
This function implements a configurable rule-based validation system that:
- Reads validation rules from an external spreadsheet
- Applies these rules to objects in the Speckle model
- Reports validation results back to the Speckle platform
- Provides an annotated view of the model showing validation issues
"""
from pandas import DataFrame
from speckle_automate import AutomationContext
from specklepy.objects.base import Base
from src.helpers import flatten_base
from src.helpers import flatten_base, speckle_print
from src.inputs import FunctionInputs
from src.rule_processor import apply_rules_to_objects
from src.spreadsheet import read_rules_from_spreadsheet
@@ -18,41 +29,95 @@ def automate_function(
automate_context: AutomationContext,
function_inputs: FunctionInputs,
) -> None:
"""This VERSION of the function will add a check for the new provide inputs.
"""Main entry point for the Speckle Automate function.
This function is called by the Speckle Automate system when the automation is triggered.
It orchestrates the entire validation process:
1. Receiving and flattening the model data
2. Detecting the Speckle object schema version
3. Loading and grouping rules from the external spreadsheet
4. Applying rules to objects and collecting results
5. Reporting results back to the Speckle platform
Args:
automate_context: A context helper object, that carries relevant information
about the runtime context of this function.
It gives access to the Speckle project data, that triggered this run.
It also has convenience methods attach result data to the Speckle model.
function_inputs: An instance object matching the defined schema.
automate_context: A context helper provided by Speckle Automate that:
- Provides access to the Speckle model data
- Handles result reporting and view management
- Manages run status (success, failure, exception)
function_inputs: User-provided inputs defined in the FunctionInputs schema,
particularly the URL to the rules spreadsheet
"""
# the context provides a convenient way, to receive the triggering VERSION
# -------------------------------------------------------------------------
# Step 1: Receive and process the model data
# -------------------------------------------------------------------------
# The AutomationContext provides a convenient way to access the model data
# that triggered this automation run
version_root_object: Base = automate_context.receive_version()
# We can continue to work with a flattened list of objects.
# Flatten the object tree into a list of objects
# The Speckle object model is hierarchical, but for validation purposes,
# it's easier to work with a flat list of objects
flat_list_of_objects = list(flatten_base(version_root_object))
# If it is a next_gen model, we can get the VERSION from the root object
# This function's rules don't make use of this check, but it is here for reference if you want to.
# -------------------------------------------------------------------------
# Step 2: Detect Speckle object schema version
# -------------------------------------------------------------------------
# The Speckle object schema has evolved over time
# In newer models, we can detect the version from the root object
# This version information helps our validation logic handle different schemas
global VERSION
VERSION = getattr(version_root_object, "version", 2) # noqa: F841SION = getattr(version_root_object,"version", 2) # noqa: F841 # noqa: F841
# read the rules from the spreadsheet
rules: DataFrame = read_rules_from_spreadsheet(function_inputs.spreadsheet_url)
# In v2, parameters are stored in a 'parameters' dictionary on each object
# In v3, they are nested in 'properties.Parameters' with categorization
speckle_print(f"Detected Speckle object schema version: {VERSION}")
if (rules is None) or (len(rules) == 0):
automate_context.mark_run_exception("No rules defined")
# -------------------------------------------------------------------------
# Step 3: Load and process rules from the spreadsheet
# -------------------------------------------------------------------------
grouped_rules = rules.groupby("Rule Number")
# The rules are defined in an external spreadsheet (TSV format)
# This allows non-technical users to define and modify rules
# without changing the code
grouped_rules, messages = read_rules_from_spreadsheet(function_inputs.spreadsheet_url)
# apply the rules to the objects
apply_rules_to_objects(flat_list_of_objects, grouped_rules, automate_context)
# Handle any validation messages from rule processing
for message in messages:
speckle_print(message) # or log them appropriately
# set the automation context view, to the original model / VERSION view
# If rule processing failed, mark the run as failed and exit
if grouped_rules is None:
automate_context.mark_run_exception("Failed to process rules")
return
# -------------------------------------------------------------------------
# Step 4: Apply rules to objects and collect results
# -------------------------------------------------------------------------
# This is where the actual validation happens
# Each rule is applied to relevant objects, and results are collected
# Results are attached to objects in the model to create an annotated view
apply_rules_to_objects(
flat_list_of_objects,
grouped_rules,
automate_context,
minimum_severity=function_inputs.minimum_severity,
hide_skipped=function_inputs.hide_skipped,
)
# -------------------------------------------------------------------------
# Step 5: Finalize the automation run
# -------------------------------------------------------------------------
# Set the context view to the original model/version view
# This ensures that the results are displayed in the correct context
automate_context.set_context_view()
# report success
# Mark the run as successful and provide a summary message
# This message will be displayed to the user in the Speckle UI
automate_context.mark_run_success(
f"Successfully applied {len(grouped_rules)} rules to {len(flat_list_of_objects)} version {VERSION} objects."
)
+33 -16
View File
@@ -5,9 +5,20 @@ from speckle_automate import AutomateBase
class PropertyMatchMode(Enum):
STRICT = "strict" # Exact parameter path match
FUZZY = "fuzzy" # Search all parameters ignoring hierarchy
MIXED = "mixed" # Exact match first, fuzzy fallback
"""Controls how strictly parameter names must match."""
STRICT = "strict" # Exact parameter path match
FUZZY = "fuzzy" # Search all parameters ignoring hierarchy
MIXED = "mixed" # Exact match first, fuzzy fallback
class MinimumSeverity(str, Enum):
"""Enum for minimum severity level to report."""
INFO = "Info"
WARNING = "Warning"
ERROR = "Error"
class FunctionInputs(AutomateBase):
"""These are function author defined values.
@@ -17,23 +28,29 @@ class FunctionInputs(AutomateBase):
https://docs.pydantic.dev/latest/usage/models/
"""
# In this exercise, we will move rules to an external source so not to hardcode them.
spreadsheet_url: str = Field(
title="Spreadsheet URL",
description="This is the URL of the spreadsheet to check. It should be a TSV format data source.",
)
property_match_mode: PropertyMatchMode = Field(
default=PropertyMatchMode.MIXED,
title="Property Match Mode",
description='Controls how strictly parameter names must match. ' +
'STRICT will only match exact parameter paths, ' +
'FUZZY will search all parameters ignoring hierarchy, ' +
'MIXED will exact match first, fuzzy fallback.'
minimum_severity: MinimumSeverity = Field(
default=MinimumSeverity.INFO,
title="Minimum Severity Level",
description="Only report test results with this severity level or higher. Info will show all results, Warning will show warnings and errors, Error will show only errors.",
)
hide_skipped: bool = Field(
default=False,
title="Hide Skipped Tests",
description="If enabled, tests that were skipped (no matching objects found) will not be reported.",
)
# property_match_mode: PropertyMatchMode = Field(
# default=PropertyMatchMode.MIXED,
# title="Property Match Mode",
# description='Controls how strictly parameter names must match. ' +
# 'STRICT will only match exact parameter paths, ' +
# 'FUZZY will search all parameters ignoring hierarchy, ' +
# 'MIXED will exact match first, fuzzy fallback.'
# )
+3 -2
View File
@@ -5,15 +5,16 @@ from src.rules import PropertyRules
# Mapping of input predicates to the corresponding methods in PropertyRules
PREDICATE_METHOD_MAP = {
"exists": PropertyRules.has_parameter.__name__,
"matches": PropertyRules.is_parameter_value.__name__,
"greater than": PropertyRules.is_parameter_value_greater_than.__name__,
"less than": PropertyRules.is_parameter_value_less_than.__name__,
"in range": PropertyRules.is_parameter_value_in_range.__name__,
"in list": PropertyRules.is_parameter_value_in_list.__name__,
"equal to": PropertyRules.is_equal_value.__name__,
"not equal to": PropertyRules.is_not_equal_value.__name__,
"is true": PropertyRules.is_parameter_value_true.__name__,
"is false": PropertyRules.is_parameter_value_false.__name__,
"is like": PropertyRules.is_parameter_value_like.__name__,
"identical to": PropertyRules.is_identical_value.__name__,
"not equal": PropertyRules.is_not_equal_value.__name__,
"contains": PropertyRules.is_parameter_value_containing.__name__,
"does not contain": PropertyRules.is_parameter_value_not_containing.__name__,
}
+285 -73
View File
@@ -1,3 +1,18 @@
"""Module for processing rules against Speckle objects and updating the automate context with the results.
This module implements the core rule processing logic that:
1. Validates rule structure and logic
2. Evaluates rule conditions against Speckle objects
3. Separates filtering conditions and final check conditions
4. Processes rule groups and tracks results
5. Reports results back to the Speckle Automate context
The rule processing follows a "filter then validate" approach:
- Filter conditions (WHERE, AND) narrow down which objects to check
- The final check condition (CHECK or last AND) determines pass/fail
"""
import json
from enum import Enum
from typing import Any
@@ -7,91 +22,208 @@ from speckle_automate import AutomationContext, ObjectResultLevel
from specklepy.objects.base import Base
from src.helpers import speckle_print
from src.inputs import MinimumSeverity
from src.predicates import PREDICATE_METHOD_MAP
from src.rules import PropertyRules
def validate_rule_structure(rule_group: pd.DataFrame) -> None:
"""Validates the structure and logic of a rule group.
This ensures the rule follows the proper format:
- First condition must be WHERE
- Following conditions can be AND
- Only one CHECK condition is allowed (and must be last)
Args:
rule_group: DataFrame containing the rule conditions
Raises:
ValueError: If rule structure is invalid
"""
if rule_group.empty:
return
# Validate Logic column exists
if "Logic" not in rule_group.columns:
raise ValueError("Rule must have a 'Logic' column")
# Get uppercase Logic values for case-insensitive comparison
logic_values = rule_group["Logic"].str.upper()
# Check if first condition is WHERE
if logic_values.iloc[0] != "WHERE":
raise ValueError(f"Rule {rule_group.iloc[0]['Rule Number']} must start with WHERE")
# Count CHECK conditions
check_count = sum(1 for value in logic_values if value == "CHECK")
if check_count > 1:
raise ValueError(f"Rule {rule_group.iloc[0]['Rule Number']} has multiple CHECK conditions")
# If CHECK exists, ensure it's the last condition
check_indices = logic_values[logic_values == "CHECK"].index
if check_count == 1 and check_indices[0] != rule_group.index[-1]:
raise ValueError(f"CHECK must be the last condition in rule {rule_group.iloc[0]['Rule Number']}")
# Validate Logic values
valid_values = {"WHERE", "AND", "CHECK"}
invalid_values = set(logic_values.unique()) - valid_values
if invalid_values:
raise ValueError(f"Invalid Logic values found: {invalid_values}")
def evaluate_condition(
speckle_object: Base, condition: pd.Series, rule_number: str | None = None, case_number: int | None = None
) -> bool:
"""Given a Speckle object and a condition, evaluates the condition and returns a boolean value.
"""Evaluates a single condition against a Speckle object.
A condition is a pandas Series object with the following keys:
- 'Property Name': The name of the property to evaluate.
- 'Predicate': The predicate to use for evaluation.
- 'Value': The value to compare against.
This function is the bridge between the rules defined in the spreadsheet
and the property checking methods in PropertyRules. It:
1. Extracts the property name, predicate, and value from the condition
2. Maps the predicate to the corresponding method in PropertyRules
3. Calls the method with the object, property name, and value
Args:
rule_number (string): For information the rule number.
case_number (int): For information the rule clause number.
speckle_object (Base): The Speckle object to evaluate.
condition (pd.Series): The condition to evaluate.
speckle_object: The Speckle object to evaluate against
condition: A pandas Series containing the condition details
- 'Property Name': The name of the property to check
- 'Predicate': The comparison operation (like 'equals', 'greater than')
- 'Value': The value to compare against
rule_number: For tracking, the rule number being evaluated
case_number: For tracking, the condition number within the rule
Returns:
bool: The result of the evaluation. True if the condition is met, False otherwise.
True if the condition is met, False otherwise
"""
property_name = condition["Property Name"]
predicate_key = condition["Predicate"]
value = condition["Value"]
# Debugging info
_ = rule_number
_ = case_number
# Look up the method name in the predicate map
# This map connects spreadsheet predicates to PropertyRules methods
if predicate_key in PREDICATE_METHOD_MAP:
method_name = PREDICATE_METHOD_MAP[predicate_key]
method = getattr(PropertyRules, method_name, None)
if method:
check_answer = method(speckle_object, property_name, value)
# Call the method with the object, property name, and value
return method(speckle_object, property_name, value)
return check_answer
return False
def get_filters_and_check(rule_group: pd.DataFrame) -> tuple[pd.DataFrame, pd.Series]:
"""Separates rule conditions into filtering conditions and the final check condition.
This function handles two rule formats:
1. Explicit format: WHERE + AND... + CHECK
2. Legacy format: WHERE + AND... (last AND is implicitly the check)
This separation enables the "filter then validate" approach.
Args:
rule_group: DataFrame containing rule conditions
Returns:
Tuple containing (filter_conditions, final_check_condition)
"""
if rule_group.empty:
return pd.DataFrame(), pd.Series()
# Get uppercase Logic values for case-insensitive comparison
logic_values = rule_group["Logic"].str.upper()
# Look for explicit CHECK
check_conditions = rule_group[logic_values == "CHECK"]
has_explicit_check = not check_conditions.empty
if has_explicit_check:
# Use first CHECK condition as final check
final_check = check_conditions.iloc[0]
# All other conditions are filters
filters = rule_group[logic_values != "CHECK"]
else:
# Legacy behavior: use last AND as check if present
and_conditions = rule_group[logic_values == "AND"]
if not and_conditions.empty:
# Get the last AND as the check
final_check = and_conditions.iloc[-1]
# All conditions up to the last AND are filters
last_and_idx = and_conditions.index[-1]
filters = rule_group[rule_group.index < last_and_idx]
else:
# No AND conditions found, just use WHERE as filter
filters = rule_group
final_check = rule_group.iloc[0] # Default to first condition as check
return filters, final_check
def process_rule(
speckle_objects: list[Base], rule_group: pd.DataFrame
) -> tuple[list[Any], list[Any]] | tuple[list[Base], list[Base]]:
"""Processes a set of rules against Speckle objects, returning those that pass and fail.
"""Processes a rule group against a list of Speckle objects.
The first rule is used as a filter ('WHERE'), and subsequent rules as conditions ('AND').
This function implements the "filter then validate" approach:
1. Apply filter conditions sequentially to narrow down objects
2. Apply the final check condition to determine pass/fail
This approach is efficient for large models as it reduces the number
of objects that need full validation.
Args:
speckle_objects: List of Speckle objects to be processed.
rule_group: DataFrame defining the filter and conditions.
speckle_objects: List of Speckle objects to be processed
rule_group: DataFrame defining the filter and check conditions
Returns:
A tuple of lists containing objects that passed and failed the rule.
A tuple of lists (pass_objects, fail_objects)
"""
# Extract the 'WHERE' condition and subsequent 'AND' conditions
filter_condition = rule_group.iloc[0]
subsequent_conditions = rule_group.iloc[1:]
# get the last row of the rule_group and get the Message and Report Severity
rule_info = rule_group.iloc[-1]
rule_number = rule_info["Rule Number"]
# Filter objects based on the 'WHERE' condition
filtered_objects = [
speckle_object for speckle_object in speckle_objects if evaluate_condition(speckle_object, filter_condition)
]
if not filtered_objects or len(list(filtered_objects)) == 0:
if not speckle_objects or rule_group.empty:
return [], []
# Initialize lists for passed and failed objects
pass_objects, fail_objects = [], []
try:
validate_rule_structure(rule_group)
except ValueError as e:
speckle_print(f"Rule validation error: {str(e)}")
return [], []
# Evaluate each filtered object against the 'AND' conditions
for speckle_object in filtered_objects:
if all(
evaluate_condition(
speckle_object=speckle_object, condition=condition, rule_number=rule_number, case_number=index
# Get filters and final check
filters, final_check = get_filters_and_check(rule_group)
# Start with all objects
filtered_objects = speckle_objects.copy()
rule_number = rule_group.iloc[0]["Rule Number"]
# Apply each filter condition sequentially
for index, (_, filter_condition) in enumerate(filters.iterrows()):
filtered_objects = [
obj
for obj in filtered_objects
if evaluate_condition(
speckle_object=obj, condition=filter_condition, rule_number=rule_number, case_number=index
)
for index, condition in subsequent_conditions.iterrows()
]
# Early exit if no objects pass filters
if not filtered_objects:
return [], []
# For remaining objects, evaluate the final check
# This separates objects into pass/fail groups
pass_objects = []
fail_objects = []
for obj in filtered_objects:
if evaluate_condition(
speckle_object=obj, condition=final_check, rule_number=rule_number, case_number=len(filters)
):
pass_objects.append(speckle_object)
pass_objects.append(obj)
else:
fail_objects.append(speckle_object)
fail_objects.append(obj)
return pass_objects, fail_objects
@@ -100,21 +232,34 @@ def apply_rules_to_objects(
speckle_objects: list[Base],
grouped_rules: DataFrameGroupBy,
automate_context: AutomationContext,
minimum_severity: MinimumSeverity = MinimumSeverity.INFO,
hide_skipped: bool = False,
) -> dict[str, tuple[list[Base], list[Base]]]:
"""Applies defined rules to a list of objects and updates the automate context based on the results.
"""Applies defined rules to a list of objects and updates the automate context with the results.
This is the main orchestration function that:
1. Processes each rule group against all objects
2. Filters results based on severity levels
3. Attaches results to objects in the Speckle Automate context
4. Reports skipped rules (where no objects matched filters)
Args:
speckle_objects (List[Base]): The list of objects to which rules are applied.
grouped_rules (pd.DataFrameGroupBy): The DataFrame containing rule definitions.
automate_context (Any): Context manager for attaching rule results.
speckle_objects: The list of objects to which rules are applied
grouped_rules: The rules grouped by rule number
automate_context: Context manager for attaching results to objects
minimum_severity: Minimum severity level to report
hide_skipped: Whether to hide skipped rules in results
Returns:
Dictionary mapping rule IDs to (pass_objects, fail_objects) tuples
"""
grouped_results = {}
rules_processed = 0
severity_levels = {MinimumSeverity.INFO: 0, MinimumSeverity.WARNING: 1, MinimumSeverity.ERROR: 2}
min_severity_level = severity_levels[minimum_severity]
for rule_id, rule_group in grouped_rules:
rule_id_str = str(rule_id) # Convert rule_id to string
rules_processed += 1
# Ensure rule_group has necessary columns
@@ -123,17 +268,25 @@ def apply_rules_to_objects(
pass_objects, fail_objects = process_rule(speckle_objects, rule_group)
attach_results(pass_objects, rule_group.iloc[-1], rule_id_str, automate_context, True)
attach_results(fail_objects, rule_group.iloc[-1], rule_id_str, automate_context, False)
# Get the severity level for this rule
rule_severity = get_severity(rule_group.iloc[-1])
rule_severity_level = severity_levels[MinimumSeverity(rule_severity.value)]
if len(pass_objects) == 0 and len(fail_objects) == 0:
# For passing objects, only attach if we're showing all levels (INFO)
if minimum_severity == MinimumSeverity.INFO:
attach_results(pass_objects, rule_group.iloc[-1], rule_id_str, automate_context, True)
# For failing objects, attach if they meet minimum severity threshold
if rule_severity_level >= min_severity_level:
attach_results(fail_objects, rule_group.iloc[-1], rule_id_str, automate_context, False)
if len(pass_objects) == 0 and len(fail_objects) == 0 and not hide_skipped:
automate_context.attach_info_to_objects(
category=f"Rule {rule_id_str} Skipped",
object_ids=["0"], # This is a hack to get a rule to report with no valid objects
message=f"No objects found for rule {rule_id_str}",
metadata={},
)
# pass
grouped_results[rule_id_str] = (pass_objects, fail_objects)
@@ -142,7 +295,13 @@ def apply_rules_to_objects(
class SeverityLevel(Enum):
"""Enum for severity levels."""
"""Enumeration for severity levels of rule results.
These severity levels determine how rule failures are displayed:
- INFO: Informational, no action required
- WARNING: Potential issue that should be reviewed
- ERROR: Critical issue requiring attention
"""
INFO = "Info"
WARNING = "Warning"
@@ -150,13 +309,19 @@ class SeverityLevel(Enum):
def get_severity(rule_info: pd.Series) -> SeverityLevel:
"""Convert a string severity level to the corresponding SeverityLevel enum.
"""Convert a string severity level from the spreadsheet to the corresponding SeverityLevel enum.
This function normalizes input strings (because processing user entered dead is hard), handling:
This function normalizes user input with robust handling for:
- Case insensitivity (e.g., "info", "WARNING""Info", "Warning")
- Shorthand mappings (e.g., "WARN""Warning")
- Stripping whitespace
- Defaults to SeverityLevel.ERROR if the input is invalid
- Whitespace handling
- Default fallback to ERROR for invalid input
Args:
rule_info: Series containing rule information with 'Report Severity' key
Returns:
Appropriate SeverityLevel enum value
"""
severity = rule_info.get("Report Severity") # Extract severity from input data
@@ -184,15 +349,39 @@ def get_severity(rule_info: pd.Series) -> SeverityLevel:
def get_metadata(
rule_id: str, rule_info: pd.Series, passed: bool, speckle_objects: list[Base]
) -> dict[str, str | int | Any]:
"""Function that generates metadata with severity validation."""
metadata = {
"rule_id": rule_id,
"status": "PASS" if passed else "FAIL",
"severity": get_severity(rule_info).value, # Keep proper casing
"rule_message": rule_info["Message"],
"object_count": len(speckle_objects),
}
return metadata
"""Generates structured metadata for rule results.
This metadata is attached to objects in the Speckle platform and is:
1. Validated for JSON serializability
2. Structured for consistent representation
3. Includes key information about the rule and results
Args:
rule_id: Identifier for the rule
rule_info: Series containing rule information
passed: Boolean indicating if the rule passed
speckle_objects: List of Speckle objects affected
Returns:
Dictionary containing metadata if valid JSON serializable, empty dict otherwise
"""
try:
metadata = {
"rule_id": rule_id,
"status": "PASS" if passed else "FAIL",
"severity": get_severity(rule_info).value,
"rule_message": format_message(rule_info),
"object_count": len(speckle_objects),
}
# Validate JSON serializability
json.dumps(metadata)
return metadata
except (TypeError, ValueError, json.JSONDecodeError) as e:
# Log the error for debugging purposes
print(f"Error creating metadata: {str(e)}")
return {}
def attach_results(
@@ -202,14 +391,19 @@ def attach_results(
context: AutomationContext,
passed: bool,
) -> None:
"""Attaches the results of a rule to the objects in the context.
"""Attaches rule results to objects in the Speckle Automate context.
This function is the interface to the Speckle platform for reporting results:
- For failing objects, attaches results with appropriate severity levels
- For passing objects, attaches informational results
- Includes structured metadata for consistent reporting
Args:
speckle_objects (List[Base]): The list of objects to which the rule was applied.
rule_info (pd.Series): The information about the rule.
rule_id (str): The ID of the rule.
context (AutomationContext): The context manager for attaching results.
passed (bool): Whether the rule passed or failed.
speckle_objects: The list of objects affected by the rule
rule_info: Information about the rule
rule_id: Identifier for the rule
context: The Speckle Automate context for result attachment
passed: Whether the objects passed the rule
"""
if not speckle_objects:
return
@@ -217,8 +411,7 @@ def attach_results(
# Create structured metadata for onward data analysis uses
metadata = get_metadata(rule_id, rule_info, passed, speckle_objects)
message = f"{rule_info['Message']}"
message = format_message(rule_info)
if not passed:
speckle_print(rule_info["Report Severity"])
@@ -242,3 +435,22 @@ def attach_results(
message=message,
metadata=metadata,
)
def format_message(rule_info):
"""Format the message for the rule result.
Handles cases where the message might be None or NaN.
Args:
rule_info: Series containing rule information with 'Message' key
Returns:
Formatted message string
"""
message = (
str(rule_info["Message"])
if rule_info["Message"] is not None and not pd.isna(rule_info["Message"])
else "No Message"
)
return message
+441 -54
View File
@@ -1,3 +1,16 @@
"""A collection of rules for processing Speckle objects and their properties.
This module provides essential utilities for:
1. Accessing and comparing properties across different Speckle object versions (v2/v3)
2. Handling nested property paths with a flexible search mechanism
3. Converting between different value types (strings, booleans, numbers)
4. Implementing various comparison predicates for validation rules
The core challenge addressed by this module is the evolving schema of Speckle objects.
In v2, parameters were stored directly in a 'parameters' dictionary, while in v3,
they are nested within a more complex 'properties.Parameters' structure with categories.
"""
import math
import re
from typing import Any
@@ -9,13 +22,30 @@ PRIMITIVE_TYPES = (bool, int, float, str, type(None))
class Rules:
"""A collection of rules for processing properties in Speckle objects."""
"""A collection of rules for processing properties in Speckle objects.
This class provides utilities for working with displayable objects
in the Speckle ecosystem.
"""
@staticmethod
def try_get_display_value(
speckle_object: Base,
) -> list[Base] | None:
"""Try fetching the display value from a Speckle object."""
"""Try fetching the display value from a Speckle object.
Speckle objects might store display geometry in various ways:
- 'displayValue' (newer versions)
- '@displayValue' (older versions)
This method handles both cases transparently.
Args:
speckle_object: The Speckle object to extract display value from
Returns:
List of Base objects representing display geometry, or None if not found
"""
raw_display_value = getattr(speckle_object, "displayValue", None) or getattr(
speckle_object, "@displayValue", None
)
@@ -32,7 +62,21 @@ class Rules:
@staticmethod
def is_displayable_object(speckle_object: Base) -> bool:
"""Determines if a given Speckle object is displayable."""
"""Determines if a given Speckle object is displayable.
A Speckle object is considered displayable if:
1. It has an ID and displayable geometry, OR
2. It has a definition with an ID and displayable geometry
(typically for instanced objects)
This is useful for filtering out non-visible/utility objects.
Args:
speckle_object: The Speckle object to check
Returns:
True if the object is displayable, False otherwise
"""
display_values = Rules.try_get_display_value(speckle_object)
if display_values and getattr(speckle_object, "id", None) is not None:
return True
@@ -47,7 +91,17 @@ class Rules:
@staticmethod
def get_displayable_objects(flat_list_of_objects: list[Base]) -> list[Base]:
"""Filters a list of Speckle objects to only include displayable objects."""
"""Filters a list of Speckle objects to only include displayable objects.
This is useful when processing a flattened object tree but only wanting
to work with objects that have visual representation.
Args:
flat_list_of_objects: A list of Speckle objects to filter
Returns:
A filtered list containing only displayable objects with IDs
"""
return [
speckle_object
for speckle_object in flat_list_of_objects
@@ -56,18 +110,99 @@ class Rules:
class PropertyRules:
"""A collection of rules for processing parameters in Speckle objects."""
"""A collection of rules for processing parameters in Speckle objects.
This class provides the core functionality for:
- Locating properties in complex object hierarchies
- Converting between different value types
- Comparing values with appropriate type handling
- Implementing various comparison predicates for validation rules
It's designed to work with both Speckle v2 and v3 object schemas.
"""
@staticmethod
def is_parameter_value_not_containing(speckle_object: Base, parameter_name: str, substring: str) -> bool:
"""Checks if parameter value does not contain the given substring.
This is the logical inverse of is_parameter_value_containing.
Args:
speckle_object: The Speckle object to check
parameter_name: Name of the parameter to check
substring: The substring to look for
Returns:
True if the parameter value does not contain the substring
"""
# Invert the result of contains check
return not PropertyRules.is_parameter_value_containing(speckle_object, parameter_name, substring)
@staticmethod
def is_parameter_value_containing(speckle_object: Base, parameter_name: str, substring: str) -> bool:
"""Checks if parameter value contains the given substring.
Case-insensitive substring matching for parameters.
If the parameter doesn't exist, returns False.
Args:
speckle_object: The Speckle object to check
parameter_name: Name of the parameter to check
substring: The substring to look for
Returns:
True if the parameter value contains the substring
"""
parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
if parameter_value is None:
return False
# Convert both to strings for comparison
try:
parameter_str = str(parameter_value).lower()
substring_str = str(substring).lower()
return substring_str in parameter_str
except (TypeError, ValueError) as e:
print(f"Error in is_parameter_value_contains: {e}")
return False
@staticmethod
def normalize_path(path: str) -> str:
"""Remove technical path prefixes like 'properties' and 'parameters'."""
"""Remove technical path prefixes like 'properties' and 'parameters'.
This helps make property paths version-agnostic by focusing on the
meaningful parts of the path rather than the container structure.
Examples:
- 'properties.Parameters.Type Parameters.Construction.Width' becomes 'Type Parameters.Construction.Width'
- 'parameters.WALL_ATTR_WIDTH_PARAM' becomes 'WALL_ATTR_WIDTH_PARAM'
Args:
path: The parameter path to normalize
Returns:
A normalized path with technical prefixes removed
"""
parts = path.split(".")
filtered = [p for p in parts if p.lower() not in ("properties", "parameters")]
return ".".join(filtered)
@staticmethod
def convert_revit_boolean(value: Any) -> Any:
"""Convert Revit-style Yes/No strings to boolean values."""
"""Convert Revit-style Yes/No strings to boolean values.
Revit and some other BIM applications use "Yes"/"No" strings
instead of boolean values. This function converts them:
- "Yes" → True
- "No" → False
- Other values remain unchanged
Args:
value: The value to potentially convert
Returns:
Converted boolean if applicable, otherwise original value
"""
# Handle None case
if value is None:
return None
@@ -89,7 +224,20 @@ class PropertyRules:
@staticmethod
def get_obj_value(obj: Any, get_raw: bool = False) -> Any:
"""Extract appropriate value from an object, handling special cases."""
"""Extract appropriate value from an object, handling special cases.
This function handles the various ways values might be stored:
- In v2 Parameter objects (with .value property)
- In v3 dictionary structures (with 'value' key)
- As primitive values directly
Args:
obj: The object to extract value from
get_raw: If True, return the object itself without extracting value
Returns:
The extracted value, possibly with Yes/No conversion
"""
if get_raw:
return obj
@@ -113,7 +261,19 @@ class PropertyRules:
@staticmethod
def search_obj(obj: Any, parts: list[str]) -> tuple[bool, Any]:
"""Recursively search an object following a path."""
"""Recursively search an object following a path.
This is a key part of the property access mechanism, allowing
navigation through nested object structures using dot notation.
The search is case-insensitive to handle inconsistencies.
Args:
obj: The object to search within
parts: List of path components to follow
Returns:
Tuple of (found: bool, value: Any)
"""
if not parts:
return True, obj
@@ -142,6 +302,14 @@ class PropertyRules:
def find_property(root: Any, search_path: str, get_raw: bool = False) -> tuple[bool, Any]:
"""Find a property by searching through nested objects.
This method implements a flexible property search that:
1. First attempts a direct path match
2. Then recursively searches through nested object structures
3. Uses cycle detection to prevent infinite recursion
The approach handles both v2 and v3 Speckle object schemas and
supports fuzzy property matching by normalizing paths.
Args:
root: The root object to search
search_path: Path to the property to find
@@ -199,7 +367,17 @@ class PropertyRules:
@staticmethod
def has_parameter(speckle_object: Base, parameter_name: str, *_args, **_kwargs) -> bool:
"""Check if a parameter exists in the Speckle object."""
"""Check if a parameter exists in the Speckle object.
This method is version-agnostic and works with both v2 and v3 objects.
Args:
speckle_object: The Speckle object to check
parameter_name: The parameter name/path to look for
Returns:
True if parameter exists, False otherwise
"""
found, _ = PropertyRules.find_property(speckle_object, parameter_name)
return found
@@ -210,29 +388,58 @@ class PropertyRules:
default_value: Any = None,
get_raw: bool = False,
) -> Any:
"""Get a parameter value from the Speckle object using strict path matching.
"""Get a parameter value from the Speckle object using path matching.
This is the core property access method that:
1. Handles both v2 and v3 object structures
2. Supports direct and nested property paths
3. Applies appropriate value extraction and conversion
Args:
speckle_object: The Speckle object to search
parameter_name: Exact parameter path to find
parameter_name: Parameter path to find
default_value: Value to return if parameter not found
get_raw: Whether to return raw values without conversion
Returns:
The parameter value if found using exact path matching, otherwise default_value
The parameter value if found, otherwise default_value
"""
found, value = PropertyRules.find_property(speckle_object, parameter_name, get_raw)
return value if found else default_value
@staticmethod
def is_parameter_value(speckle_object: Base, parameter_name: str, value_to_match: Any) -> bool:
"""Checks if the value of the specified parameter matches the given value."""
"""Checks if the value of the specified parameter matches the given value.
This is a basic equality check that leverages the parameter access system.
Args:
speckle_object: The Speckle object to check
parameter_name: The parameter name/path to check
value_to_match: The value to compare against
Returns:
True if values match, False otherwise
"""
parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
return parameter_value == value_to_match
@staticmethod
def parse_number_from_string(input_string: str):
"""Attempts to parse a number from a string."""
"""Attempts to parse a number from a string.
First tries to parse as integer, then as float if that fails.
Raises ValueError if the string is not a valid number.
Args:
input_string: The string to parse
Returns:
int or float value
Raises:
ValueError: If the string is not a valid number
"""
try:
return int(input_string)
except ValueError:
@@ -243,27 +450,77 @@ class PropertyRules:
@staticmethod
def is_parameter_value_greater_than(speckle_object: Base, parameter_name: str, threshold: str) -> bool:
"""Checks if parameter value is greater than threshold."""
"""Checks if parameter value is greater than threshold.
This implements the 'greater than' predicate for numeric comparisons.
Note: From a UX perspective, if someone writes 'height greater than 2401',
they mean "flag an error if height <= 2401". So we implement the check to match
that intuitive interpretation.
Args:
speckle_object: The Speckle object to check
parameter_name: The parameter name/path to check
threshold: The threshold value as a string
Returns:
True if parameter value > threshold, False otherwise
"""
parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
if parameter_value is None:
return False
if not isinstance(parameter_value, int | float):
raise ValueError(f"Parameter value must be a number, got {type(parameter_value)}")
try:
parameter_value = float(parameter_value)
except (ValueError, TypeError):
return False # Return False if the value cannot be converted to a number
return parameter_value > PropertyRules.parse_number_from_string(threshold)
@staticmethod
def is_parameter_value_less_than(speckle_object: Base, parameter_name: str, threshold: str) -> bool:
"""Checks if parameter value is less than threshold."""
"""Checks if parameter value is less than threshold.
This implements the 'less than' predicate for numeric comparisons.
Note: From a UX perspective, if someone writes 'height less than 2401',
they mean "flag an error if height >= 2401". So we implement the check to match
that intuitive interpretation.
Args:
speckle_object: The Speckle object to check
parameter_name: The parameter name/path to check
threshold: The threshold value as a string
Returns:
True if parameter value < threshold, False otherwise
"""
parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
if parameter_value is None:
return False
if not isinstance(parameter_value, int | float):
raise ValueError(f"Parameter value must be a number, got {type(parameter_value)}")
try:
parameter_value = float(parameter_value)
except (ValueError, TypeError):
return False # Return False if the value cannot be converted to a number
return parameter_value < PropertyRules.parse_number_from_string(threshold)
@staticmethod
def is_parameter_value_in_range(speckle_object: Base, parameter_name: str, value_range: str) -> bool:
"""Checks if parameter value falls within range."""
"""Checks if parameter value falls within specified range.
This implements the 'in range' predicate for numeric comparisons.
The range is specified as "min,max" and is inclusive.
Note: From a UX perspective, if someone writes 'height in range 2401,3000',
they mean "flag an error if height < 2401 or height > 3000".
Args:
speckle_object: The Speckle object to check
parameter_name: The parameter name/path to check
value_range: Range specification as "min,max"
Returns:
True if min <= parameter value <= max, False otherwise
"""
min_value, max_value = value_range.split(",")
min_value = PropertyRules.parse_number_from_string(min_value)
max_value = PropertyRules.parse_number_from_string(max_value)
@@ -271,8 +528,10 @@ class PropertyRules:
parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
if parameter_value is None:
return False
if not isinstance(parameter_value, int | float):
raise ValueError(f"Parameter value must be a number, got {type(parameter_value)}")
try:
parameter_value = float(parameter_value)
except (ValueError, TypeError):
return False # Return False if the value cannot be converted to a number
return min_value <= parameter_value <= max_value
@@ -284,7 +543,22 @@ class PropertyRules:
fuzzy: bool = False,
threshold: float = 0.8,
) -> bool:
"""Checks if parameter value matches pattern."""
"""Checks if parameter value matches pattern.
This implements the 'is like' predicate with two modes:
1. Regular expression matching (fuzzy=False)
2. Levenshtein distance-based fuzzy matching (fuzzy=True)
Args:
speckle_object: The Speckle object to check
parameter_name: The parameter name/path to check
pattern: Regex pattern or string to match
fuzzy: Whether to use fuzzy matching
threshold: Similarity threshold for fuzzy matching (0.0-1.0)
Returns:
True if the parameter value matches the pattern, False otherwise
"""
parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
if parameter_value is None:
return False
@@ -297,7 +571,20 @@ class PropertyRules:
@staticmethod
def is_parameter_value_in_list(speckle_object: Base, parameter_name: str, value_list: list[Any] | str) -> bool:
"""Checks if parameter value is in list."""
"""Checks if parameter value is in list.
This implements the 'in list' predicate, supporting both:
1. Python lists
2. Comma-separated string lists
Args:
speckle_object: The Speckle object to check
parameter_name: The parameter name/path to check
value_list: List of values or comma-separated string
Returns:
True if parameter value is in the list, False otherwise
"""
parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
if isinstance(value_list, str):
@@ -311,8 +598,20 @@ class PropertyRules:
return is_value_in_list(parameter_value, value_list)
@staticmethod
def _check_boolean_value(value: Any, values_to_match: tuple[str, ...]) -> bool:
"""Check if a value matches any target value in expected format."""
def check_boolean_value(value: Any, values_to_match: tuple[str, ...]) -> bool:
"""Check if a value matches any target value in expected format.
This is a helper for boolean parameter checking that handles:
- Boolean literals (True/False)
- String representations ("yes", "true", "1", etc.)
Args:
value: The value to check
values_to_match: Tuple of string values representing the target state
Returns:
True if value matches any target value, False otherwise
"""
if isinstance(value, bool):
return value is (True if "true" in values_to_match else False)
@@ -323,35 +622,103 @@ class PropertyRules:
@staticmethod
def is_parameter_value_true(speckle_object: Base, parameter_name: str) -> bool:
"""Check if parameter value represents true."""
"""Check if parameter value represents true.
This implements the 'is true' predicate, handling various
representations of true values ("yes", "true", "1").
Args:
speckle_object: The Speckle object to check
parameter_name: The parameter name/path to check
Returns:
True if parameter value represents true, False otherwise
"""
parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
return PropertyRules._check_boolean_value(parameter_value, ("yes", "true", "1"))
return PropertyRules.check_boolean_value(parameter_value, ("yes", "true", "1"))
@staticmethod
def is_parameter_value_false(speckle_object: Base, parameter_name: str) -> bool:
"""Check if parameter value represents false."""
"""Check if parameter value represents false.
This implements the 'is false' predicate, handling various
representations of false values ("no", "false", "0").
Args:
speckle_object: The Speckle object to check
parameter_name: The parameter name/path to check
Returns:
True if parameter value represents false, False otherwise
"""
parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
return PropertyRules._check_boolean_value(parameter_value, ("no", "false", "0"))
return PropertyRules.check_boolean_value(parameter_value, ("no", "false", "0"))
@staticmethod
def has_category(speckle_object: Base) -> bool:
"""Check if object has category."""
"""Check if object has category.
This is a convenience method specifically for checking
the existence of the 'category' property.
Args:
speckle_object: The Speckle object to check
Returns:
True if object has a category property, False otherwise
"""
return PropertyRules.has_parameter(speckle_object, "category")
@staticmethod
def is_category(speckle_object: Base, category_input: str) -> bool:
"""Check if object matches category."""
"""Check if object matches category.
This is a convenience method for filtering objects by category,
which is a common operation in Speckle.
Args:
speckle_object: The Speckle object to check
category_input: The category value to match
Returns:
True if object's category matches input, False otherwise
"""
category_value = PropertyRules.get_parameter_value(speckle_object, "category")
return category_value == category_input
@staticmethod
def get_category_value(speckle_object: Base) -> str:
"""Get object's category value."""
"""Get object's category value.
This is a convenience method for retrieving an object's category.
Args:
speckle_object: The Speckle object to get category from
Returns:
The category value as a string
"""
return PropertyRules.get_parameter_value(speckle_object, "category")
@staticmethod
def _try_boolean_comparison(value1: Any, value2: Any, allow_yes_no: bool) -> tuple[bool, bool]:
"""Attempts to compare two values as booleans."""
def try_boolean_comparison(value1: Any, value2: Any, allow_yes_no: bool) -> tuple[bool, bool]:
"""Attempts to compare two values as booleans.
This handles various boolean representations:
- Boolean literals (True/False)
- String representations ("true"/"false")
- Revit-style "Yes"/"No" strings (if allow_yes_no=True)
Args:
value1: First value to compare
value2: Second value to compare
allow_yes_no: Whether to convert Yes/No strings to booleans
Returns:
Tuple of (can_compare: bool, result: bool) where:
- can_compare indicates if both values could be interpreted as booleans
- result is the comparison result if can_compare is True
"""
def strict_convert_boolean(value: Any) -> Any:
"""Convert 'True'/'False' strings to booleans, and use `convert_revit_boolean` for Yes/No."""
@@ -380,7 +747,7 @@ class PropertyRules:
return False, False
@staticmethod
def _compare_values(
def compare_values(
value1: Any,
value2: Any,
case_sensitive: bool = False,
@@ -390,26 +757,41 @@ class PropertyRules:
) -> bool:
"""Core logic for comparing two values with type handling and tolerance.
This is the comprehensive value comparison function that:
1. Tries boolean comparison first
2. Handles numeric string conversion
3. Implements case sensitivity options for strings
4. Uses tolerance-based floating point comparison
5. Falls back to regular equality
This function is used by multiple predicates.
Args:
value1: First value to compare
value2: Second value to compare
case_sensitive: Whether to perform case-sensitive string comparison
tolerance: Tolerance for floating point comparisons
allow_yes_no_bools: Whether to convert Yes/No strings to booleans when comparing with boolean values
allow_yes_no_bools: Whether to convert Yes/No strings to booleans
use_exact: Whether to use exact equality for numeric comparisons
Returns:
bool: True if values are considered equal, False otherwise
True if values are considered equal, False otherwise
"""
# Try boolean comparison first
can_compare, result = PropertyRules._try_boolean_comparison(value1, value2, allow_yes_no_bools)
can_compare, result = PropertyRules.try_boolean_comparison(value1, value2, allow_yes_no_bools)
if can_compare:
return result
# Handle case where one value is a string that can be interpreted as a number
if isinstance(value1, str) and value1.replace(".", "", 1).isdigit():
value1 = float(value1)
if isinstance(value2, str) and value2.replace(".", "", 1).isdigit():
value2 = float(value2)
def safe_convert_to_number(val):
if isinstance(val, str):
val = val.strip() # Remove whitespace
if val.replace(".", "", 1).replace("-", "", 1).isdigit(): # Handle negative numbers
return float(val)
return val
value1 = safe_convert_to_number(value1)
value2 = safe_convert_to_number(value2)
# For strings: Allow case insensitivity if specified
if isinstance(value1, str) and isinstance(value2, str):
@@ -436,21 +818,26 @@ class PropertyRules:
) -> bool:
"""Compares a parameter value from a Speckle object with the provided value.
This implements the 'equal to' predicate with flexible comparison rules:
- Case insensitivity option for strings
- Tolerance-based comparison for floating point numbers
- Type conversion for common scenarios (numeric strings, Yes/No)
Args:
speckle_object (Base): The Speckle object containing the parameter
parameter_name (str): Name of the parameter to compare
value_to_match: The value to compare against (float, string, int, etc.)
case_sensitive (bool): Whether to perform case-sensitive comparison for strings
tolerance (float): Tolerance for floating point comparisons
speckle_object: The Speckle object containing the parameter
parameter_name: Name of the parameter to compare
value_to_match: The value to compare against
case_sensitive: Whether to perform case-sensitive comparison for strings
tolerance: Tolerance for floating point comparisons
Returns:
bool: True if values are considered equal, False otherwise
True if values are considered equal, False otherwise
"""
parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
if parameter_value is None:
return False
return PropertyRules._compare_values(
return PropertyRules.compare_values(
parameter_value, value_to_match, case_sensitive, tolerance, allow_yes_no_bools=True
)
@@ -478,7 +865,7 @@ class PropertyRules:
if parameter_value is None:
return True # Non-existent parameters are considered not equal
return not PropertyRules._compare_values(
return not PropertyRules.compare_values(
parameter_value, value_to_match, case_sensitive, tolerance, allow_yes_no_bools=True
)
@@ -500,6 +887,6 @@ class PropertyRules:
if parameter_value is None:
return False
return PropertyRules._compare_values(
return PropertyRules.compare_values(
parameter_value, value_to_match, case_sensitive=True, tolerance=0, allow_yes_no_bools=False, use_exact=True
)
-756
View File
@@ -1,756 +0,0 @@
# import re
# from typing import Any
#
# from Levenshtein import ratio
# from specklepy.objects.base import Base
#
# from src.helpers import get_item, has_item, speckle_print
# from src.inputs import PropertyMatchMode
# We're going to define a set of rules that will allow us to filter and
# process parameters in our Speckle objects. These rules will be encapsulated
# in a class called `ParameterRules`.
# class Rules:
# """A collection of rules for processing properties in Speckle objects.
#
# Simple rules can be straightforwardly implemented as static methods that
# return boolean value to be used either as a filter or a condition.
# These can then be abstracted into returning lambda functions that we can
# use in our main processing logic. By encapsulating these rules, we can easily
# extend or modify them in the future.
# """
#
# @staticmethod
# def try_get_display_value(
# speckle_object: Base,
# ) -> list[Base] | None:
# """Try fetching the display value from a Speckle object.
#
# This method encapsulates the logic for attempting to retrieve the display value from a
# Speckle object. It returns a list containing the display values if found,
# otherwise it returns None.
#
# Args:
# speckle_object (Base): The Speckle object to extract the display value from.
#
# Returns:
# Optional[List[Base]]: A list containing the display values.
# If no display value is found, returns None.
# """
# # Attempt to get the display value from the speckle_object
# raw_display_value = getattr(speckle_object, "displayValue", None) or getattr(
# speckle_object, "@displayValue", None
# )
#
# # If no display value found, return None
# if raw_display_value is None:
# return None
#
# # If display value found, filter out non-Base objects
# display_values = [value for value in raw_display_value if isinstance(value, Base)]
#
# # If no valid display values found, return None
# if not display_values:
# return None
#
# return display_values
#
# @staticmethod
# def is_displayable_object(speckle_object: Base) -> bool:
# """Determines if a given Speckle object is displayable.
#
# This method encapsulates the logic for determining if a Speckle object is displayable.
# It checks if the speckle_object has a display value and returns True if it does, otherwise it returns False.
#
# Args:
# speckle_object (Base): The Speckle object to check.
#
# Returns:
# bool: True if the object has a display value, False otherwise.
# """
# # Check for direct displayable state using try_get_display_value
# display_values = Rules.try_get_display_value(speckle_object)
# if display_values and getattr(speckle_object, "id", None) is not None:
# return True
#
# # Check for displayable state via definition, using try_get_display_value on the definition object
# definition = getattr(speckle_object, "definition", None)
# if definition:
# definition_display_values = Rules.try_get_display_value(definition)
# if definition_display_values and getattr(definition, "id", None) is not None:
# return True
#
# return False
#
# @staticmethod
# def get_displayable_objects(flat_list_of_objects: list[Base]) -> list[Base]:
# """Filters a list of Speckle objects to only include displayable objects.
#
# This function takes a list of Speckle objects and filters out the objects that are displayable.
# It returns a list containing only the displayable objects.
#
# Args:
# flat_list_of_objects (List[Base]): The list of Speckle objects to filter.
# """
# return [
# speckle_object
# for speckle_object in flat_list_of_objects
# if Rules.is_displayable_object(speckle_object) and getattr(speckle_object, "id", None)
# ]
#
#
# class PropertyRules:
# """A collection of rules for processing Revit parameters in Speckle objects."""
#
# @staticmethod
# def has_parameter(speckle_object: Base, parameter_name: str, *_args, **_kwargs) -> bool:
# """Checks if the speckle_object has a parameter with the given name."""
# found, _ = ParameterSearch.lookup_parameter(speckle_object, parameter_name)
# return found
#
# @staticmethod
# def get_parameter_value(
# speckle_object: Base,
# parameter_name: str,
# match_mode: PropertyMatchMode = PropertyMatchMode.MIXED,
# default_value: Any = None,
# ) -> Any:
# """Gets the value of a parameter if it exists."""
# found, value = ParameterSearch.lookup_parameter(speckle_object, parameter_name, match_mode)
# return value if found else default_value
#
# @staticmethod
# def is_v3(speckle_object: Base) -> bool:
# """Determines if a Speckle object uses v3 parameter structure.
#
# Args:
# speckle_object (Base): The Speckle object to check
#
# Returns:
# bool: True if object uses v3 structure, False otherwise
# """
# properties = get_item(speckle_object, "properties")
# return bool(properties and has_item(properties, "Parameters"))
#
# # @staticmethod
# # def has_parameter(speckle_object: Base, parameter_name: str, *_args, **_kwargs) -> bool:
# # """Checks if the speckle_object has a Revit parameter with the given name.
# #
# # First checks direct properties, then determines if it's a v2 or v3 object structure
# # and searches in the appropriate parameter hierarchy.
# #
# # Args:
# # speckle_object (Base): The Speckle object to check.
# # parameter_name (str): The name of the parameter to check for.
# # *_args: Extra positional arguments which are ignored.
# # **_kwargs: Extra keyword arguments which are ignored.
# #
# # Returns:
# # bool: True if the object has the parameter, False otherwise.
# # """
# # # Check direct property first regardless of version
# # if has_item(speckle_object, parameter_name):
# # return True
# #
# # if PropertyRules.is_v3(speckle_object):
# # properties = get_item(speckle_object, "properties")
# # parameters = get_item(properties, "Parameters")
# # if parameters:
# #
# # def search_v3_params(params: dict, search_name: str) -> bool:
# # for key, value in params.items():
# # if isinstance(value, dict):
# # # Check direct name match
# # if key.lower() == search_name.lower():
# # return True
# # # Check nested parameters
# # if search_v3_params(value, search_name):
# # return True
# # return False
# #
# # return search_v3_params(parameters, parameter_name)
# # else:
# # # Handle v2 structure
# # parameters = get_item(speckle_object, "parameters")
# # if not parameters:
# # return False
# #
# # # Check direct parameter name match
# # if has_item(parameters, parameter_name):
# # return True
# #
# # # Check nested parameters with name property
# # def check_nested_name(value: Any) -> bool:
# # if isinstance(value, dict):
# # return get_item(value, "name") == parameter_name
# # return get_item(value, "name") == parameter_name if hasattr(value, "name") else False
# #
# # return any(check_nested_name(param_value) for param_value in parameters.values() if param_value is not None)
# #
# # return False
# #
# # @staticmethod
# # def get_parameter_value(
# # speckle_object: Base,
# # parameter_name: str,
# # match_mode: PropertyMatchMode = PropertyMatchMode.MIXED,
# # default_value: Any = None,
# # ) -> Any | None:
# # """Retrieves the value of the specified parameter from the speckle_object.
# #
# # First checks direct properties, then determines if it's a v2 or v3 object structure
# # and retrieves from the appropriate parameter hierarchy.
# #
# # Args:
# # speckle_object (Base): The Speckle object to retrieve the parameter value from.
# # parameter_name (str): The name of the parameter to retrieve the value for.
# # match_mode (PropertyMatchMode): The matching mode to use for parameter lookup
# # default_value: The default value to return if parameter not found.
# #
# # Returns:
# # The value of the parameter if found, else default_value.
# # """
# # # Check direct property first regardless of version
# # if has_item(speckle_object, parameter_name):
# # value = get_item(speckle_object, parameter_name)
# # return value if value is not None else default_value
# #
# # if PropertyRules.is_v3(speckle_object):
# # return PropertyRules.get_v3_parameter(speckle_object, parameter_name, match_mode, default_value)
# # else:
# # return PropertyRules.get_v2_parameter(speckle_object, parameter_name, match_mode, default_value)
#
# # @staticmethod
# # def get_v2_parameter(obj: Base, name: str, mode: PropertyMatchMode, default: Any) -> Any:
# # """Get parameter value from v2 Speckle object structure.
# #
# # Args:
# # obj: Speckle object to get parameter from
# # name: Parameter name to retrieve
# # mode: Match mode for parameter lookup
# # default: Default value if parameter not found
# #
# # Returns:
# # Parameter value if found, else default
# # """
# # parameters = get_item(obj, "parameters")
# # if not parameters:
# # return default
# #
# # if mode == PropertyMatchMode.STRICT:
# # return PropertyRules.strict_parameter_lookup(name, parameters, default)
# #
# # def search_params(param_dict: dict, search_name: str, fuzzy: bool) -> Any:
# # for key, value in param_dict.items():
# # key_match = (key.lower() == search_name.lower()) or (fuzzy and search_name.lower() in key.lower())
# # if key_match:
# # # Handle both direct values and nested parameter objects
# # return get_item(value, "value", value)
# # return None
# #
# # result = search_params(parameters, name, mode == PropertyMatchMode.FUZZY)
# # return result if result is not None else default
# #
# # @staticmethod
# # def get_v3_parameter(obj: Base, name: str, mode: PropertyMatchMode, default: Any) -> Any:
# # """Get parameter value from v3 Speckle object structure.
# #
# # Args:
# # obj: Speckle object to get parameter from
# # name: Parameter name to retrieve
# # mode: Match mode for parameter lookup
# # default: Default value if parameter not found
# #
# # Returns:
# # Parameter value if found, else default
# # """
# # properties = get_item(obj, "properties")
# # if not properties or not has_item(properties, "Parameters"):
# # return default
# #
# # parameters = get_item(properties, "Parameters")
# # if not parameters:
# # return default
# #
# # if mode == PropertyMatchMode.STRICT:
# # return PropertyRules.strict_parameter_lookup(name, parameters, default)
# #
# # def search_nested(data: dict, search_name: str, fuzzy: bool) -> Any:
# # for nested_key, value in data.items():
# # if isinstance(value, dict):
# # key_match = (nested_key.lower() == search_name.lower()) or (
# # fuzzy and search_name.lower() in nested_key.lower()
# # )
# #
# # if key_match and has_item(value, "value"):
# # return get_item(value, "value")
# #
# # nested_result = search_nested(value, search_name, fuzzy)
# # if nested_result is not None:
# # return nested_result
# # return None
# #
# # result = search_nested(parameters, name, mode == PropertyMatchMode.FUZZY)
# # return result if result is not None else default
# #
# # @staticmethod
# # def strict_parameter_lookup(name: str, parameters: dict, default: Any) -> Any:
# # """Perform strict parameter lookup following exact path.
# #
# # Args:
# # name: Parameter path (dot separated)
# # parameters: Parameters dictionary
# # default: Default value if not found
# #
# # Returns:
# # Parameter value if found, else default
# # """
# # path_parts = name.split(".")
# # current = parameters
# #
# # for part in path_parts:
# # if not current or not isinstance(current, dict):
# # return default
# #
# # # Find exact case-insensitive match
# # key = next((k for k in current.keys() if k.lower() == part.lower()), None)
# # if not key:
# # return default
# #
# # current = get_item(current, key)
# #
# # # Handle both direct values and parameter objects
# # if isinstance(current, dict):
# # return get_item(current, "value", current)
# # return current
#
# @staticmethod
# def is_parameter_value(speckle_object: Base, parameter_name: str, value_to_match: Any) -> bool:
# """Checks if the value of the specified parameter matches the given value.
#
# Args:
# speckle_object (Base): The Speckle object to check.
# parameter_name (str): The name of the parameter to check.
# value_to_match (Any): The value to match against.
#
# Returns:
# bool: True if the parameter value matches the given value, False otherwise.
# """
# parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
# return parameter_value == value_to_match
#
# @staticmethod
# def is_parameter_value_like(
# speckle_object: Base,
# parameter_name: str,
# pattern: str,
# fuzzy: bool = False,
# threshold: float = 0.8,
# ) -> bool:
# """Checks if the value of the specified parameter matches the given pattern.
#
# Args:
# speckle_object (Base): The Speckle object to check.
# parameter_name (str): The name of the parameter to check.
# pattern (str): The pattern to match against.
# fuzzy (bool): If True, performs fuzzy matching using Levenshtein distance.
# If False (default), performs exact pattern matching using regular expressions.
# threshold (float): The similarity threshold for fuzzy matching (default: 0.8).
# Only applicable when fuzzy=True.
#
# Returns:
# bool: True if the parameter value matches the pattern (exact or fuzzy), False otherwise.
# """
# parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
# if parameter_value is None:
# return False
#
# if fuzzy:
# similarity = ratio(str(parameter_value), pattern)
# return similarity >= threshold
# else:
# return bool(re.match(pattern, str(parameter_value)))
#
# @staticmethod
# def parse_number_from_string(input_string: str):
# """Attempts to parse an integer or float from a given string.
#
# Args:
# input_string (str): The string containing the number to be parsed.
#
# Returns:
# int or float: The parsed number, or raises ValueError if parsing is not possible.
# """
# try:
# # First try to convert it to an integer
# return int(input_string)
# except ValueError:
# # If it fails to convert to an integer, try to convert to a float
# try:
# return float(input_string)
# except ValueError:
# # Raise an error if neither conversion is possible
# raise ValueError("Input string is not a valid integer or float")
#
# @staticmethod
# def is_parameter_value_greater_than(speckle_object: Base, parameter_name: str, threshold: str) -> bool:
# """Checks if the value of the specified parameter is greater than the given threshold.
#
# Args:
# speckle_object (Base): The Speckle object to check.
# parameter_name (str): The name of the parameter to check.
# threshold (Union[int, float]): The threshold value to compare against.
#
# Returns:
# bool: True if the parameter value is greater than the threshold, False otherwise.
# """
# parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
# if parameter_value is None:
# return False
#
# if not isinstance(parameter_value, int | float):
# raise ValueError(f"Parameter value must be a number, got {type(parameter_value)}")
# return parameter_value > PropertyRules.parse_number_from_string(threshold)
#
# @staticmethod
# def is_parameter_value_less_than(speckle_object: Base, parameter_name: str, threshold: str) -> bool:
# """Checks if the value of the specified parameter is less than the given threshold.
#
# Args:
# speckle_object (Base): The Speckle object to check.
# parameter_name (str): The name of the parameter to check.
# threshold (Union[int, float]): The threshold value to compare against.
#
# Returns:
# bool: True if the parameter value is less than the threshold, False otherwise.
# """
# parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
# if parameter_value is None:
# return False
# if not isinstance(parameter_value, int | float):
# raise ValueError(f"Parameter value must be a number, got {type(parameter_value)}")
# return parameter_value < PropertyRules.parse_number_from_string(threshold)
#
# @staticmethod
# def is_parameter_value_in_range(speckle_object: Base, parameter_name: str, value_range: str) -> bool:
# """Checks if the value of the specified parameter falls within the given range.
#
# Args:
# speckle_object (Base): The Speckle object to check.
# parameter_name (str): The name of the parameter to check.
# value_range (str): The range to check against, in the format "min_value, max_value".
#
# Returns:
# bool: True if the parameter value falls within the range (inclusive), False otherwise.
# """
# min_value, max_value = value_range.split(",")
# min_value = PropertyRules.parse_number_from_string(min_value)
# max_value = PropertyRules.parse_number_from_string(max_value)
#
# parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
# if parameter_value is None:
# return False
# if not isinstance(parameter_value, int | float):
# raise ValueError(f"Parameter value must be a number, got {type(parameter_value)}")
#
# return min_value <= parameter_value <= max_value
#
# @staticmethod
# def is_parameter_value_in_range_expanded(
# speckle_object: Base,
# parameter_name: str,
# min_value: int | float,
# max_value: int | float,
# inclusive: bool = True,
# ) -> bool:
# """Checks if the value of the specified parameter falls within the given range.
#
# Args:
# speckle_object (Base): The Speckle object to check.
# parameter_name (str): The name of the parameter to check.
# min_value (Union[int, float]): The minimum value of the range.
# max_value (Union[int, float]): The maximum value of the range.
# inclusive (bool): If True (default), the range is inclusive (min <= value <= max).
# If False, the range is exclusive (min < value < max).
#
# Returns:
# bool: True if the parameter value falls within the range (inclusive), False otherwise.
# """
# parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
# if parameter_value is None:
# return False
# if not isinstance(parameter_value, int | float):
# raise ValueError(f"Parameter value must be a number, got {type(parameter_value)}")
#
# return min_value <= parameter_value <= max_value if inclusive else min_value < parameter_value < max_value
#
# @staticmethod
# def is_parameter_value_in_list(speckle_object: Base, parameter_name: str, value_list: list[Any] | str) -> bool:
# """Checks if the value of the specified parameter is present in the given list of values.
#
# Args:
# speckle_object (Base): The Speckle object to check.
# parameter_name (str): The name of the parameter to check.
# value_list (List[Any]): The list of values to check against.
#
# Returns:
# bool: True if the parameter value is found in the list, False otherwise.
# """
# parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
#
# if isinstance(value_list, str):
# value_list = [value.strip() for value in value_list.split(",")]
#
# # parameter_value is effectively Any type, so to find its value in the value_list
# def is_value_in_list(value: Any, my_list: Any) -> bool:
# # Ensure that my_list is actually a list
# if isinstance(my_list, list):
# return value in my_list or str(value) in my_list
# else:
# speckle_print(f"Expected a list, got {type(my_list)} instead.")
# return False
#
# return is_value_in_list(parameter_value, value_list)
#
# @staticmethod
# def _check_boolean_value(value: Any, values_to_match: tuple[str, ...]) -> bool:
# """Check if a value matches any target value in expected format."""
# if isinstance(value, bool):
# return value is (True if "true" in values_to_match else False)
#
# if isinstance(value, str):
# return value.lower() in values_to_match
#
# return False
#
# @staticmethod
# def is_parameter_value_true(speckle_object: Base, parameter_name: str) -> bool:
# """Check if parameter value represents true (boolean True, 'yes', 'true', '1')."""
# parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
# return PropertyRules._check_boolean_value(parameter_value, ("yes", "true", "1"))
#
# @staticmethod
# def is_parameter_value_false(speckle_object: Base, parameter_name: str) -> bool:
# """Check if parameter value represents false (boolean False, 'no', 'false', '0')."""
# parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
# return PropertyRules._check_boolean_value(parameter_value, ("no", "false", "0"))
#
# @staticmethod
# def has_category(speckle_object: Base) -> bool:
# """Checks if the speckle_object has a 'category' parameter.
#
# This method checks if the speckle_object has a 'category' parameter.
# If the 'category' parameter exists, it returns True; otherwise, it returns False.
#
# Args:
# speckle_object (Base): The Speckle object to check.
#
# Returns:
# bool: True if the object has the 'category' parameter, False otherwise.
# """
# return PropertyRules.has_parameter(speckle_object, "category")
#
# @staticmethod
# def is_category(speckle_object: Base, category_input: str) -> bool:
# """Checks if the value of the 'category' property matches the given input.
#
# This method checks if the 'category' property of the speckle_object
# matches the given category_input. If they match, it returns True;
# otherwise, it returns False.
#
# Args:
# speckle_object (Base): The Speckle object to check.
# category_input (str): The category value to compare against.
#
# Returns:
# bool: True if the 'category' property matches the input, False otherwise.
# """
# category_value = PropertyRules.get_parameter_value(speckle_object, "category")
# return category_value == category_input
#
# @staticmethod
# def get_category_value(speckle_object: Base) -> str:
# """Retrieves the value of the 'category' parameter from the speckle_object.
#
# This method retrieves the value of the 'category' parameter from the speckle_object.
# If the 'category' parameter exists and its value is not None, it returns the value.
# If the 'category' parameter does not exist or its value is None, it returns an empty string.
#
# Args:
# speckle_object (Base): The Speckle object to retrieve the 'category' parameter value from.
#
# Returns:
# str: The value of the 'category' parameter if it exists and is not None, or an empty string otherwise.
# """
# return PropertyRules.get_parameter_value(speckle_object, "category")
#
#
# class ParameterSearch:
# """Unified parameter search functionality for Speckle objects."""
#
# @staticmethod
# def convert_revit_boolean(value: Any) -> Any:
# """Convert Revit-style Yes/No strings to boolean values.
#
# Args:
# value: The value to potentially convert
#
# Returns:
# bool if value is a Revit boolean string, original value otherwise
# """
# if isinstance(value, str):
# if value.lower() == "yes":
# return True
# if value.lower() == "no":
# return False
# return value
#
# @staticmethod
# def search_parameters(
# params: dict, search_name: str, mode: PropertyMatchMode = PropertyMatchMode.STRICT
# ) -> tuple[bool, Any]:
# """Search for parameters using consistent matching logic.
#
# Supports flexible property chain matching that can find paths like "Instance Parameters.Dimensions.Length"
# within longer chains like "properties.Parameters.Instance Parameters.Dimensions.Length.value".
# Uses STRICT matching by default for more predictable results.
#
# Args:
# params: Parameter dictionary to search
# search_name: Name of parameter to find, can be dot-separated chain
# mode: Matching mode to use (STRICT by default, or FUZZY/MIXED for looser matching)
#
# Returns:
# Tuple of (value_found: bool, value: Any)
# """
#
# def matches_name(match_key: str, target: str, match_mode: PropertyMatchMode) -> bool:
# if match_mode == PropertyMatchMode.STRICT:
# return match_key.lower() == target.lower()
# elif match_mode == PropertyMatchMode.FUZZY:
# return target.lower() in match_key.lower()
# else: # MIXED mode
# return match_key.lower() == target.lower() or target.lower() in match_key.lower()
#
# def try_get_value(obj: Any) -> Any:
# """Extract value from parameter object or return as is.
#
# Handles both dict and Base objects, checking for 'value' property in both cases.
# Returns the 'value' if found, otherwise returns the original object.
# """
# # Handle dictionary objects
# if isinstance(obj, dict):
# return obj.get("value", obj)
#
# # Handle Base objects
# if isinstance(obj, Base):
# return getattr(obj, "value", obj)
#
# # For all other types, return as is
# return obj
#
# # First try property chain lookup
# if "." in search_name:
# search_parts = search_name.split(".")
#
# def try_match_path(current: dict, remaining_search_parts: list[str], depth: int = 0) -> tuple[bool, Any]:
# if not isinstance(current, dict):
# return False, None
#
# if not remaining_search_parts: # We've matched all parts
# return True, try_get_value(current)
#
# current_search = remaining_search_parts[0]
#
# # Try each key at current level
# for key, item_value in current.items():
# if matches_name(key, current_search, mode):
# # Found a match for current part, recurse with rest
# match_found, result = try_match_path(item_value, remaining_search_parts[1:], depth + 1)
# if match_found:
# return True, result
#
# # If no match found and value is a dict, try searching deeper
# if isinstance(item_value, dict):
# match_found, result = try_match_path(item_value, remaining_search_parts, depth)
# if match_found:
# return True, result
#
# return False, None
#
# try:
# found, value = try_match_path(params, search_parts)
# if found:
# return True, value
# except Exception:
# pass # Fall through to recursive search if chain lookup fails
#
# # Recursive search through nested dictionaries
# def recursive_search(data: dict | Base, target: str) -> tuple[bool, Any]:
# if not isinstance(data, dict | Base):
# return False, None
#
# # Handle both dict and Base objects for iteration
# if isinstance(data, dict):
# items = data.items()
# else:
# items = [(k, getattr(data, k)) for k in dir(data) if not k.startswith("_")]
#
# # First check current level
# for key, item_value in items:
# if matches_name(key, target, mode):
# return True, try_get_value(item_value)
#
# # Then check nested levels
# for _, item_value in items:
# if isinstance(item_value, dict | Base):
# item_found, result = recursive_search(item_value, target)
# if item_found:
# return True, result
#
# return False, None
#
# return recursive_search(params, search_name.split(".")[-1] if "." in search_name else search_name)
#
# @staticmethod
# def lookup_parameter(
# obj: Base, param_name: str, mode: PropertyMatchMode = PropertyMatchMode.MIXED
# ) -> tuple[bool, Any]:
# """Unified parameter lookup for both checking existence and getting values.
#
# Args:
# obj: Speckle object to search
# param_name: Parameter name to find
# mode: Matching mode to use
#
# Returns:
# Tuple of (found: bool, value: Any)
# """
# # Check direct property first
# if has_item(obj, param_name):
# value = get_item(obj, param_name)
# # Check if the direct property has a value field
# if isinstance(value, dict) and "value" in value:
# return True, value["value"]
# return True, value
#
# # Handle v3 structure
# if PropertyRules.is_v3(obj):
# properties = get_item(obj, "properties")
# if not properties or not has_item(properties, "Parameters"):
# return False, None
#
# parameters = get_item(properties, "Parameters")
# if not parameters:
# return False, None
#
# return ParameterSearch.search_parameters(parameters, param_name, mode)
#
# # Handle v2 structure
# parameters = get_item(obj, "parameters")
# if not parameters:
# return False, None
#
# return ParameterSearch.search_parameters(parameters, param_name, mode)
+186 -21
View File
@@ -1,38 +1,203 @@
"""Module for reading and processing rules from a cloud hosted TSV file.
This module handles the loading and processing of validation rules from external
spreadsheet data, enabling non-technical users to define and modify rules.
Key features:
1. Reading from hosted TSV files (e.g., from Google Sheets)
2. Processing rule numbers for consistent grouping
3. Handling mixed data types in spreadsheet columns
4. Validating rule structure and providing feedback
5. Grouping related rule conditions for execution
The spreadsheet format used follows a specific structure:
- Rule Number: Groups related conditions together
- Logic: WHERE/AND/CHECK to define condition relationships
- Property Name: The property path to check
- Predicate: The comparison operation (equals, greater than, etc.)
- Value: The value to compare against
- Message: The message to display for rule results
- Severity: INFO/WARNING/ERROR level for failures
"""
import traceback
import pandas as pd
from pandas import DataFrame
from pandas.core.groupby import DataFrameGroupBy
def read_rules_from_spreadsheet(url: str) -> DataFrame | None:
"""Reads a TSV file from a provided URL and returns a DataFrame.
def process_rule_numbers(df: DataFrame) -> DataFrame:
"""Process rule numbers in a DataFrame while preserving original rule identifiers.
This function handles various rule numbering scenarios:
1. Preserves existing rule numbers exactly as provided
2. Generates sequential numbers for missing rule numbers
3. Ensures all rows in a logical rule group have the same rule number
This is important because rule numbers determine how conditions are grouped
and executed together.
Args:
url (str): The URL to the TSV file.
df: DataFrame with columns including 'Rule Number' and 'Logic'
Returns:
DataFrame: Pandas DataFrame containing the TSV data.
DataFrame with processed rule numbers, where all related conditions
have the same rule number
"""
try:
# Since the output is a TSV, we use `pd.read_csv` with `sep='\t'` to specify tab-separated values.
df = pd.read_csv(url, sep="\t")
df = convert_mixed_columns(df)
# Create a copy to avoid modifying original
df = df.copy()
# Convert columns to appropriate types based on their content.
return df
# Initialize tracking variables
used_rule_nums = set()
processed_rule_nums = []
next_auto_num = 1 # For generating missing rule numbers only
except Exception as e:
print(f"Failed to read the TSV from the URL: {e}")
return None
# Find indices where Logic is 'WHERE' to identify rule group starts
where_indices = df[df["Logic"].str.upper() == "WHERE"].index
# Process each group
for i in range(len(where_indices)):
start_idx = where_indices[i]
end_idx = where_indices[i + 1] if i + 1 < len(where_indices) else len(df)
def convert_mixed_columns(df):
"""Converts columns in a DataFrame to appropriate types based on their content.
# Get slice of rows for this group
group_slice = df.iloc[start_idx:end_idx]
Args:
df (DataFrame): The DataFrame whose columns are to be converted.
# Try to get rule number from first row
group_rule_num = group_slice["Rule Number"].iloc[0]
Returns:
DataFrame: The DataFrame with columns converted to appropriate types.
"""
df = df.apply(lambda c: c.astype(object) if any(str(x).replace(".", "", 1).isdigit() for x in c) else c.astype(str))
if pd.isna(group_rule_num):
# If no rule number, generate next available number
while str(next_auto_num) in used_rule_nums:
next_auto_num += 1
group_rule_num = str(next_auto_num)
next_auto_num += 1
else:
# Keep the original rule number exactly as is
group_rule_num = str(group_rule_num)
# Update tracking
used_rule_nums.add(group_rule_num)
# Fill rule numbers for this group
processed_rule_nums.extend([group_rule_num] * len(group_slice))
# Update DataFrame with processed rule numbers
df["Rule Number"] = processed_rule_nums
return df
def validate_rule_numbers(df: DataFrame) -> list[str]:
""" "
Validate rule numbers and return any warnings or errors.
This checks for issues like:
1. Missing rule numbers
2. Non-integer rule numbers
3. Duplicate rule numbers
These validations help ensure rule integrity without being overly strict,
allowing for different user approaches to rule numbering.
Args:
df: DataFrame with processed rule numbers
Returns:
List of warning/error messages
"""
messages = []
# Check for missing rule numbers
if df["Rule Number"].isna().any():
messages.append("Warning: Some rules are missing rule numbers")
# # Check for non-integer rule numbers
# non_int_mask = df["Rule Number"].apply(lambda x: not pd.isna(x) and not float(x).is_integer())
# if non_int_mask.any():
# messages.append("Warning: Some rule numbers are not integers")
# Check for duplicate rule numbers in WHERE rows
where_rules = df[df["Logic"].str.upper() == "WHERE"]["Rule Number"]
duplicates = where_rules[where_rules.duplicated()]
if not duplicates.empty:
messages.append(f"Warning: Duplicate rule numbers found: {list(duplicates)}")
return messages
def read_rules_from_spreadsheet(url: str) -> tuple[DataFrameGroupBy, list[str]] | tuple[None, list[str]]:
""" "
Reads rules from a TSV file at the provided URL, processes them, and returns grouped rules.
This function is the main entry point for rule loading:
1. Reads the TSV file from the provided URL
2. Converts mixed type columns to appropriate types
3. Processes rule numbers for consistent grouping
4. Validates rule numbers and collects messages
5. Groups rules by rule number for execution
Args:
url: The URL to the TSV file containing rule definitions
Returns:
Tuple containing:
- DataFrameGroupBy object with rules grouped by rule number (or None if error)
- List of validation messages/warnings
"""
try:
# Read the TSV file
# The TSV format is chosen for compatibility with Google Sheets
# and other spreadsheet applications
df = pd.read_csv(url, sep="\t")
# Convert mixed type columns
# This handles inconsistencies in spreadsheet data
df = convert_mixed_columns(df)
# Process rule numbers
# This ensures all related conditions have the same rule number
df = process_rule_numbers(df)
# Get validation messages
# These are warnings about potential issues with the rules
messages = validate_rule_numbers(df)
# Group by rule number
# This creates a DataFrameGroupBy object that groups related conditions
grouped_rules = df.groupby("Rule Number")
return grouped_rules, messages
except Exception as e:
# Handle any errors in reading or processing the spreadsheet
traceback.print_exc()
return None, [f"Failed to read the TSV from the URL: {str(e)}:{e.with_traceback(None)}"]
def convert_mixed_columns(df: DataFrame) -> DataFrame:
"""Converts columns in a DataFrame to appropriate types based on their content.
This handles common issues with spreadsheet data:
1. Numeric columns that contain strings
2. Mixed type columns
3. Empty cells and NaN values
The approach is to convert each column appropriately:
- Numeric columns remain as numbers
- Other columns are converted to strings, with empty strings for missing values
Args:
df: The DataFrame whose columns are to be converted
Returns:
DataFrame with columns converted to appropriate types
"""
df = df.apply(
lambda c: c
if c.dropna().apply(lambda x: str(x).replace(".", "", 1).isdigit()).any()
else c.map(lambda x: "" if pd.isna(x) else str(x))
)
return df
+86
View File
@@ -0,0 +1,86 @@
from typing import Any
import pytest
from src.rules import PropertyRules
class TestValueComparison:
"""Test suite for value comparison functionality."""
@pytest.mark.parametrize(
"value1, value2",
[
# Basic numeric strings
("1400", 1400.0),
("1400.0", 1400),
("1400.00", 1400),
# Whitespace handling
(" 1400 ", 1400.0),
(" 1400 ", 1400.0),
("\t1400\n", 1400.0),
# Negative numbers
("-1400", -1400.0),
(" -1400 ", -1400.0),
("-1400.0", -1400),
# Zero handling
("0", 0.0),
("-0", 0.0),
("0.0", 0),
# Simple integers
("1", 1),
("1.0", 1),
],
)
def test_numeric_string_comparison(self, value1: Any, value2: Any):
"""Test comparison of numeric strings with numbers."""
assert PropertyRules.compare_values(value1, value2)
# Test reverse comparison
assert PropertyRules.compare_values(value2, value1)
@pytest.mark.parametrize(
"value1, value2, expected",
[
("Yes", True, True),
("No", False, True),
("yes", True, True),
("no", False, True),
("YES", True, True),
("NO", False, True),
("true", True, True),
("false", False, True),
("True", True, True),
("False", False, True),
],
)
def test_boolean_string_comparison(self, value1: str, value2: bool, expected: bool):
"""Test comparison of boolean strings with booleans."""
assert PropertyRules.compare_values(value1, value2) == expected
# Test reverse comparison
assert PropertyRules.compare_values(value2, value1) == expected
@pytest.mark.parametrize(
"value1, value2, case_sensitive, expected",
[
("hello", "HELLO", False, True),
("hello", "HELLO", True, False),
("Hello", "hello", False, True),
("Hello", "Hello", True, True),
],
)
def test_string_comparison(self, value1: str, value2: str, case_sensitive: bool, expected: bool):
"""Test string comparison with case sensitivity options."""
assert PropertyRules.compare_values(value1, value2, case_sensitive=case_sensitive) == expected
@pytest.mark.parametrize(
"value1, value2, tolerance, expected",
[
(1.0001, 1.0, 1e-3, True),
(1.0001, 1.0, 1e-6, False),
(1.00000001, 1.0, 1e-6, True),
(-1.0001, -1.0, 1e-3, True),
],
)
def test_float_comparison_tolerance(self, value1: float, value2: float, tolerance: float, expected: bool):
"""Test float comparison with different tolerance levels."""
assert PropertyRules.compare_values(value1, value2, tolerance=tolerance) == expected
+21 -21
View File
@@ -8,34 +8,34 @@ from speckle_automate import (
)
from speckle_automate.fixtures import * # noqa: F401, F403
from inputs import MinimumSeverity
from src.function import automate_function
from src.helpers import speckle_print
from src.inputs import FunctionInputs
def test_function_run(test_automation_run_data: AutomationRunData, test_automation_token: str):
"""Run an integration test for the automate function.
class TestFunction:
"""Test suite for the automate function."""
Args:
test_automation_run_data (AutomationRunData): The automation run data provided by sdk.
test_automation_token (str): The automation token.
def test_function_run(self, test_automation_run_data: AutomationRunData, test_automation_token: str):
"""Run an integration test for the automate function.
"""
speckle_print(str(test_automation_run_data))
speckle_print(str(test_automation_token))
Args:
test_automation_run_data (AutomationRunData): The automation run data provided by sdk.
test_automation_token (str): The automation token.
"""Run an integration test for the automate function."""
automation_context = AutomationContext.initialize(
test_automation_run_data, test_automation_token
)
default_url: str = (
"https://drive.google.com/uc?export=download&id=1hiPSw23eOaqd27QD_YsXvZg9PWm7_XBx"
)
"""
speckle_print(str(test_automation_run_data))
speckle_print(str(test_automation_token))
automate_sdk = run_function(
automation_context,
automate_function,
FunctionInputs(spreadsheet_url=default_url),
)
"""Run an integration test for the automate function."""
automation_context = AutomationContext.initialize(test_automation_run_data, test_automation_token)
default_url: str = "https://drive.google.com/uc?export=download&id=1hiPSw23eOaqd27QD_YsXvZg9PWm7_XBx"
assert automate_sdk.run_status == AutomationStatus.SUCCEEDED
automate_sdk = run_function(
automation_context,
automate_function,
FunctionInputs(spreadsheet_url=default_url, minimum_severity=MinimumSeverity.WARNING, hide_skipped=True),
)
assert automate_sdk.run_status == AutomationStatus.SUCCEEDED
+365 -352
View File
@@ -1,3 +1,5 @@
"""Test suite for parameter handling functionality."""
import os
from typing import Any
@@ -15,390 +17,401 @@ from helpers import speckle_print
from src.rules import PropertyRules
def load_test_objects(v2_wall: Any, v3_wall: Any) -> tuple[Base, Base]:
"""Load test objects from a Speckle server."""
client = SpeckleClient(host="https://app.speckle.systems", use_ssl=True)
class TestParameterHandling:
"""Test suite for parameter handling functionality."""
load_dotenv(dotenv_path="../.env")
@staticmethod
def load_test_objects(v2_wall: Any, v3_wall: Any) -> tuple[Base, Base]:
"""Load test objects from a Speckle server."""
client = SpeckleClient(host="https://app.speckle.systems", use_ssl=True)
client.authenticate_with_token(os.getenv("SPECKLE_TOKEN"))
load_dotenv(dotenv_path="../.env")
transport = ServerTransport(client=client, stream_id=os.getenv("SPECKLE_PROJECT_ID"))
client.authenticate_with_token(os.getenv("SPECKLE_TOKEN"))
speckle_print(v2_wall)
v2_obj = operations.receive("cdb18060dc48281909e94f0f1d8d3cc0", transport)
v3_obj = operations.receive("46f06fef727d64a0bbcbd7ced51e0cd2", transport)
transport = ServerTransport(client=client, stream_id=os.getenv("SPECKLE_PROJECT_ID"))
# return v2_wall, v3_wall
return v2_obj, v3_obj
speckle_print(v2_wall)
speckle_print(v3_wall)
v2_obj = operations.receive("cdb18060dc48281909e94f0f1d8d3cc0", transport)
v3_obj = operations.receive("46f06fef727d64a0bbcbd7ced51e0cd2", transport)
# return v2_wall, v3_wall
return v2_obj, v3_obj
@pytest.fixture
def test_objects(v2_wall: Any, v3_wall: Any) -> tuple[Base, Base]:
"""Pytest fixture to provide test objects."""
return load_test_objects(v2_wall, v3_wall)
@pytest.fixture
def test_objects(self, v2_wall: Any, v3_wall: Any) -> tuple[Base, Base]:
"""Pytest fixture to provide test objects."""
return self.load_test_objects(v2_wall, v3_wall)
def test_deserialization_structure(self, test_objects):
"""Test that objects are properly deserialized with correct structure."""
v2_obj, v3_obj = test_objects
def test_deserialization_structure(test_objects):
"""Test that objects are properly deserialized with correct structure."""
v2_obj, v3_obj = test_objects
# Check base class type
for obj in [v2_obj, v3_obj]:
assert isinstance(obj, Base), f"Expected {obj} to be an instance of Base"
# Check base class type
for obj in [v2_obj, v3_obj]:
assert isinstance(obj, Base), f"Expected {obj} to be an instance of Base"
# Check v2 structure
assert hasattr(v2_obj, "parameters"), "v2_obj should have 'parameters' attribute"
assert v2_obj["parameters"] is not None, "v2_obj['parameters'] should not be None"
# Check v2 structure
assert hasattr(v2_obj, "parameters"), "v2_obj should have 'parameters' attribute"
assert v2_obj["parameters"] is not None, "v2_obj['parameters'] should not be None"
# Check v3 structure
assert hasattr(v3_obj, "properties"), "v3_obj should have 'properties' attribute"
assert v3_obj["properties"] is not None, "v3_obj['properties'] should not be None"
assert "Parameters" in v3_obj["properties"], "'Parameters' key should exist in v3_obj['properties']"
# Check v3 structure
assert hasattr(v3_obj, "properties"), "v3_obj should have 'properties' attribute"
assert v3_obj["properties"] is not None, "v3_obj['properties'] should not be None"
assert "Parameters" in v3_obj["properties"], "'Parameters' key should exist in v3_obj['properties']"
@pytest.mark.parametrize(
"param_name, expected_result",
[
("category", True), # Test parameters that should exist
("WALL_ATTR_WIDTH_PARAM", True), # Test nested parameters
("WALL_ATTR_WIDTH_PARAM.value", True),
("WALL_ATTR_WIDTH_PARAM.id", True),
("WALL_ATTR_WIDTH_PARAM.units", True),
("non_existent_param", False), # Test non-existent parameters
],
)
def test_v2_parameter_exists(test_objects, param_name, expected_result):
"""Test parameter existence checking in v2 objects."""
v2_obj, _ = test_objects
assert PropertyRules.has_parameter(v2_obj, param_name) == expected_result
@pytest.mark.parametrize(
"param_name",
[
"WALL_ATTR_WIDTH_PARAM.id",
"WALL_ATTR_WIDTH_PARAM.value",
"WALL_ATTR_WIDTH_PARAM",
"WALL_ATTR_WIDTH_PARAM.units",
],
)
def test_v2_parameter_value_retrieval(test_objects, param_name):
"""Test parameter value retrieval in v2 objects."""
v2_obj, _ = test_objects
assert PropertyRules.get_parameter_value(v2_obj, param_name)
@pytest.mark.parametrize(
"param_name, expected_result",
[
("category", True), # Test parameters that should exist
("Width", True), # Test nested parameters
("non_existent_param", False), # Test non-existent parameters
],
)
def test_v3_parameter_exists(test_objects, param_name, expected_result):
"""Test parameter existence checking in v3 objects."""
_, v3_obj = test_objects
assert PropertyRules.has_parameter(v3_obj, param_name) == expected_result
@pytest.mark.parametrize(
"param_name_1, param_name_2",
[
(
"properties.Parameters.Instance Parameters.Dimensions.Length.value",
"Instance Parameters.Dimensions.Length",
),
],
)
def test_v3_parameter_search_equivalence(test_objects, param_name_1, param_name_2):
"""Test parameter existence checking equivalence in v3 objects."""
_, v3_obj = test_objects
assert PropertyRules.get_parameter_value(v3_obj, param_name_1) == PropertyRules.get_parameter_value(
v3_obj, param_name_2
@pytest.mark.parametrize(
"param_name, expected_result",
[
("category", True), # Test parameters that should exist
("WALL_ATTR_WIDTH_PARAM", True), # Test nested parameters
("WALL_ATTR_WIDTH_PARAM.value", True),
("WALL_ATTR_WIDTH_PARAM.id", True),
("WALL_ATTR_WIDTH_PARAM.units", True),
("non_existent_param", False), # Test non-existent parameters
],
)
def test_v2_parameter_exists(self, test_objects, param_name, expected_result):
"""Test parameter existence checking in v2 objects."""
v2_obj, _ = test_objects
assert PropertyRules.has_parameter(v2_obj, param_name) == expected_result
@pytest.mark.parametrize(
"param_name",
[
"WALL_ATTR_WIDTH_PARAM.id",
"WALL_ATTR_WIDTH_PARAM.value",
"WALL_ATTR_WIDTH_PARAM",
"WALL_ATTR_WIDTH_PARAM.units",
],
)
def test_v2_parameter_value_retrieval(self, test_objects, param_name):
"""Test parameter value retrieval in v2 objects."""
v2_obj, _ = test_objects
assert PropertyRules.get_parameter_value(v2_obj, param_name)
@pytest.mark.parametrize(
"obj_version, param_name, expected_value, default_value",
[
# Test direct parameters
("v2", "category", "Walls", None),
("v3", "category", "Walls", None),
# Test nested parameters - using both internal and friendly names
("v2", "WALL_ATTR_WIDTH_PARAM", 300, None),
("v3", "Construction.Width", 300, None),
# Test parameters with units
("v2", "CURVE_ELEM_LENGTH", 5300.000000000001, None),
("v3", "Instance Parameters.Dimensions.Length", 5300.000000000001, None),
# Test non-existent parameters with a default value
("v2", "parameters.non_existent", "default", "default"),
("v3", "properties.Parameters.non_existent", "default", "default"),
],
)
def test_parameter_value_retrieval(test_objects, obj_version, param_name, expected_value, default_value):
"""Test parameter value retrieval from both v2 and v3 objects."""
v2_obj, v3_obj = test_objects
obj = v2_obj if obj_version == "v2" else v3_obj
result = PropertyRules.get_parameter_value(obj, param_name, default_value=default_value)
assert result == expected_value
@pytest.mark.parametrize(
"param_name, expected_result",
[
("category", True), # Test parameters that should exist
("Width", True), # Test nested parameters
("non_existent_param", False), # Test non-existent parameters
],
)
def test_v3_parameter_exists(self, test_objects, param_name, expected_result):
"""Test parameter existence checking in v3 objects."""
_, v3_obj = test_objects
assert PropertyRules.has_parameter(v3_obj, param_name) == expected_result
@pytest.mark.parametrize(
"param_name_1, param_name_2",
[
(
"properties.Parameters.Instance Parameters.Dimensions.Length.value",
"Instance Parameters.Dimensions.Length",
),
],
)
def test_v3_parameter_search_equivalence(self, test_objects, param_name_1, param_name_2):
"""Test parameter existence checking equivalence in v3 objects."""
_, v3_obj = test_objects
assert PropertyRules.get_parameter_value(v3_obj, param_name_1) == PropertyRules.get_parameter_value(
v3_obj, param_name_2
)
@pytest.mark.parametrize(
"param_name, expected_value, expected_result",
[
("category", "Walls", True), # Test exact match
("WALL_ATTR_WIDTH_PARAM", 300, True), # Test numeric match
("category", "Windows", False), # Test non-match
],
)
def test_v2_parameter_value_matching(test_objects, param_name, expected_value, expected_result):
"""Test parameter value matching in v2 objects."""
v2_obj, _ = test_objects
assert PropertyRules.is_parameter_value(v2_obj, param_name, expected_value) == expected_result
@pytest.mark.parametrize(
"obj_version, param_name, expected_value, default_value",
[
# Test direct parameters
("v2", "category", "Walls", None),
("v3", "category", "Walls", None),
# Test nested parameters - using both internal and friendly names
("v2", "WALL_ATTR_WIDTH_PARAM", 300, None),
("v3", "Construction.Width", 300, None),
# Test parameters with units
("v2", "CURVE_ELEM_LENGTH", 5300.000000000001, None),
("v3", "Instance Parameters.Dimensions.Length", 5300.000000000001, None),
# Test non-existent parameters with a default value
("v2", "parameters.non_existent", "default", "default"),
("v3", "properties.Parameters.non_existent", "default", "default"),
],
)
def test_parameter_value_retrieval(self, test_objects, obj_version, param_name, expected_value, default_value):
"""Test parameter value retrieval from both v2 and v3 objects."""
v2_obj, v3_obj = test_objects
obj = v2_obj if obj_version == "v2" else v3_obj
result = PropertyRules.get_parameter_value(obj, param_name, default_value=default_value)
assert result == expected_value
@pytest.mark.parametrize(
"param_name, expected_value, expected_result",
[
("category", "Walls", True), # Test exact match
("WALL_ATTR_WIDTH_PARAM", 300, True), # Test numeric match
("category", "Windows", False), # Test non-match
],
)
def test_v2_parameter_value_matching(self, test_objects, param_name, expected_value, expected_result):
"""Test parameter value matching in v2 objects."""
v2_obj, _ = test_objects
assert PropertyRules.is_parameter_value(v2_obj, param_name, expected_value) == expected_result
@pytest.mark.parametrize(
"param_name, expected_value, expected_result",
[
("category", "Walls", True), # Test exact match
("Width", 300, True), # Test numeric match
("category", "Windows", False), # Test non-match
],
)
def test_v3_parameter_value_matching(test_objects, param_name, expected_value, expected_result):
"""Test parameter value matching in v3 objects."""
_, v3_obj = test_objects
assert PropertyRules.is_parameter_value(v3_obj, param_name, expected_value) == expected_result
@pytest.mark.parametrize(
"param_name, expected_value, expected_result",
[
("category", "Walls", True), # Test exact match
("Width", 300, True), # Test numeric match
("category", "Windows", False), # Test non-match
],
)
def test_v3_parameter_value_matching(self, test_objects, param_name, expected_value, expected_result):
"""Test parameter value matching in v3 objects."""
_, v3_obj = test_objects
assert PropertyRules.is_parameter_value(v3_obj, param_name, expected_value) == expected_result
@pytest.mark.parametrize(
"comparison_func, param_name, value, expected_result",
[
(PropertyRules.is_parameter_value_greater_than, "WALL_ATTR_WIDTH_PARAM", "200", True), # Test greater than
(PropertyRules.is_parameter_value_less_than, "WALL_ATTR_WIDTH_PARAM", "400", True), # Test less than
(PropertyRules.is_parameter_value_in_range, "WALL_ATTR_WIDTH_PARAM", "200,400", True), # Test in range
],
)
def test_v2_parameter_numeric_comparisons(self, test_objects, comparison_func, param_name, value, expected_result):
"""Test numeric parameter comparisons in v2 objects."""
v2_obj, _ = test_objects
assert comparison_func(v2_obj, param_name, value) == expected_result
@pytest.mark.parametrize(
"comparison_func, param_name, value, expected_result",
[
(PropertyRules.is_parameter_value_greater_than, "WALL_ATTR_WIDTH_PARAM", "200", True), # Test greater than
(PropertyRules.is_parameter_value_less_than, "WALL_ATTR_WIDTH_PARAM", "400", True), # Test less than
(PropertyRules.is_parameter_value_in_range, "WALL_ATTR_WIDTH_PARAM", "200,400", True), # Test in range
],
)
def test_v2_parameter_numeric_comparisons(test_objects, comparison_func, param_name, value, expected_result):
"""Test numeric parameter comparisons in v2 objects."""
v2_obj, _ = test_objects
assert comparison_func(v2_obj, param_name, value) == expected_result
@pytest.mark.parametrize(
"comparison_func, param_name, value, expected_result",
[
(PropertyRules.is_parameter_value_greater_than, "Width", "200", True), # Test greater than
(PropertyRules.is_parameter_value_less_than, "Width", "400", True), # Test less than
(PropertyRules.is_parameter_value_in_range, "Width", "200,400", True), # Test in range
],
)
def test_v3_parameter_numeric_comparisons(self, test_objects, comparison_func, param_name, value, expected_result):
"""Test numeric parameter comparisons in v3 objects."""
_, v3_obj = test_objects
assert comparison_func(v3_obj, param_name, value) == expected_result
@pytest.mark.parametrize(
"param_name, pattern, fuzzy, expected_result",
[
("category", "^Walls$", False, True), # Test exact pattern matches
("category", "Walls", True, True), # Test fuzzy matches
("category", "Wall", False, True), # Test partial pattern matches
("category", "^Windows$", False, False), # Test non-matches
],
)
def test_v2_parameter_value_like(self, test_objects, param_name, pattern, fuzzy, expected_result):
"""Test pattern matching on parameter values in v2 objects."""
v2_obj, _ = test_objects
assert PropertyRules.is_parameter_value_like(v2_obj, param_name, pattern, fuzzy=fuzzy) == expected_result
@pytest.mark.parametrize(
"comparison_func, param_name, value, expected_result",
[
(PropertyRules.is_parameter_value_greater_than, "Width", "200", True), # Test greater than
(PropertyRules.is_parameter_value_less_than, "Width", "400", True), # Test less than
(PropertyRules.is_parameter_value_in_range, "Width", "200,400", True), # Test in range
],
)
def test_v3_parameter_numeric_comparisons(test_objects, comparison_func, param_name, value, expected_result):
"""Test numeric parameter comparisons in v3 objects."""
_, v3_obj = test_objects
assert comparison_func(v3_obj, param_name, value) == expected_result
@pytest.mark.parametrize(
"param_name, pattern, fuzzy, expected_result",
[
("category", "^Walls$", False, True), # Test exact pattern matches
("category", "Walls", True, True), # Test fuzzy matches
("category", "Wall", False, True), # Test partial pattern matches
("category", "^Windows$", False, False), # Test non-matches
],
)
def test_v3_parameter_value_like(self, test_objects, param_name, pattern, fuzzy, expected_result):
"""Test pattern matching on parameter values in v3 objects."""
_, v3_obj = test_objects
assert PropertyRules.is_parameter_value_like(v3_obj, param_name, pattern, fuzzy=fuzzy) == expected_result
@pytest.mark.parametrize(
"param_name, valid_list, expected_result",
[
("category", ["Walls", "Windows", "Doors"], True), # Test value in list
("category", "Walls,Windows,Doors", True), # Test comma-separated string list
("category", ["Windows", "Doors"], False), # Test value not in list
],
)
def test_v2_parameter_lists(self, test_objects, param_name, valid_list, expected_result):
"""Test list-based parameter checks in v2 objects."""
v2_obj, _ = test_objects
assert PropertyRules.is_parameter_value_in_list(v2_obj, param_name, valid_list) == expected_result
@pytest.mark.parametrize(
"param_name, pattern, fuzzy, expected_result",
[
("category", "^Walls$", False, True), # Test exact pattern matches
("category", "Walls", True, True), # Test fuzzy matches
("category", "Wall", False, True), # Test partial pattern matches
("category", "^Windows$", False, False), # Test non-matches
],
)
def test_v2_parameter_value_like(test_objects, param_name, pattern, fuzzy, expected_result):
"""Test pattern matching on parameter values in v2 objects."""
v2_obj, _ = test_objects
assert PropertyRules.is_parameter_value_like(v2_obj, param_name, pattern, fuzzy=fuzzy) == expected_result
@pytest.mark.parametrize(
"param_name, valid_list, expected_result",
[
("category", ["Walls", "Windows", "Doors"], True), # Test value in list
("category", "Walls,Windows,Doors", True), # Test comma-separated string list
("category", ["Windows", "Doors"], False), # Test value not in list
],
)
def test_v3_parameter_lists(self, test_objects, param_name, valid_list, expected_result):
"""Test list-based parameter checks in v3 objects."""
_, v3_obj = test_objects
assert PropertyRules.is_parameter_value_in_list(v3_obj, param_name, valid_list) == expected_result
@pytest.mark.parametrize(
"param_name, expected_result",
[
("WALL_ATTR_ROOM_BOUNDING.value", True), # Test true values
("wall_top_is_attached", False), # Test false values
],
)
def test_v2_boolean_parameters(self, test_objects, param_name, expected_result):
"""Test boolean parameter checks in v2 objects."""
v2_obj, _ = test_objects
if expected_result:
assert PropertyRules.is_parameter_value_true(v2_obj, param_name)
else:
assert PropertyRules.is_parameter_value_false(v2_obj, param_name)
@pytest.mark.parametrize(
"param_name, pattern, fuzzy, expected_result",
[
("category", "^Walls$", False, True), # Test exact pattern matches
("category", "Walls", True, True), # Test fuzzy matches
("category", "Wall", False, True), # Test partial pattern matches
("category", "^Windows$", False, False), # Test non-matches
],
)
def test_v3_parameter_value_like(test_objects, param_name, pattern, fuzzy, expected_result):
"""Test pattern matching on parameter values in v3 objects."""
_, v3_obj = test_objects
assert PropertyRules.is_parameter_value_like(v3_obj, param_name, pattern, fuzzy=fuzzy) == expected_result
@pytest.mark.parametrize(
"param_name, expected_result",
[
("Room Bounding", True), # Test true values
("top is attached", False), # Test false values
("Top is Attached", False), # Case sensitivity test
],
)
def test_v3_boolean_parameters(self, test_objects, param_name, expected_result):
"""Test boolean parameter checks in v3 objects."""
_, v3_obj = test_objects
if expected_result:
assert PropertyRules.is_parameter_value_true(v3_obj, param_name)
else:
assert PropertyRules.is_parameter_value_false(v3_obj, param_name)
@pytest.mark.parametrize(
"param_name, expected_value, expected_result",
[
# Test numeric value comparisons
("WALL_ATTR_WIDTH_PARAM", 300, True),
("WALL_ATTR_WIDTH_PARAM.value", 300, True),
("baseLine.length", 5300.000000000002, True),
# Test string value comparisons
("STRUCTURAL_MATERIAL_PARAM.value", "Fc24", True),
("ee1f33e1-5506-4a64-b87b-7b98d30aea52.value", "W30", True),
# Test non-matches
("WALL_ATTR_WIDTH_PARAM", 301, False),
("nonexistent_param", "any_value", False),
],
)
def test_v2_parameter_value_comparisons(self, v2_wall, param_name, expected_value, expected_result):
"""Test value comparisons using v2 wall parameters."""
assert PropertyRules.is_equal_value(v2_wall, param_name, expected_value) == expected_result
@pytest.mark.parametrize(
"param_name, valid_list, expected_result",
[
("category", ["Walls", "Windows", "Doors"], True), # Test value in list
("category", "Walls,Windows,Doors", True), # Test comma-separated string list
("category", ["Windows", "Doors"], False), # Test value not in list
],
)
def test_v2_parameter_lists(test_objects, param_name, valid_list, expected_result):
"""Test list-based parameter checks in v2 objects."""
v2_obj, _ = test_objects
assert PropertyRules.is_parameter_value_in_list(v2_obj, param_name, valid_list) == expected_result
@pytest.mark.parametrize(
"attribute, value, expected",
[
# Test numeric value comparisons
("Type Parameters.Structure.Fc24 (0).thickness", 300, True),
("location.length", 5300.000000000002, True),
("location.length", 5300, True),
# Test string value comparisons
("Type Parameters.Text.符号.value", "W30", True),
("Instance Parameters.Structural.Structural.value", "Yes", True),
# Test non-matches
("Type Parameters.Structure.Fc24 (0).thickness", 301, False),
("nonexistent_param", "any_value", False),
],
)
def test_v3_parameter_value_comparisons(self, v3_wall, attribute, value, expected):
"""Test value comparisons using v3 wall parameters."""
assert PropertyRules.is_equal_value(v3_wall, attribute, value) == expected
@pytest.mark.parametrize(
"wall, attribute, value, expected",
[
# V2 wall tests
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", 300, True),
("v2_wall", "type", "W30(Fc24)", True),
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", 300.0001, False),
# V3 wall tests
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", 300, True),
("v3_wall", "type", "W30(Fc24)", True),
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", 300.0001, False),
("v3_wall", "location.length", 5300.000000000002, True),
("v3_wall", "location.length", 5300, False),
],
)
def test_identical_comparisons(self, request, wall, attribute, value, expected):
"""Test identical value comparisons on both wall versions."""
wall_instance = request.getfixturevalue(wall)
assert PropertyRules.is_identical_value(wall_instance, attribute, value) == expected
@pytest.mark.parametrize(
"param_name, valid_list, expected_result",
[
("category", ["Walls", "Windows", "Doors"], True), # Test value in list
("category", "Walls,Windows,Doors", True), # Test comma-separated string list
("category", ["Windows", "Doors"], False), # Test value not in list
],
)
def test_v3_parameter_lists(test_objects, param_name, valid_list, expected_result):
"""Test list-based parameter checks in v3 objects."""
_, v3_obj = test_objects
assert PropertyRules.is_parameter_value_in_list(v3_obj, param_name, valid_list) == expected_result
@pytest.mark.parametrize(
"wall, attribute, value",
[
# V2 wall tests
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", 301),
("v2_wall", "STRUCTURAL_MATERIAL_PARAM.value", "Fc25"),
("v2_wall", "nonexistent_param", "any_value"),
# V3 wall tests
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", 301),
("v3_wall", "Type Parameters.Text.符号.value", "W31"),
("v3_wall", "nonexistent_param", "any_value"),
],
)
def test_not_equal_comparisons(self, request, wall, attribute, value):
"""Test not equal comparisons on both wall versions."""
wall_instance = request.getfixturevalue(wall)
assert PropertyRules.is_not_equal_value(wall_instance, attribute, value)
@pytest.mark.parametrize(
"attribute, value, expected_equal, expected_identical",
[
# Test Yes/No conversion in equals (should convert)
("Instance Parameters.Structural.Structural.value", True, True, False), # Yes vs True
("Instance Parameters.Structural.Structural.value", "Yes", True, True), # Yes vs "Yes"
("Instance Parameters.Structural.Structural.value", "yes", True, False), # Yes vs "yes"
],
)
def test_boolean_conversions(self, v3_wall, attribute, value, expected_equal, expected_identical):
"""Test conversion of Yes/No strings to boolean values."""
assert PropertyRules.is_equal_value(v3_wall, attribute, value) == expected_equal
assert PropertyRules.is_identical_value(v3_wall, attribute, value) == expected_identical
@pytest.mark.parametrize(
"param_name, expected_result",
[
("WALL_ATTR_ROOM_BOUNDING.value", True), # Test true values
("wall_top_is_attached", False), # Test false values
],
)
def test_v2_boolean_parameters(test_objects, param_name, expected_result):
"""Test boolean parameter checks in v2 objects."""
v2_obj, _ = test_objects
if expected_result:
assert PropertyRules.is_parameter_value_true(v2_obj, param_name)
else:
assert PropertyRules.is_parameter_value_false(v2_obj, param_name)
@pytest.mark.parametrize(
"wall, attribute, expected_value",
[
# V2 wall tests
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", "300"),
("v2_wall", "baseLine.length", "5300.000000000002"),
# V3 wall tests
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", "300"),
("v3_wall", "location.length", "5300.000000000002"),
],
)
def test_numeric_string_handling(self, wall, attribute, expected_value, request):
"""Test handling of numeric strings in both wall versions."""
wall_instance = request.getfixturevalue(wall) # Retrieve fixture dynamically
assert PropertyRules.is_equal_value(wall_instance, attribute, expected_value)
@pytest.mark.parametrize(
"param_name, substring, expected_result",
[
("speckle_type", "Revit", True), # Test basic substring match
("speckle_type", "revit", True), # Test case-insensitive
("speckle_type", "NotPresent", False), # Test no match
("speckle_type", "", True), # Test empty string
("non_existent", "anything", False), # Test non-existent parameter
],
)
def test_parameter_value_contains(self, test_objects, param_name, substring, expected_result):
"""Test substring matching on parameter values."""
v2_obj, _ = test_objects
assert PropertyRules.is_parameter_value_containing(v2_obj, param_name, substring) == expected_result
@pytest.mark.parametrize(
"param_name, expected_result",
[
("Room Bounding", True), # Test true values
("top is attached", False), # Test false values
("Top is Attached", False), # Case sensitivity test
],
)
def test_v3_boolean_parameters(test_objects, param_name, expected_result):
"""Test boolean parameter checks in v3 objects."""
_, v3_obj = test_objects
if expected_result:
assert PropertyRules.is_parameter_value_true(v3_obj, param_name)
else:
assert PropertyRules.is_parameter_value_false(v3_obj, param_name)
@pytest.mark.parametrize(
"param_name, expected_value, expected_result",
[
# Test numeric value comparisons
("WALL_ATTR_WIDTH_PARAM", 300, True),
("WALL_ATTR_WIDTH_PARAM.value", 300, True),
("baseLine.length", 5300.000000000002, True),
# Test string value comparisons
("STRUCTURAL_MATERIAL_PARAM.value", "Fc24", True),
("ee1f33e1-5506-4a64-b87b-7b98d30aea52.value", "W30", True),
# Test non-matches
("WALL_ATTR_WIDTH_PARAM", 301, False),
("nonexistent_param", "any_value", False),
],
)
def test_v2_parameter_value_comparisons(v2_wall, param_name, expected_value, expected_result):
"""Test value comparisons using v2 wall parameters."""
assert PropertyRules.is_equal_value(v2_wall, param_name, expected_value) == expected_result
@pytest.mark.parametrize(
"attribute, value, expected",
[
# Test numeric value comparisons
("Type Parameters.Structure.Fc24 (0).thickness", 300, True),
("location.length", 5300.000000000002, True),
("location.length", 5300, True),
# Test string value comparisons
("Type Parameters.Text.符号.value", "W30", True),
("Instance Parameters.Structural.Structural.value", "Yes", True),
# Test non-matches
("Type Parameters.Structure.Fc24 (0).thickness", 301, False),
("nonexistent_param", "any_value", False),
],
)
def test_v3_parameter_value_comparisons(v3_wall, attribute, value, expected):
"""Test value comparisons using v3 wall parameters."""
assert PropertyRules.is_equal_value(v3_wall, attribute, value) == expected
@pytest.mark.parametrize(
"wall, attribute, value, expected",
[
# V2 wall tests
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", 300, True),
("v2_wall", "type", "W30(Fc24)", True),
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", 300.0001, False),
# V3 wall tests
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", 300, True),
("v3_wall", "type", "W30(Fc24)", True),
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", 300.0001, False),
("v3_wall", "location.length", 5300.000000000002, True),
("v3_wall", "location.length", 5300, False),
],
)
def test_identical_comparisons(request, wall, attribute, value, expected):
"""Test identical value comparisons on both wall versions."""
wall_instance = request.getfixturevalue(wall)
assert PropertyRules.is_identical_value(wall_instance, attribute, value) == expected
@pytest.mark.parametrize(
"wall, attribute, value",
[
# V2 wall tests
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", 301),
("v2_wall", "STRUCTURAL_MATERIAL_PARAM.value", "Fc25"),
("v2_wall", "nonexistent_param", "any_value"),
# V3 wall tests
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", 301),
("v3_wall", "Type Parameters.Text.符号.value", "W31"),
("v3_wall", "nonexistent_param", "any_value"),
],
)
def test_not_equal_comparisons(request, wall, attribute, value):
"""Test not equal comparisons on both wall versions."""
wall_instance = request.getfixturevalue(wall)
assert PropertyRules.is_not_equal_value(wall_instance, attribute, value)
@pytest.mark.parametrize(
"attribute, value, expected_equal, expected_identical",
[
# Test Yes/No conversion in equals (should convert)
("Instance Parameters.Structural.Structural.value", True, True, False), # Yes vs True
("Instance Parameters.Structural.Structural.value", "Yes", True, True), # Yes vs "Yes"
("Instance Parameters.Structural.Structural.value", "yes", True, False), # Yes vs "yes"
],
)
def test_boolean_conversions(v3_wall, attribute, value, expected_equal, expected_identical):
"""Test conversion of Yes/No strings to boolean values."""
assert PropertyRules.is_equal_value(v3_wall, attribute, value) == expected_equal
assert PropertyRules.is_identical_value(v3_wall, attribute, value) == expected_identical
@pytest.mark.parametrize(
"wall, attribute, expected_value",
[
# V2 wall tests
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", "300"),
("v2_wall", "baseLine.length", "5300.000000000002"),
# V3 wall tests
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", "300"),
("v3_wall", "location.length", "5300.000000000002"),
],
)
def test_numeric_string_handling(wall, attribute, expected_value, request):
"""Test handling of numeric strings in both wall versions."""
wall_instance = request.getfixturevalue(wall) # Retrieve fixture dynamically
assert PropertyRules.is_equal_value(wall_instance, attribute, expected_value)
@pytest.mark.parametrize(
"param_name, substring, expected_result",
[
("speckle_type", "Revit", False), # Should fail as it does contain Revit
("speckle_type", "NotPresent", True), # Should pass as it doesn't contain
("speckle_type", "", False), # Should fail as empty string is contained
("non_existent", "anything", True), # Should pass as non-existent can't contain
],
)
def test_parameter_value_not_contains(self, test_objects, param_name, substring, expected_result):
"""Test negative substring matching on parameter values."""
v2_obj, _ = test_objects
assert PropertyRules.is_parameter_value_not_containing(v2_obj, param_name, substring) == expected_result
+36
View File
@@ -0,0 +1,36 @@
"""Tests for rule processing functionality."""
import pandas as pd
import pytest
@pytest.fixture
def explicit_check_rule():
"""Create a rule using explicit CHECK format."""
return pd.DataFrame(
{
"Rule Number": [1, 1, 1],
"Logic": ["WHERE", "AND", "CHECK"],
"Property Name": ["category", "width", "material"],
"Predicate": ["matches", "greater than", "matches"],
"Value": ["Walls", "200", "Concrete"],
"Message": ["Test message", "", ""],
"Severity": ["Error", "", ""],
}
)
@pytest.fixture
def legacy_rule():
"""Create a rule using legacy format (last AND is implicit check)."""
return pd.DataFrame(
{
"Rule Number": [1, 1, 1],
"Logic": ["WHERE", "AND", "AND"],
"Property Name": ["category", "width", "material"],
"Predicate": ["matches", "greater than", "matches"],
"Value": ["Walls", "200", "Concrete"],
"Message": ["Test message", "", ""],
"Severity": ["Error", "", ""],
}
)
+2 -2
View File
@@ -16,8 +16,8 @@ from src.rule_processor import SeverityLevel, get_severity
("ERROR", SeverityLevel.ERROR),
("error", SeverityLevel.ERROR),
("Error", SeverityLevel.ERROR),
("WARN", SeverityLevel.WARNING), # Invalid → Defaults to ERROR
("warn", SeverityLevel.WARNING), # Invalid → Defaults to ERROR
("WARN", SeverityLevel.WARNING),
("warn", SeverityLevel.WARNING),
("Critical", SeverityLevel.ERROR), # Invalid → Defaults to ERROR
("Severe", SeverityLevel.ERROR), # Invalid → Defaults to ERROR
("", SeverityLevel.ERROR), # Empty string → Defaults to ERROR
Generated
-977
View File
@@ -1,977 +0,0 @@
version = 1
requires-python = ">=3.12"
[[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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
]
[[package]]
name = "anyio"
version = "4.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 },
]
[[package]]
name = "appdirs"
version = "1.4.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566 },
]
[[package]]
name = "attrs"
version = "23.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e3/fc/f800d51204003fa8ae392c4e8278f256206e7a919b708eef054f5f4b650d/attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", size = 780820 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/44/827b2a91a5816512fcaf3cc4ebc465ccd5d598c45cefa6703fcf4a79018f/attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1", size = 60752 },
]
[[package]]
name = "backoff"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148 },
]
[[package]]
name = "black"
version = "25.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "mypy-extensions" },
{ name = "packaging" },
{ name = "pathspec" },
{ name = "platformdirs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988 },
{ url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985 },
{ url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816 },
{ url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860 },
{ url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673 },
{ url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190 },
{ url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926 },
{ url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613 },
{ url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 },
]
[[package]]
name = "certifi"
version = "2025.1.31"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
]
[[package]]
name = "charset-normalizer"
version = "3.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 },
{ url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 },
{ url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 },
{ url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 },
{ url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 },
{ url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 },
{ url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 },
{ url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 },
{ url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 },
{ url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 },
{ url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 },
{ url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 },
{ url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 },
{ url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
{ url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
{ url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
{ url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
{ url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
{ url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
{ url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
{ url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
{ url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
{ url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
{ url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
{ url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
{ url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
{ url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
]
[[package]]
name = "click"
version = "8.1.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
]
[[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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
]
[[package]]
name = "deprecated"
version = "1.2.18"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998 },
]
[[package]]
name = "gql"
version = "3.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "backoff" },
{ name = "graphql-core" },
{ name = "yarl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/85/feda24b33adcc6c8463a62a8e2ca2cc3425dc6d687388ff728ceae231204/gql-3.5.0.tar.gz", hash = "sha256:ccb9c5db543682b28f577069950488218ed65d4ac70bb03b6929aaadaf636de9", size = 179939 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/74/fb/01a200e1c31b79690427c8e983014e4220d2652b4372a46fe4598e1d7a8e/gql-3.5.0-py2.py3-none-any.whl", hash = "sha256:70dda5694a5b194a8441f077aa5fb70cc94e4ec08016117523f013680901ecb7", size = 74001 },
]
[package.optional-dependencies]
requests = [
{ name = "requests" },
{ name = "requests-toolbelt" },
]
websockets = [
{ name = "websockets" },
]
[[package]]
name = "graphql-core"
version = "3.2.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c4/16/7574029da84834349b60ed71614d66ca3afe46e9bf9c7b9562102acb7d4f/graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab", size = 505353 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/4f/7297663840621022bc73c22d7d9d80dbc78b4db6297f764b545cd5dd462d/graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f", size = 203416 },
]
[[package]]
name = "h11"
version = "0.14.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
]
[[package]]
name = "httpcore"
version = "1.0.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
]
[[package]]
name = "httpx"
version = "0.25.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8c/23/911d93a022979d3ea295f659fbe7edb07b3f4561a477e83b3a6d0e0c914e/httpx-0.25.2.tar.gz", hash = "sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8", size = 123889 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/65/6940eeb21dcb2953778a6895281c179efd9100463ff08cb6232bb6480da7/httpx-0.25.2-py3-none-any.whl", hash = "sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118", size = 74980 },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "iniconfig"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
]
[[package]]
name = "levenshtein"
version = "0.26.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "rapidfuzz" },
]
sdist = { url = "https://files.pythonhosted.org/packages/97/e6/79807d3b59a67dd78bb77072ca6a28d8db0935161fecf935e6c38c5f6825/levenshtein-0.26.1.tar.gz", hash = "sha256:0d19ba22330d50609b2349021ec3cf7d905c6fe21195a2d0d876a146e7ed2575", size = 374307 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4c/53/3685ee7fbe9b8eb4b82d8045255e59dd6943f94e8091697ef3808e7ecf63/levenshtein-0.26.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cc741ca406d3704dc331a69c04b061fc952509a069b79cab8287413f434684bd", size = 176447 },
{ url = "https://files.pythonhosted.org/packages/82/7f/7d6fe9b76bd030200f8f9b162f3de862d597804d292af292ec3ce9ae8bee/levenshtein-0.26.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:821ace3b4e1c2e02b43cf5dc61aac2ea43bdb39837ac890919c225a2c3f2fea4", size = 157589 },
{ url = "https://files.pythonhosted.org/packages/bc/d3/44539e952df93c5d88a95a0edff34af38e4f87330a76e8335bfe2c0f31bf/levenshtein-0.26.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92694c9396f55d4c91087efacf81297bef152893806fc54c289fc0254b45384", size = 153306 },
{ url = "https://files.pythonhosted.org/packages/ba/fe/21443c0c50824314e2d2ce7e1e9cd11d21b3643f3c14da156b15b4d399c7/levenshtein-0.26.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51ba374de7a1797d04a14a4f0ad3602d2d71fef4206bb20a6baaa6b6a502da58", size = 184409 },
{ url = "https://files.pythonhosted.org/packages/f0/7b/c95066c64bb18628cf7488e0dd6aec2b7cbda307d93ba9ede68a21af2a7b/levenshtein-0.26.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7aa5c3327dda4ef952769bacec09c09ff5bf426e07fdc94478c37955681885b", size = 193134 },
{ url = "https://files.pythonhosted.org/packages/36/22/5f9760b135bdefb8cf8d663890756136754db03214f929b73185dfa33f05/levenshtein-0.26.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e2517e8d3c221de2d1183f400aed64211fcfc77077b291ed9f3bb64f141cdc", size = 162266 },
{ url = "https://files.pythonhosted.org/packages/11/50/6b1a5f3600caae40db0928f6775d7efc62c13dec2407d3d540bc4afdb72c/levenshtein-0.26.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9092b622765c7649dd1d8af0f43354723dd6f4e570ac079ffd90b41033957438", size = 246339 },
{ url = "https://files.pythonhosted.org/packages/26/eb/ede282fcb495570898b39a0d2f21bbc9be5587d604c93a518ece80f3e7dc/levenshtein-0.26.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fc16796c85d7d8b259881d59cc8b5e22e940901928c2ff6924b2c967924e8a0b", size = 1077937 },
{ url = "https://files.pythonhosted.org/packages/35/41/eebe1c4a75f592d9bdc3c2595418f083bcad747e0aec52a1a9ffaae93f5c/levenshtein-0.26.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4370733967f5994ceeed8dc211089bedd45832ee688cecea17bfd35a9eb22b9", size = 1330607 },
{ url = "https://files.pythonhosted.org/packages/12/8e/4d34b1857adfd69c2a72d84bca1b8538d4cfaaf6fddd8599573f4281a9d1/levenshtein-0.26.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3535ecfd88c9b283976b5bc61265855f59bba361881e92ed2b5367b6990c93fe", size = 1197505 },
{ url = "https://files.pythonhosted.org/packages/c0/7b/6afcda1b0a0622cedaa4f7a5b3507c2384a7358fc051ccf619e5d2453bf2/levenshtein-0.26.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:90236e93d98bdfd708883a6767826fafd976dac8af8fc4a0fb423d4fa08e1bf0", size = 1352832 },
{ url = "https://files.pythonhosted.org/packages/21/5e/0ed4e7b5c820b6bc40e2c391633292c3666400339042a3d306f0dc8fdcb4/levenshtein-0.26.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:04b7cabb82edf566b1579b3ed60aac0eec116655af75a3c551fee8754ffce2ea", size = 1135970 },
{ url = "https://files.pythonhosted.org/packages/c9/91/3ff1abacb58642749dfd130ad855370e01b9c7aeaa73801964361f6e355f/levenshtein-0.26.1-cp312-cp312-win32.whl", hash = "sha256:ae382af8c76f6d2a040c0d9ca978baf461702ceb3f79a0a3f6da8d596a484c5b", size = 87599 },
{ url = "https://files.pythonhosted.org/packages/7d/f9/727f3ba7843a3fb2a0f3db825358beea2a52bc96258874ee80cb2e5ecabb/levenshtein-0.26.1-cp312-cp312-win_amd64.whl", hash = "sha256:fd091209798cfdce53746f5769987b4108fe941c54fb2e058c016ffc47872918", size = 98809 },
{ url = "https://files.pythonhosted.org/packages/d4/f4/f87f19222d279dbac429b9bc7ccae271d900fd9c48a581b8bc180ba6cd09/levenshtein-0.26.1-cp312-cp312-win_arm64.whl", hash = "sha256:7e82f2ea44a81ad6b30d92a110e04cd3c8c7c6034b629aca30a3067fa174ae89", size = 88227 },
{ url = "https://files.pythonhosted.org/packages/7e/d6/b4b522b94d7b387c023d22944590befc0ac6b766ac6d197afd879ddd77fc/levenshtein-0.26.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:790374a9f5d2cbdb30ee780403a62e59bef51453ac020668c1564d1e43438f0e", size = 175836 },
{ url = "https://files.pythonhosted.org/packages/25/76/06d1e26a8e6d0de68ef4a157dd57f6b342413c03550309e4aa095a453b28/levenshtein-0.26.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7b05c0415c386d00efda83d48db9db68edd02878d6dbc6df01194f12062be1bb", size = 157036 },
{ url = "https://files.pythonhosted.org/packages/7e/23/21209a9e96b878aede3bea104533866762ba621e36fc344aa080db5feb02/levenshtein-0.26.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3114586032361722ddededf28401ce5baf1cf617f9f49fb86b8766a45a423ff", size = 153326 },
{ url = "https://files.pythonhosted.org/packages/06/38/9fc68685fffd8863b13864552eba8f3eb6a82a4dc558bf2c6553c2347d6c/levenshtein-0.26.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2532f8a13b68bf09f152d906f118a88da2063da22f44c90e904b142b0a53d534", size = 183693 },
{ url = "https://files.pythonhosted.org/packages/f6/82/ccd7bdd7d431329da025e649c63b731df44f8cf31b957e269ae1c1dc9a8e/levenshtein-0.26.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:219c30be6aa734bf927188d1208b7d78d202a3eb017b1c5f01ab2034d2d4ccca", size = 190581 },
{ url = "https://files.pythonhosted.org/packages/6e/c5/57f90b4aea1f89f853872b27a5a5dbce37b89ffeae42c02060b3e82038b2/levenshtein-0.26.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397e245e77f87836308bd56305bba630010cd8298c34c4c44bd94990cdb3b7b1", size = 162446 },
{ url = "https://files.pythonhosted.org/packages/fc/da/df6acca738921f896ce2d178821be866b43a583f85e2d1de63a4f8f78080/levenshtein-0.26.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeff6ea3576f72e26901544c6c55c72a7b79b9983b6f913cba0e9edbf2f87a97", size = 247123 },
{ url = "https://files.pythonhosted.org/packages/22/fb/f44a4c0d7784ccd32e4166714fea61e50f62b232162ae16332f45cb55ab2/levenshtein-0.26.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a19862e3539a697df722a08793994e334cd12791e8144851e8a1dee95a17ff63", size = 1077437 },
{ url = "https://files.pythonhosted.org/packages/f0/5e/d9b9e7daa13cc7e2184a3c2422bb847f05d354ce15ba113b20d83e9ab366/levenshtein-0.26.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:dc3b5a64f57c3c078d58b1e447f7d68cad7ae1b23abe689215d03fc434f8f176", size = 1330362 },
{ url = "https://files.pythonhosted.org/packages/bf/67/480d85bb516798014a6849be0225b246f35df4b54499c348c9c9e311f936/levenshtein-0.26.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bb6c7347424a91317c5e1b68041677e4c8ed3e7823b5bbaedb95bffb3c3497ea", size = 1198721 },
{ url = "https://files.pythonhosted.org/packages/9a/7d/889ff7d86903b6545665655627113d263c88c6d596c68fb09a640ee4f0a7/levenshtein-0.26.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b817376de4195a207cc0e4ca37754c0e1e1078c2a2d35a6ae502afde87212f9e", size = 1351820 },
{ url = "https://files.pythonhosted.org/packages/b9/29/cd42273150f08c200ed2d1879486d73502ee35265f162a77952f101d93a0/levenshtein-0.26.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7b50c3620ff47c9887debbb4c154aaaac3e46be7fc2e5789ee8dbe128bce6a17", size = 1135747 },
{ url = "https://files.pythonhosted.org/packages/1d/90/cbcfa3dd86023e82036662a19fec2fcb48782d3f9fa322d44dc898d95a5d/levenshtein-0.26.1-cp313-cp313-win32.whl", hash = "sha256:9fb859da90262eb474c190b3ca1e61dee83add022c676520f5c05fdd60df902a", size = 87318 },
{ url = "https://files.pythonhosted.org/packages/83/73/372edebc79fd09a8b2382cf1244d279ada5b795124f1e1c4fc73d9fbb00f/levenshtein-0.26.1-cp313-cp313-win_amd64.whl", hash = "sha256:8adcc90e3a5bfb0a463581d85e599d950fe3c2938ac6247b29388b64997f6e2d", size = 98418 },
{ url = "https://files.pythonhosted.org/packages/b2/6d/f0160ea5a7bb7a62b3b3d56e9fc5024b440cb59555a90be2347abf2e7888/levenshtein-0.26.1-cp313-cp313-win_arm64.whl", hash = "sha256:c2599407e029865dc66d210b8804c7768cbdbf60f061d993bb488d5242b0b73e", size = 87792 },
]
[[package]]
name = "more-itertools"
version = "10.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/88/3b/7fa1fe835e2e93fd6d7b52b2f95ae810cf5ba133e1845f726f5a992d62c2/more-itertools-10.6.0.tar.gz", hash = "sha256:2cd7fad1009c31cc9fb6a035108509e6547547a7a738374f10bd49a09eb3ee3b", size = 125009 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/23/62/0fe302c6d1be1c777cab0616e6302478251dfbf9055ad426f5d0def75c89/more_itertools-10.6.0-py3-none-any.whl", hash = "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89", size = 63038 },
]
[[package]]
name = "multidict"
version = "6.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/16/92057c74ba3b96d5e211b553895cd6dc7cc4d1e43d9ab8fafc727681ef71/multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa", size = 48713 },
{ url = "https://files.pythonhosted.org/packages/94/3d/37d1b8893ae79716179540b89fc6a0ee56b4a65fcc0d63535c6f5d96f217/multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436", size = 29516 },
{ url = "https://files.pythonhosted.org/packages/a2/12/adb6b3200c363062f805275b4c1e656be2b3681aada66c80129932ff0bae/multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761", size = 29557 },
{ url = "https://files.pythonhosted.org/packages/47/e9/604bb05e6e5bce1e6a5cf80a474e0f072e80d8ac105f1b994a53e0b28c42/multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e", size = 130170 },
{ url = "https://files.pythonhosted.org/packages/7e/13/9efa50801785eccbf7086b3c83b71a4fb501a4d43549c2f2f80b8787d69f/multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef", size = 134836 },
{ url = "https://files.pythonhosted.org/packages/bf/0f/93808b765192780d117814a6dfcc2e75de6dcc610009ad408b8814dca3ba/multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95", size = 133475 },
{ url = "https://files.pythonhosted.org/packages/d3/c8/529101d7176fe7dfe1d99604e48d69c5dfdcadb4f06561f465c8ef12b4df/multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925", size = 131049 },
{ url = "https://files.pythonhosted.org/packages/ca/0c/fc85b439014d5a58063e19c3a158a889deec399d47b5269a0f3b6a2e28bc/multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966", size = 120370 },
{ url = "https://files.pythonhosted.org/packages/db/46/d4416eb20176492d2258fbd47b4abe729ff3b6e9c829ea4236f93c865089/multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305", size = 125178 },
{ url = "https://files.pythonhosted.org/packages/5b/46/73697ad7ec521df7de5531a32780bbfd908ded0643cbe457f981a701457c/multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2", size = 119567 },
{ url = "https://files.pythonhosted.org/packages/cd/ed/51f060e2cb0e7635329fa6ff930aa5cffa17f4c7f5c6c3ddc3500708e2f2/multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2", size = 129822 },
{ url = "https://files.pythonhosted.org/packages/df/9e/ee7d1954b1331da3eddea0c4e08d9142da5f14b1321c7301f5014f49d492/multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6", size = 128656 },
{ url = "https://files.pythonhosted.org/packages/77/00/8538f11e3356b5d95fa4b024aa566cde7a38aa7a5f08f4912b32a037c5dc/multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3", size = 125360 },
{ url = "https://files.pythonhosted.org/packages/be/05/5d334c1f2462d43fec2363cd00b1c44c93a78c3925d952e9a71caf662e96/multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133", size = 26382 },
{ url = "https://files.pythonhosted.org/packages/a3/bf/f332a13486b1ed0496d624bcc7e8357bb8053823e8cd4b9a18edc1d97e73/multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1", size = 28529 },
{ url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 },
{ url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 },
{ url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 },
{ url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 },
{ url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 },
{ url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 },
{ url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 },
{ url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 },
{ url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 },
{ url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 },
{ url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 },
{ url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 },
{ url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 },
{ url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 },
{ url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 },
{ url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 },
]
[[package]]
name = "mypy"
version = "1.15.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mypy-extensions" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 },
{ url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 },
{ url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 },
{ url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 },
{ url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 },
{ url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 },
{ url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 },
{ url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 },
{ url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 },
{ url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 },
{ url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 },
{ url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 },
{ url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 },
]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
]
[[package]]
name = "numpy"
version = "2.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ec/d0/c12ddfd3a02274be06ffc71f3efc6d0e457b0409c4481596881e748cb264/numpy-2.2.2.tar.gz", hash = "sha256:ed6906f61834d687738d25988ae117683705636936cc605be0bb208b23df4d8f", size = 20233295 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/e6/847d15770ab7a01e807bdfcd4ead5bdae57c0092b7dc83878171b6af97bb/numpy-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ac9bea18d6d58a995fac1b2cb4488e17eceeac413af014b1dd26170b766d8467", size = 20912636 },
{ url = "https://files.pythonhosted.org/packages/d1/af/f83580891577b13bd7e261416120e036d0d8fb508c8a43a73e38928b794b/numpy-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae9f0c2d889b7b2d88a3791f6c09e2ef827c2446f1c4a3e3e76328ee4afd9a", size = 14098403 },
{ url = "https://files.pythonhosted.org/packages/2b/86/d019fb60a9d0f1d4cf04b014fe88a9135090adfadcc31c1fadbb071d7fa7/numpy-2.2.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3074634ea4d6df66be04f6728ee1d173cfded75d002c75fac79503a880bf3825", size = 5128938 },
{ url = "https://files.pythonhosted.org/packages/7a/1b/50985edb6f1ec495a1c36452e860476f5b7ecdc3fc59ea89ccad3c4926c5/numpy-2.2.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ec0636d3f7d68520afc6ac2dc4b8341ddb725039de042faf0e311599f54eb37", size = 6661937 },
{ url = "https://files.pythonhosted.org/packages/f4/1b/17efd94cad1b9d605c3f8907fb06bcffc4ce4d1d14d46b95316cccccf2b9/numpy-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ffbb1acd69fdf8e89dd60ef6182ca90a743620957afb7066385a7bbe88dc748", size = 14049518 },
{ url = "https://files.pythonhosted.org/packages/5b/73/65d2f0b698df1731e851e3295eb29a5ab8aa06f763f7e4188647a809578d/numpy-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0349b025e15ea9d05c3d63f9657707a4e1d471128a3b1d876c095f328f8ff7f0", size = 16099146 },
{ url = "https://files.pythonhosted.org/packages/d5/69/308f55c0e19d4b5057b5df286c5433822e3c8039ede06d4051d96f1c2c4e/numpy-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:463247edcee4a5537841d5350bc87fe8e92d7dd0e8c71c995d2c6eecb8208278", size = 15246336 },
{ url = "https://files.pythonhosted.org/packages/f0/d8/d8d333ad0d8518d077a21aeea7b7c826eff766a2b1ce1194dea95ca0bacf/numpy-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9dd47ff0cb2a656ad69c38da850df3454da88ee9a6fde0ba79acceee0e79daba", size = 17863507 },
{ url = "https://files.pythonhosted.org/packages/82/6e/0b84ad3103ffc16d6673e63b5acbe7901b2af96c2837174c6318c98e27ab/numpy-2.2.2-cp312-cp312-win32.whl", hash = "sha256:4525b88c11906d5ab1b0ec1f290996c0020dd318af8b49acaa46f198b1ffc283", size = 6276491 },
{ url = "https://files.pythonhosted.org/packages/fc/84/7f801a42a67b9772a883223a0a1e12069a14626c81a732bd70aac57aebc1/numpy-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:5acea83b801e98541619af398cc0109ff48016955cc0818f478ee9ef1c5c3dcb", size = 12616372 },
{ url = "https://files.pythonhosted.org/packages/e1/fe/df5624001f4f5c3e0b78e9017bfab7fdc18a8d3b3d3161da3d64924dd659/numpy-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b208cfd4f5fe34e1535c08983a1a6803fdbc7a1e86cf13dd0c61de0b51a0aadc", size = 20899188 },
{ url = "https://files.pythonhosted.org/packages/a9/80/d349c3b5ed66bd3cb0214be60c27e32b90a506946857b866838adbe84040/numpy-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d0bbe7dd86dca64854f4b6ce2ea5c60b51e36dfd597300057cf473d3615f2369", size = 14113972 },
{ url = "https://files.pythonhosted.org/packages/9d/50/949ec9cbb28c4b751edfa64503f0913cbfa8d795b4a251e7980f13a8a655/numpy-2.2.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:22ea3bb552ade325530e72a0c557cdf2dea8914d3a5e1fecf58fa5dbcc6f43cd", size = 5114294 },
{ url = "https://files.pythonhosted.org/packages/8d/f3/399c15629d5a0c68ef2aa7621d430b2be22034f01dd7f3c65a9c9666c445/numpy-2.2.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:128c41c085cab8a85dc29e66ed88c05613dccf6bc28b3866cd16050a2f5448be", size = 6648426 },
{ url = "https://files.pythonhosted.org/packages/2c/03/c72474c13772e30e1bc2e558cdffd9123c7872b731263d5648b5c49dd459/numpy-2.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:250c16b277e3b809ac20d1f590716597481061b514223c7badb7a0f9993c7f84", size = 14045990 },
{ url = "https://files.pythonhosted.org/packages/83/9c/96a9ab62274ffafb023f8ee08c88d3d31ee74ca58869f859db6845494fa6/numpy-2.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0c8854b09bc4de7b041148d8550d3bd712b5c21ff6a8ed308085f190235d7ff", size = 16096614 },
{ url = "https://files.pythonhosted.org/packages/d5/34/cd0a735534c29bec7093544b3a509febc9b0df77718a9b41ffb0809c9f46/numpy-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b6fb9c32a91ec32a689ec6410def76443e3c750e7cfc3fb2206b985ffb2b85f0", size = 15242123 },
{ url = "https://files.pythonhosted.org/packages/5e/6d/541717a554a8f56fa75e91886d9b79ade2e595918690eb5d0d3dbd3accb9/numpy-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:57b4012e04cc12b78590a334907e01b3a85efb2107df2b8733ff1ed05fce71de", size = 17859160 },
{ url = "https://files.pythonhosted.org/packages/b9/a5/fbf1f2b54adab31510728edd06a05c1b30839f37cf8c9747cb85831aaf1b/numpy-2.2.2-cp313-cp313-win32.whl", hash = "sha256:4dbd80e453bd34bd003b16bd802fac70ad76bd463f81f0c518d1245b1c55e3d9", size = 6273337 },
{ url = "https://files.pythonhosted.org/packages/56/e5/01106b9291ef1d680f82bc47d0c5b5e26dfed15b0754928e8f856c82c881/numpy-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:5a8c863ceacae696aff37d1fd636121f1a512117652e5dfb86031c8d84836369", size = 12609010 },
{ url = "https://files.pythonhosted.org/packages/9f/30/f23d9876de0f08dceb707c4dcf7f8dd7588266745029debb12a3cdd40be6/numpy-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b3482cb7b3325faa5f6bc179649406058253d91ceda359c104dac0ad320e1391", size = 20924451 },
{ url = "https://files.pythonhosted.org/packages/6a/ec/6ea85b2da9d5dfa1dbb4cb3c76587fc8ddcae580cb1262303ab21c0926c4/numpy-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9491100aba630910489c1d0158034e1c9a6546f0b1340f716d522dc103788e39", size = 14122390 },
{ url = "https://files.pythonhosted.org/packages/68/05/bfbdf490414a7dbaf65b10c78bc243f312c4553234b6d91c94eb7c4b53c2/numpy-2.2.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:41184c416143defa34cc8eb9d070b0a5ba4f13a0fa96a709e20584638254b317", size = 5156590 },
{ url = "https://files.pythonhosted.org/packages/f7/ec/fe2e91b2642b9d6544518388a441bcd65c904cea38d9ff998e2e8ebf808e/numpy-2.2.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7dca87ca328f5ea7dafc907c5ec100d187911f94825f8700caac0b3f4c384b49", size = 6671958 },
{ url = "https://files.pythonhosted.org/packages/b1/6f/6531a78e182f194d33ee17e59d67d03d0d5a1ce7f6be7343787828d1bd4a/numpy-2.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bc61b307655d1a7f9f4b043628b9f2b721e80839914ede634e3d485913e1fb2", size = 14019950 },
{ url = "https://files.pythonhosted.org/packages/e1/fb/13c58591d0b6294a08cc40fcc6b9552d239d773d520858ae27f39997f2ae/numpy-2.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fad446ad0bc886855ddf5909cbf8cb5d0faa637aaa6277fb4b19ade134ab3c7", size = 16079759 },
{ url = "https://files.pythonhosted.org/packages/2c/f2/f2f8edd62abb4b289f65a7f6d1f3650273af00b91b7267a2431be7f1aec6/numpy-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:149d1113ac15005652e8d0d3f6fd599360e1a708a4f98e43c9c77834a28238cb", size = 15226139 },
{ url = "https://files.pythonhosted.org/packages/aa/29/14a177f1a90b8ad8a592ca32124ac06af5eff32889874e53a308f850290f/numpy-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:106397dbbb1896f99e044efc90360d098b3335060375c26aa89c0d8a97c5f648", size = 17856316 },
{ url = "https://files.pythonhosted.org/packages/95/03/242ae8d7b97f4e0e4ab8dd51231465fb23ed5e802680d629149722e3faf1/numpy-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:0eec19f8af947a61e968d5429f0bd92fec46d92b0008d0a6685b40d6adf8a4f4", size = 6329134 },
{ url = "https://files.pythonhosted.org/packages/80/94/cd9e9b04012c015cb6320ab3bf43bc615e248dddfeb163728e800a5d96f0/numpy-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:97b974d3ba0fb4612b77ed35d7627490e8e3dff56ab41454d9e8b23448940576", size = 12696208 },
]
[[package]]
name = "packaging"
version = "24.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
]
[[package]]
name = "pandas"
version = "2.2.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "python-dateutil" },
{ name = "pytz" },
{ name = "tzdata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893 },
{ url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475 },
{ url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645 },
{ url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445 },
{ url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235 },
{ url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756 },
{ url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 },
{ url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 },
{ url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 },
{ url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 },
{ url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 },
{ url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 },
{ url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 },
{ url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 },
{ url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 },
{ url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 },
{ url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 },
{ url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 },
{ url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 },
{ url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 },
]
[[package]]
name = "pathspec"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
]
[[package]]
name = "platformdirs"
version = "4.3.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
]
[[package]]
name = "pluggy"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
]
[[package]]
name = "propcache"
version = "0.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/20/c8/2a13f78d82211490855b2fb303b6721348d0787fdd9a12ac46d99d3acde1/propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64", size = 41735 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4c/28/1d205fe49be8b1b4df4c50024e62480a442b1a7b818e734308bb0d17e7fb/propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a", size = 79588 },
{ url = "https://files.pythonhosted.org/packages/21/ee/fc4d893f8d81cd4971affef2a6cb542b36617cd1d8ce56b406112cb80bf7/propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0", size = 45825 },
{ url = "https://files.pythonhosted.org/packages/4a/de/bbe712f94d088da1d237c35d735f675e494a816fd6f54e9db2f61ef4d03f/propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d", size = 45357 },
{ url = "https://files.pythonhosted.org/packages/7f/14/7ae06a6cf2a2f1cb382586d5a99efe66b0b3d0c6f9ac2f759e6f7af9d7cf/propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4", size = 241869 },
{ url = "https://files.pythonhosted.org/packages/cc/59/227a78be960b54a41124e639e2c39e8807ac0c751c735a900e21315f8c2b/propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d", size = 247884 },
{ url = "https://files.pythonhosted.org/packages/84/58/f62b4ffaedf88dc1b17f04d57d8536601e4e030feb26617228ef930c3279/propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5", size = 248486 },
{ url = "https://files.pythonhosted.org/packages/1c/07/ebe102777a830bca91bbb93e3479cd34c2ca5d0361b83be9dbd93104865e/propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24", size = 243649 },
{ url = "https://files.pythonhosted.org/packages/ed/bc/4f7aba7f08f520376c4bb6a20b9a981a581b7f2e385fa0ec9f789bb2d362/propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff", size = 229103 },
{ url = "https://files.pythonhosted.org/packages/fe/d5/04ac9cd4e51a57a96f78795e03c5a0ddb8f23ec098b86f92de028d7f2a6b/propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f", size = 226607 },
{ url = "https://files.pythonhosted.org/packages/e3/f0/24060d959ea41d7a7cc7fdbf68b31852331aabda914a0c63bdb0e22e96d6/propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec", size = 221153 },
{ url = "https://files.pythonhosted.org/packages/77/a7/3ac76045a077b3e4de4859a0753010765e45749bdf53bd02bc4d372da1a0/propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348", size = 222151 },
{ url = "https://files.pythonhosted.org/packages/e7/af/5e29da6f80cebab3f5a4dcd2a3240e7f56f2c4abf51cbfcc99be34e17f0b/propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6", size = 233812 },
{ url = "https://files.pythonhosted.org/packages/8c/89/ebe3ad52642cc5509eaa453e9f4b94b374d81bae3265c59d5c2d98efa1b4/propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6", size = 238829 },
{ url = "https://files.pythonhosted.org/packages/e9/2f/6b32f273fa02e978b7577159eae7471b3cfb88b48563b1c2578b2d7ca0bb/propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518", size = 230704 },
{ url = "https://files.pythonhosted.org/packages/5c/2e/f40ae6ff5624a5f77edd7b8359b208b5455ea113f68309e2b00a2e1426b6/propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246", size = 40050 },
{ url = "https://files.pythonhosted.org/packages/3b/77/a92c3ef994e47180862b9d7d11e37624fb1c00a16d61faf55115d970628b/propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1", size = 44117 },
{ url = "https://files.pythonhosted.org/packages/0f/2a/329e0547cf2def8857157f9477669043e75524cc3e6251cef332b3ff256f/propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc", size = 77002 },
{ url = "https://files.pythonhosted.org/packages/12/2d/c4df5415e2382f840dc2ecbca0eeb2293024bc28e57a80392f2012b4708c/propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9", size = 44639 },
{ url = "https://files.pythonhosted.org/packages/d0/5a/21aaa4ea2f326edaa4e240959ac8b8386ea31dedfdaa636a3544d9e7a408/propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439", size = 44049 },
{ url = "https://files.pythonhosted.org/packages/4e/3e/021b6cd86c0acc90d74784ccbb66808b0bd36067a1bf3e2deb0f3845f618/propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536", size = 224819 },
{ url = "https://files.pythonhosted.org/packages/3c/57/c2fdeed1b3b8918b1770a133ba5c43ad3d78e18285b0c06364861ef5cc38/propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629", size = 229625 },
{ url = "https://files.pythonhosted.org/packages/9d/81/70d4ff57bf2877b5780b466471bebf5892f851a7e2ca0ae7ffd728220281/propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b", size = 232934 },
{ url = "https://files.pythonhosted.org/packages/3c/b9/bb51ea95d73b3fb4100cb95adbd4e1acaf2cbb1fd1083f5468eeb4a099a8/propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052", size = 227361 },
{ url = "https://files.pythonhosted.org/packages/f1/20/3c6d696cd6fd70b29445960cc803b1851a1131e7a2e4ee261ee48e002bcd/propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce", size = 213904 },
{ url = "https://files.pythonhosted.org/packages/a1/cb/1593bfc5ac6d40c010fa823f128056d6bc25b667f5393781e37d62f12005/propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d", size = 212632 },
{ url = "https://files.pythonhosted.org/packages/6d/5c/e95617e222be14a34c709442a0ec179f3207f8a2b900273720501a70ec5e/propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce", size = 207897 },
{ url = "https://files.pythonhosted.org/packages/8e/3b/56c5ab3dc00f6375fbcdeefdede5adf9bee94f1fab04adc8db118f0f9e25/propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95", size = 208118 },
{ url = "https://files.pythonhosted.org/packages/86/25/d7ef738323fbc6ebcbce33eb2a19c5e07a89a3df2fded206065bd5e868a9/propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf", size = 217851 },
{ url = "https://files.pythonhosted.org/packages/b3/77/763e6cef1852cf1ba740590364ec50309b89d1c818e3256d3929eb92fabf/propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f", size = 222630 },
{ url = "https://files.pythonhosted.org/packages/4f/e9/0f86be33602089c701696fbed8d8c4c07b6ee9605c5b7536fd27ed540c5b/propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30", size = 216269 },
{ url = "https://files.pythonhosted.org/packages/cc/02/5ac83217d522394b6a2e81a2e888167e7ca629ef6569a3f09852d6dcb01a/propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6", size = 39472 },
{ url = "https://files.pythonhosted.org/packages/f4/33/d6f5420252a36034bc8a3a01171bc55b4bff5df50d1c63d9caa50693662f/propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1", size = 43363 },
{ url = "https://files.pythonhosted.org/packages/41/b6/c5319caea262f4821995dca2107483b94a3345d4607ad797c76cb9c36bcc/propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54", size = 11818 },
]
[[package]]
name = "pydantic"
version = "2.10.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 },
]
[[package]]
name = "pydantic-core"
version = "2.27.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 },
{ url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 },
{ url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 },
{ url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 },
{ url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 },
{ url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 },
{ url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 },
{ url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 },
{ url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 },
{ url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 },
{ url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 },
{ url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 },
{ url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 },
{ url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 },
{ url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 },
{ url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 },
{ url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 },
{ url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 },
{ url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 },
{ url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 },
{ url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 },
{ url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 },
{ url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 },
{ url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 },
{ url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 },
{ url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 },
{ url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 },
{ url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 },
]
[[package]]
name = "pydantic-settings"
version = "2.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/73/7b/c58a586cd7d9ac66d2ee4ba60ca2d241fa837c02bca9bea80a9a8c3d22a9/pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93", size = 79920 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/46/93416fdae86d40879714f72956ac14df9c7b76f7d41a4d68aa9f71a0028b/pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd", size = 29718 },
]
[[package]]
name = "pytest"
version = "8.3.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 },
]
[[package]]
name = "pytest-assertcount"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/84/9f/e6eee30903096887e2fa359163a4ed0969dfd81b301400f5c46fe3e99a41/pytest-assertcount-1.0.0.tar.gz", hash = "sha256:e8271a284e337f89a28789790e68faa18ab8d5e1ca9aad5bd439a672b836a76b", size = 2652 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/8d/4415383d305c7d79271e8e349dedc62d7a2a00f997d894b12a18b3ec487a/pytest_assertcount-1.0.0-py3-none-any.whl", hash = "sha256:08a2945248f666787ee1413736fff91875e355aee23bf03900d1438bd4bf81ec", size = 3287 },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
]
[[package]]
name = "python-dotenv"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
]
[[package]]
name = "python-levenshtein"
version = "0.26.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "levenshtein" },
]
sdist = { url = "https://files.pythonhosted.org/packages/31/72/58d77cb80b3c130d94f53a8204ffad9acfddb925b2fb5818ff9af0b3c832/python_levenshtein-0.26.1.tar.gz", hash = "sha256:24ba578e28058ebb4afa2700057e1678d7adf27e43cd1f17700c09a9009d5d3a", size = 12276 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/d7/03e0453719ed89724664f781f0255949408118093dbf77a2aa2a1198b38e/python_Levenshtein-0.26.1-py3-none-any.whl", hash = "sha256:8ef5e529dd640fb00f05ee62d998d2ee862f19566b641ace775d5ae16167b2ef", size = 9426 },
]
[[package]]
name = "pytz"
version = "2025.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5f/57/df1c9157c8d5a05117e455d66fd7cf6dbc46974f832b1058ed4856785d8a/pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e", size = 319617 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/eb/38/ac33370d784287baa1c3d538978b5e2ea064d4c1b93ffbd12826c190dd10/pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57", size = 507930 },
]
[[package]]
name = "rapidfuzz"
version = "3.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c9/df/c300ead8c2962f54ad87872e6372a6836f0181a7f20b433c987bd106bfce/rapidfuzz-3.12.1.tar.gz", hash = "sha256:6a98bbca18b4a37adddf2d8201856441c26e9c981d8895491b5bc857b5f780eb", size = 57907552 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/20/6049061411df87f2814a2677db0f15e673bb9795bfeff57dc9708121374d/rapidfuzz-3.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f6235b57ae3faa3f85cb3f90c9fee49b21bd671b76e90fc99e8ca2bdf0b5e4a3", size = 1944328 },
{ url = "https://files.pythonhosted.org/packages/25/73/199383c4c21ae3b4b6ea6951c6896ab38e9dc96942462fa01f9d3fb047da/rapidfuzz-3.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af4585e5812632c357fee5ab781c29f00cd06bea58f8882ff244cc4906ba6c9e", size = 1430203 },
{ url = "https://files.pythonhosted.org/packages/7b/51/77ebaeec5413c53c3e6d8b800f2b979551adbed7b5efa094d1fad5c5b751/rapidfuzz-3.12.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5942dc4460e5030c5f9e1d4c9383de2f3564a2503fe25e13e89021bcbfea2f44", size = 1403662 },
{ url = "https://files.pythonhosted.org/packages/54/06/1fadd2704db0a7eecf78de812e2f4fab37c4ae105a5ce4578c9fc66bb0c5/rapidfuzz-3.12.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b31ab59e1a0df5afc21f3109b6cfd77b34040dbf54f1bad3989f885cfae1e60", size = 5555849 },
{ url = "https://files.pythonhosted.org/packages/19/45/da128c3952bd09cef2935df58db5273fc4eb67f04a69dcbf9e25af9e4432/rapidfuzz-3.12.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97c885a7a480b21164f57a706418c9bbc9a496ec6da087e554424358cadde445", size = 1655273 },
{ url = "https://files.pythonhosted.org/packages/03/ee/bf2b2a95b5af4e6d36105dd9284dc5335fdcc7f0326186d4ab0b5aa4721e/rapidfuzz-3.12.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d844c0587d969ce36fbf4b7cbf0860380ffeafc9ac5e17a7cbe8abf528d07bb", size = 1678041 },
{ url = "https://files.pythonhosted.org/packages/7f/4f/36ea4d7f306a23e30ea1a6cabf545d2a794e8ca9603d2ee48384314cde3a/rapidfuzz-3.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93c95dce8917bf428064c64024de43ffd34ec5949dd4425780c72bd41f9d969", size = 3137099 },
{ url = "https://files.pythonhosted.org/packages/70/ef/48195d94b018e7340a60c9a642ab0081bf9dc64fb0bd01dfafd93757d2a2/rapidfuzz-3.12.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:834f6113d538af358f39296604a1953e55f8eeffc20cb4caf82250edbb8bf679", size = 2307388 },
{ url = "https://files.pythonhosted.org/packages/e5/cd/53d5dbc4791df3e1a8640fc4ad5e328ebb040cc01c10c66f891aa6b83ed5/rapidfuzz-3.12.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a940aa71a7f37d7f0daac186066bf6668d4d3b7e7ef464cb50bc7ba89eae1f51", size = 6906504 },
{ url = "https://files.pythonhosted.org/packages/1b/99/c27e7db1d49cfd77780cb73978f81092682c2bdbc6de75363df6aaa086d6/rapidfuzz-3.12.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ec9eaf73501c9a7de2c6938cb3050392e2ee0c5ca3921482acf01476b85a7226", size = 2684757 },
{ url = "https://files.pythonhosted.org/packages/02/8c/2474d6282fdd4aae386a6b16272e544a3f9ea2dcdcf2f3b0b286549bc3d5/rapidfuzz-3.12.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c5ec360694ac14bfaeb6aea95737cf1a6cf805b5fe8ea7fd28814706c7fa838", size = 3229940 },
{ url = "https://files.pythonhosted.org/packages/ac/27/95d5a8ebe5fcc5462dd0fd265553c8a2ec4a770e079afabcff978442bcb3/rapidfuzz-3.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b5e176524653ac46f1802bdd273a4b44a5f8d0054ed5013a8e8a4b72f254599", size = 4148489 },
{ url = "https://files.pythonhosted.org/packages/8d/2c/e509bc24b6514de4d6f2c5480201568e1d9a3c7e4692cc969ef899227ba5/rapidfuzz-3.12.1-cp312-cp312-win32.whl", hash = "sha256:6f463c6f1c42ec90e45d12a6379e18eddd5cdf74138804d8215619b6f4d31cea", size = 1834110 },
{ url = "https://files.pythonhosted.org/packages/cc/ab/900b8d57090b30269258e3ae31752ec9c31042cd58660fcc96d50728487d/rapidfuzz-3.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:b894fa2b30cd6498a29e5c470cb01c6ea898540b7e048a0342775a5000531334", size = 1612461 },
{ url = "https://files.pythonhosted.org/packages/a0/df/3f51a0a277185b3f28b2941e071aff62908a6b81527efc67a643bcb59fb8/rapidfuzz-3.12.1-cp312-cp312-win_arm64.whl", hash = "sha256:43bb17056c5d1332f517b888c4e57846c4b5f936ed304917eeb5c9ac85d940d4", size = 864251 },
{ url = "https://files.pythonhosted.org/packages/62/d2/ceebc2446d1f3d3f2cae2597116982e50c2eed9ff2f5a322a51736981405/rapidfuzz-3.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:97f824c15bc6933a31d6e3cbfa90188ba0e5043cf2b6dd342c2b90ee8b3fd47c", size = 1936794 },
{ url = "https://files.pythonhosted.org/packages/88/38/37f7ea800aa959a4f7a63477fc9ad7f3cd024e46bfadce5d23420af6c7e5/rapidfuzz-3.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a973b3f5cabf931029a3ae4a0f72e3222e53d412ea85fc37ddc49e1774f00fbf", size = 1424155 },
{ url = "https://files.pythonhosted.org/packages/3f/14/409d0aa84430451488177fcc5cba8babcdf5a45cee772a2a265b9b5f4c7e/rapidfuzz-3.12.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df7880e012228722dec1be02b9ef3898ed023388b8a24d6fa8213d7581932510", size = 1398013 },
{ url = "https://files.pythonhosted.org/packages/4b/2c/601e3ad0bbe61e65f99e72c8cefed9713606cf4b297cc4c3876051db7722/rapidfuzz-3.12.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c78582f50e75e6c2bc38c791ed291cb89cf26a3148c47860c1a04d6e5379c8e", size = 5526157 },
{ url = "https://files.pythonhosted.org/packages/97/ce/deb7b00ce6e06713fc4df81336402b7fa062f2393c8a47401c228ee906c3/rapidfuzz-3.12.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2d7d9e6a04d8344b0198c96394c28874086888d0a2b2f605f30d1b27b9377b7d", size = 1648446 },
{ url = "https://files.pythonhosted.org/packages/ec/6f/2b8eae1748a022290815999594b438dbc1e072c38c76178ea996920a6253/rapidfuzz-3.12.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5620001fd4d6644a2f56880388179cc8f3767670f0670160fcb97c3b46c828af", size = 1676038 },
{ url = "https://files.pythonhosted.org/packages/b9/6c/5c831197aca7148ed85c86bbe940e66073fea0fa97f30307bb5850ed8858/rapidfuzz-3.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0666ab4c52e500af7ba5cc17389f5d15c0cdad06412c80312088519fdc25686d", size = 3114137 },
{ url = "https://files.pythonhosted.org/packages/fc/f2/d66ac185eeb0ee3fc0fe208dab1e72feece2c883bc0ab2097570a8159a7b/rapidfuzz-3.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:27b4d440fa50b50c515a91a01ee17e8ede719dca06eef4c0cccf1a111a4cfad3", size = 2305754 },
{ url = "https://files.pythonhosted.org/packages/6c/61/9bf74d7ea9bebc7a1bed707591617bba7901fce414d346a7c5532ef02dbd/rapidfuzz-3.12.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83dccfd5a754f2a0e8555b23dde31f0f7920601bfa807aa76829391ea81e7c67", size = 6901746 },
{ url = "https://files.pythonhosted.org/packages/81/73/d8dddf73e168f723ef21272e8abb7d34d9244da395eb90ed5a617f870678/rapidfuzz-3.12.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b572b634740e047c53743ed27a1bb3b4f93cf4abbac258cd7af377b2c4a9ba5b", size = 2673947 },
{ url = "https://files.pythonhosted.org/packages/2e/31/3c473cea7d76af162819a5b84f5e7bdcf53b9e19568fc37cfbdab4f4512a/rapidfuzz-3.12.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7fa7b81fb52902d5f78dac42b3d6c835a6633b01ddf9b202a3ca8443be4b2d6a", size = 3233070 },
{ url = "https://files.pythonhosted.org/packages/c0/b7/73227dcbf8586f0ca4a77be2720311367288e2db142ae00a1404f42e712d/rapidfuzz-3.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1d4fbff980cb6baef4ee675963c081f7b5d6580a105d6a4962b20f1f880e1fb", size = 4146828 },
{ url = "https://files.pythonhosted.org/packages/3a/c8/fea749c662e268d348a77501995b51ac95cdc3624f3f95ba261f30b000ff/rapidfuzz-3.12.1-cp313-cp313-win32.whl", hash = "sha256:3fe8da12ea77271097b303fa7624cfaf5afd90261002314e3b0047d36f4afd8d", size = 1831797 },
{ url = "https://files.pythonhosted.org/packages/66/18/11052be5984d9972eb04a52e2931e19e95b2e87731d179f60b79707b7efd/rapidfuzz-3.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:6f7e92fc7d2a7f02e1e01fe4f539324dfab80f27cb70a30dd63a95445566946b", size = 1610169 },
{ url = "https://files.pythonhosted.org/packages/db/c1/66427c618f000298edbd24e46dd3dd2d3fa441a602701ba6a260d41dd62b/rapidfuzz-3.12.1-cp313-cp313-win_arm64.whl", hash = "sha256:e31be53d7f4905a6a038296d8b773a79da9ee9f0cd19af9490c5c5a22e37d2e5", size = 863036 },
]
[[package]]
name = "requests"
version = "2.32.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
]
[[package]]
name = "requests-toolbelt"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 },
]
[[package]]
name = "ruff"
version = "0.9.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2a/e1/e265aba384343dd8ddd3083f5e33536cd17e1566c41453a5517b5dd443be/ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9", size = 3639454 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/e3/3d2c022e687e18cf5d93d6bfa2722d46afc64eaa438c7fbbdd603b3597be/ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba", size = 11714128 },
{ url = "https://files.pythonhosted.org/packages/e1/22/aff073b70f95c052e5c58153cba735748c9e70107a77d03420d7850710a0/ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504", size = 11682539 },
{ url = "https://files.pythonhosted.org/packages/75/a7/f5b7390afd98a7918582a3d256cd3e78ba0a26165a467c1820084587cbf9/ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83", size = 11132512 },
{ url = "https://files.pythonhosted.org/packages/a6/e3/45de13ef65047fea2e33f7e573d848206e15c715e5cd56095589a7733d04/ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc", size = 11929275 },
{ url = "https://files.pythonhosted.org/packages/7d/f2/23d04cd6c43b2e641ab961ade8d0b5edb212ecebd112506188c91f2a6e6c/ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b", size = 11466502 },
{ url = "https://files.pythonhosted.org/packages/b5/6f/3a8cf166f2d7f1627dd2201e6cbc4cb81f8b7d58099348f0c1ff7b733792/ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e", size = 12676364 },
{ url = "https://files.pythonhosted.org/packages/f5/c4/db52e2189983c70114ff2b7e3997e48c8318af44fe83e1ce9517570a50c6/ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666", size = 13335518 },
{ url = "https://files.pythonhosted.org/packages/66/44/545f8a4d136830f08f4d24324e7db957c5374bf3a3f7a6c0bc7be4623a37/ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5", size = 12823287 },
{ url = "https://files.pythonhosted.org/packages/c5/26/8208ef9ee7431032c143649a9967c3ae1aae4257d95e6f8519f07309aa66/ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5", size = 14592374 },
{ url = "https://files.pythonhosted.org/packages/31/70/e917781e55ff39c5b5208bda384fd397ffd76605e68544d71a7e40944945/ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217", size = 12500173 },
{ url = "https://files.pythonhosted.org/packages/84/f5/e4ddee07660f5a9622a9c2b639afd8f3104988dc4f6ba0b73ffacffa9a8c/ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6", size = 11906555 },
{ url = "https://files.pythonhosted.org/packages/f1/2b/6ff2fe383667075eef8656b9892e73dd9b119b5e3add51298628b87f6429/ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897", size = 11538958 },
{ url = "https://files.pythonhosted.org/packages/3c/db/98e59e90de45d1eb46649151c10a062d5707b5b7f76f64eb1e29edf6ebb1/ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08", size = 12117247 },
{ url = "https://files.pythonhosted.org/packages/ec/bc/54e38f6d219013a9204a5a2015c09e7a8c36cedcd50a4b01ac69a550b9d9/ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656", size = 12554647 },
{ url = "https://files.pythonhosted.org/packages/a5/7d/7b461ab0e2404293c0627125bb70ac642c2e8d55bf590f6fce85f508f1b2/ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d", size = 9949214 },
{ url = "https://files.pythonhosted.org/packages/ee/30/c3cee10f915ed75a5c29c1e57311282d1a15855551a64795c1b2bbe5cf37/ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa", size = 10999914 },
{ url = "https://files.pythonhosted.org/packages/e8/a8/d71f44b93e3aa86ae232af1f2126ca7b95c0f515ec135462b3e1f351441c/ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a", size = 10177499 },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
[[package]]
name = "speckle-automate-checker"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "more-itertools" },
{ name = "pandas" },
{ name = "pydantic" },
{ name = "pytest-assertcount" },
{ name = "python-dotenv" },
{ name = "python-levenshtein" },
{ name = "specklepy" },
]
[package.dev-dependencies]
dev = [
{ name = "black" },
{ name = "mypy" },
{ name = "pydantic-settings" },
{ name = "pytest" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "more-itertools", specifier = ">=10.6.0" },
{ name = "pandas", specifier = ">=2.2.3" },
{ name = "pydantic", specifier = "==2.10.6" },
{ name = "pytest-assertcount", specifier = ">=1.0.0" },
{ name = "python-dotenv", specifier = ">=1.0.1" },
{ name = "python-levenshtein", specifier = ">=0.26.1" },
{ name = "specklepy", specifier = ">=2.21.2" },
]
[package.metadata.requires-dev]
dev = [
{ name = "black", specifier = ">=25.1.0" },
{ name = "mypy", specifier = ">=1.15.0" },
{ name = "pydantic-settings", specifier = ">=2.7.1" },
{ name = "pytest", specifier = ">=8.3.4" },
{ name = "ruff", specifier = ">=0.9.6" },
]
[[package]]
name = "specklepy"
version = "2.21.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "appdirs" },
{ name = "attrs" },
{ name = "deprecated" },
{ name = "gql", extra = ["requests", "websockets"] },
{ name = "httpx" },
{ name = "pydantic" },
{ name = "stringcase" },
{ name = "ujson" },
]
sdist = { url = "https://files.pythonhosted.org/packages/18/79/58269be683ca06194a464deaac3655989b094c6a0df0f8dccb24eeb47031/specklepy-2.21.2.tar.gz", hash = "sha256:ba44bb9167ced32e52ba7e5717d0b1d0ece1aa23ca02243ce75f0eaba08e8107", size = 93326 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/89/78eb05af38fc455c695c2ab882db2cca0ef518313c49f73b6536d0cad9e7/specklepy-2.21.2-py3-none-any.whl", hash = "sha256:bb34bb0de56f8731f88ac9a1ada4cddfbf2d71e52d4e9cce07bab95c1dac2b2d", size = 136495 },
]
[[package]]
name = "stringcase"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/1f/1241aa3d66e8dc1612427b17885f5fcd9c9ee3079fc0d28e9a3aeeb36fa3/stringcase-1.2.0.tar.gz", hash = "sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008", size = 2958 }
[[package]]
name = "typing-extensions"
version = "4.12.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
]
[[package]]
name = "tzdata"
version = "2025.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/0f/fa4723f22942480be4ca9527bbde8d43f6c3f2fe8412f00e7f5f6746bc8b/tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694", size = 194950 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/dd/84f10e23edd882c6f968c21c2434fe67bd4a528967067515feca9e611e5e/tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639", size = 346762 },
]
[[package]]
name = "ujson"
version = "5.10.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f0/00/3110fd566786bfa542adb7932d62035e0c0ef662a8ff6544b6643b3d6fd7/ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1", size = 7154885 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/a6/fd3f8bbd80842267e2d06c3583279555e8354c5986c952385199d57a5b6c/ujson-5.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5", size = 55642 },
{ url = "https://files.pythonhosted.org/packages/a8/47/dd03fd2b5ae727e16d5d18919b383959c6d269c7b948a380fdd879518640/ujson-5.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e", size = 51807 },
{ url = "https://files.pythonhosted.org/packages/25/23/079a4cc6fd7e2655a473ed9e776ddbb7144e27f04e8fc484a0fb45fe6f71/ujson-5.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043", size = 51972 },
{ url = "https://files.pythonhosted.org/packages/04/81/668707e5f2177791869b624be4c06fb2473bf97ee33296b18d1cf3092af7/ujson-5.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1", size = 53686 },
{ url = "https://files.pythonhosted.org/packages/bd/50/056d518a386d80aaf4505ccf3cee1c40d312a46901ed494d5711dd939bc3/ujson-5.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3", size = 58591 },
{ url = "https://files.pythonhosted.org/packages/fc/d6/aeaf3e2d6fb1f4cfb6bf25f454d60490ed8146ddc0600fae44bfe7eb5a72/ujson-5.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21", size = 997853 },
{ url = "https://files.pythonhosted.org/packages/f8/d5/1f2a5d2699f447f7d990334ca96e90065ea7f99b142ce96e85f26d7e78e2/ujson-5.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2", size = 1140689 },
{ url = "https://files.pythonhosted.org/packages/f2/2c/6990f4ccb41ed93744aaaa3786394bca0875503f97690622f3cafc0adfde/ujson-5.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e", size = 1043576 },
{ url = "https://files.pythonhosted.org/packages/14/f5/a2368463dbb09fbdbf6a696062d0c0f62e4ae6fa65f38f829611da2e8fdd/ujson-5.10.0-cp312-cp312-win32.whl", hash = "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e", size = 38764 },
{ url = "https://files.pythonhosted.org/packages/59/2d/691f741ffd72b6c84438a93749ac57bf1a3f217ac4b0ea4fd0e96119e118/ujson-5.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc", size = 42211 },
{ url = "https://files.pythonhosted.org/packages/0d/69/b3e3f924bb0e8820bb46671979770c5be6a7d51c77a66324cdb09f1acddb/ujson-5.10.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287", size = 55646 },
{ url = "https://files.pythonhosted.org/packages/32/8a/9b748eb543c6cabc54ebeaa1f28035b1bd09c0800235b08e85990734c41e/ujson-5.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e", size = 51806 },
{ url = "https://files.pythonhosted.org/packages/39/50/4b53ea234413b710a18b305f465b328e306ba9592e13a791a6a6b378869b/ujson-5.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557", size = 51975 },
{ url = "https://files.pythonhosted.org/packages/b4/9d/8061934f960cdb6dd55f0b3ceeff207fcc48c64f58b43403777ad5623d9e/ujson-5.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988", size = 53693 },
{ url = "https://files.pythonhosted.org/packages/f5/be/7bfa84b28519ddbb67efc8410765ca7da55e6b93aba84d97764cd5794dbc/ujson-5.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816", size = 58594 },
{ url = "https://files.pythonhosted.org/packages/48/eb/85d465abafb2c69d9699cfa5520e6e96561db787d36c677370e066c7e2e7/ujson-5.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20", size = 997853 },
{ url = "https://files.pythonhosted.org/packages/9f/76/2a63409fc05d34dd7d929357b7a45e3a2c96f22b4225cd74becd2ba6c4cb/ujson-5.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0", size = 1140694 },
{ url = "https://files.pythonhosted.org/packages/45/ed/582c4daba0f3e1688d923b5cb914ada1f9defa702df38a1916c899f7c4d1/ujson-5.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f", size = 1043580 },
{ url = "https://files.pythonhosted.org/packages/d7/0c/9837fece153051e19c7bade9f88f9b409e026b9525927824cdf16293b43b/ujson-5.10.0-cp313-cp313-win32.whl", hash = "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165", size = 38766 },
{ url = "https://files.pythonhosted.org/packages/d7/72/6cb6728e2738c05bbe9bd522d6fc79f86b9a28402f38663e85a28fddd4a0/ujson-5.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539", size = 42212 },
]
[[package]]
name = "urllib3"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
]
[[package]]
name = "websockets"
version = "11.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/3b/2ed38e52eed4cf277f9df5f0463a99199a04d9e29c9e227cfafa57bd3993/websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016", size = 104235 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/47/96/9d5749106ff57629b54360664ae7eb9afd8302fad1680ead385383e33746/websockets-11.0.3-py3-none-any.whl", hash = "sha256:6681ba9e7f8f3b19440921e99efbb40fc89f26cd71bf539e45d8c8a25c976dc6", size = 118056 },
]
[[package]]
name = "wrapt"
version = "1.17.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799 },
{ url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821 },
{ url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919 },
{ url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721 },
{ url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899 },
{ url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222 },
{ url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707 },
{ url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685 },
{ url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567 },
{ url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672 },
{ url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865 },
{ url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800 },
{ url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824 },
{ url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920 },
{ url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690 },
{ url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861 },
{ url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174 },
{ url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721 },
{ url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763 },
{ url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585 },
{ url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676 },
{ url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871 },
{ url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312 },
{ url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062 },
{ url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155 },
{ url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471 },
{ url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208 },
{ url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339 },
{ url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232 },
{ url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476 },
{ url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377 },
{ url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986 },
{ url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750 },
{ url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 },
]
[[package]]
name = "yarl"
version = "1.18.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "multidict" },
{ name = "propcache" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/33/85/bd2e2729752ff4c77338e0102914897512e92496375e079ce0150a6dc306/yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50", size = 142644 },
{ url = "https://files.pythonhosted.org/packages/ff/74/1178322cc0f10288d7eefa6e4a85d8d2e28187ccab13d5b844e8b5d7c88d/yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576", size = 94962 },
{ url = "https://files.pythonhosted.org/packages/be/75/79c6acc0261e2c2ae8a1c41cf12265e91628c8c58ae91f5ff59e29c0787f/yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640", size = 92795 },
{ url = "https://files.pythonhosted.org/packages/6b/32/927b2d67a412c31199e83fefdce6e645247b4fb164aa1ecb35a0f9eb2058/yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2", size = 332368 },
{ url = "https://files.pythonhosted.org/packages/19/e5/859fca07169d6eceeaa4fde1997c91d8abde4e9a7c018e371640c2da2b71/yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75", size = 342314 },
{ url = "https://files.pythonhosted.org/packages/08/75/76b63ccd91c9e03ab213ef27ae6add2e3400e77e5cdddf8ed2dbc36e3f21/yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512", size = 341987 },
{ url = "https://files.pythonhosted.org/packages/1a/e1/a097d5755d3ea8479a42856f51d97eeff7a3a7160593332d98f2709b3580/yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba", size = 336914 },
{ url = "https://files.pythonhosted.org/packages/0b/42/e1b4d0e396b7987feceebe565286c27bc085bf07d61a59508cdaf2d45e63/yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb", size = 325765 },
{ url = "https://files.pythonhosted.org/packages/7e/18/03a5834ccc9177f97ca1bbb245b93c13e58e8225276f01eedc4cc98ab820/yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272", size = 344444 },
{ url = "https://files.pythonhosted.org/packages/c8/03/a713633bdde0640b0472aa197b5b86e90fbc4c5bc05b727b714cd8a40e6d/yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6", size = 340760 },
{ url = "https://files.pythonhosted.org/packages/eb/99/f6567e3f3bbad8fd101886ea0276c68ecb86a2b58be0f64077396cd4b95e/yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e", size = 346484 },
{ url = "https://files.pythonhosted.org/packages/8e/a9/84717c896b2fc6cb15bd4eecd64e34a2f0a9fd6669e69170c73a8b46795a/yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb", size = 359864 },
{ url = "https://files.pythonhosted.org/packages/1e/2e/d0f5f1bef7ee93ed17e739ec8dbcb47794af891f7d165fa6014517b48169/yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393", size = 364537 },
{ url = "https://files.pythonhosted.org/packages/97/8a/568d07c5d4964da5b02621a517532adb8ec5ba181ad1687191fffeda0ab6/yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285", size = 357861 },
{ url = "https://files.pythonhosted.org/packages/7d/e3/924c3f64b6b3077889df9a1ece1ed8947e7b61b0a933f2ec93041990a677/yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2", size = 84097 },
{ url = "https://files.pythonhosted.org/packages/34/45/0e055320daaabfc169b21ff6174567b2c910c45617b0d79c68d7ab349b02/yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477", size = 90399 },
{ url = "https://files.pythonhosted.org/packages/30/c7/c790513d5328a8390be8f47be5d52e141f78b66c6c48f48d241ca6bd5265/yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb", size = 140789 },
{ url = "https://files.pythonhosted.org/packages/30/aa/a2f84e93554a578463e2edaaf2300faa61c8701f0898725842c704ba5444/yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa", size = 94144 },
{ url = "https://files.pythonhosted.org/packages/c6/fc/d68d8f83714b221a85ce7866832cba36d7c04a68fa6a960b908c2c84f325/yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782", size = 91974 },
{ url = "https://files.pythonhosted.org/packages/56/4e/d2563d8323a7e9a414b5b25341b3942af5902a2263d36d20fb17c40411e2/yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0", size = 333587 },
{ url = "https://files.pythonhosted.org/packages/25/c9/cfec0bc0cac8d054be223e9f2c7909d3e8442a856af9dbce7e3442a8ec8d/yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482", size = 344386 },
{ url = "https://files.pythonhosted.org/packages/ab/5d/4c532190113b25f1364d25f4c319322e86232d69175b91f27e3ebc2caf9a/yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186", size = 345421 },
{ url = "https://files.pythonhosted.org/packages/23/d1/6cdd1632da013aa6ba18cee4d750d953104a5e7aac44e249d9410a972bf5/yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58", size = 339384 },
{ url = "https://files.pythonhosted.org/packages/9a/c4/6b3c39bec352e441bd30f432cda6ba51681ab19bb8abe023f0d19777aad1/yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53", size = 326689 },
{ url = "https://files.pythonhosted.org/packages/23/30/07fb088f2eefdc0aa4fc1af4e3ca4eb1a3aadd1ce7d866d74c0f124e6a85/yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2", size = 345453 },
{ url = "https://files.pythonhosted.org/packages/63/09/d54befb48f9cd8eec43797f624ec37783a0266855f4930a91e3d5c7717f8/yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8", size = 341872 },
{ url = "https://files.pythonhosted.org/packages/91/26/fd0ef9bf29dd906a84b59f0cd1281e65b0c3e08c6aa94b57f7d11f593518/yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1", size = 347497 },
{ url = "https://files.pythonhosted.org/packages/d9/b5/14ac7a256d0511b2ac168d50d4b7d744aea1c1aa20c79f620d1059aab8b2/yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a", size = 359981 },
{ url = "https://files.pythonhosted.org/packages/ca/b3/d493221ad5cbd18bc07e642894030437e405e1413c4236dd5db6e46bcec9/yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10", size = 366229 },
{ url = "https://files.pythonhosted.org/packages/04/56/6a3e2a5d9152c56c346df9b8fb8edd2c8888b1e03f96324d457e5cf06d34/yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8", size = 360383 },
{ url = "https://files.pythonhosted.org/packages/fd/b7/4b3c7c7913a278d445cc6284e59b2e62fa25e72758f888b7a7a39eb8423f/yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d", size = 310152 },
{ url = "https://files.pythonhosted.org/packages/f5/d5/688db678e987c3e0fb17867970700b92603cadf36c56e5fb08f23e822a0c/yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c", size = 315723 },
{ url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109 },
]