Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c49948081 | |||
| 3f5880156b | |||
| 6032306cc2 | |||
| c7171a54cb | |||
| 0019667302 | |||
| 129132dd3a | |||
| f902f9c23f | |||
| 7158d0576d | |||
| bb87a7b932 | |||
| f1c4e65d72 | |||
| 1fa7bcb31a | |||
| 66312e1cdd | |||
| 38d2073dbb | |||
| 091a272185 | |||
| 0e95f3998a | |||
| 05a5383060 | |||
| f3c56a48b5 | |||
| a704aded80 | |||
| 90c5051fc6 | |||
| ec6bdf3485 | |||
| ceaa75d40a | |||
| 0566f7d890 | |||
| b431662031 | |||
| e520d9bc91 | |||
| b6dcfe57df | |||
| ba8443ce92 | |||
| 0bab18d2f2 | |||
| dffb7ea7ba | |||
| 4420fd31f4 | |||
| 168a1f517a | |||
| e49bf225ec | |||
| f3987fced9 | |||
| 1ae3372f42 | |||
| b071380a4f | |||
| 460b21772a |
@@ -0,0 +1,30 @@
|
||||
# Use the official Python 3.13 slim image as the base
|
||||
FROM python:3.13-slim
|
||||
|
||||
# Change to UK mirror for better reliability (robust for missing files)
|
||||
RUN find /etc/apt/ -name '*.list' -exec sed -i 's|http://deb.debian.org|http://ftp.uk.debian.org|g' {} + || true
|
||||
|
||||
# Force apt to use IPv4 to avoid CDN/network issues
|
||||
RUN echo 'Acquire::ForceIPv4 "true";' > /etc/apt/apt.conf.d/99force-ipv4
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /home/speckle
|
||||
|
||||
# Create a non-root user
|
||||
RUN useradd -ms /bin/bash vscode
|
||||
USER vscode
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONPATH=/home/speckle
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
# Install Python dependencies
|
||||
COPY requirements.txt requirements-dev.txt pyproject.toml ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt && \
|
||||
pip install --no-cache-dir -r requirements-dev.txt && \
|
||||
echo 'export PATH=$PATH:$HOME/.local/bin' >> ~/.bashrc
|
||||
@@ -1,43 +1,49 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/python
|
||||
{
|
||||
"name": "Python 3",
|
||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||
"image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers-contrib/features/poetry:2": {}
|
||||
},
|
||||
|
||||
"remoteEnv": {
|
||||
"SPECKLE_TOKEN": "foobar"
|
||||
},
|
||||
"containerEnv": {
|
||||
"SPECKLE_TOKEN": "asdfasdf"
|
||||
},
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
"postCreateCommand": "cp .env.example .env && POETRY_VIRTUALENVS_IN_PROJECT=true poetry install --no-root",
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"name": "Model Checker - An Automate Function",
|
||||
"dockerFile": "Dockerfile",
|
||||
"context": "..",
|
||||
"workspaceFolder": "/home/speckle",
|
||||
"runArgs": [
|
||||
"--network",
|
||||
"host"
|
||||
],
|
||||
"mounts": [
|
||||
"source=${localWorkspaceFolder},target=/home/speckle,type=bind,consistency=cached"
|
||||
],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
// Add the IDs of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"ms-python.vscode-pylance",
|
||||
"ms-python.python",
|
||||
"ms-python.vscode-pylance",
|
||||
"ms-python.black-formatter",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"mikestead.dotenv"
|
||||
]
|
||||
"ms-python.isort",
|
||||
"ms-python.flake8",
|
||||
"littlefoxteam.vscode-python-test-adapter",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"charliermarsh.ruff"
|
||||
],
|
||||
"settings": {
|
||||
"python.defaultInterpreterPath": "/usr/local/bin/python",
|
||||
"python.linting.enabled": true,
|
||||
"python.linting.flake8Enabled": true,
|
||||
"python.formatting.provider": "black",
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.nosetestsEnabled": false,
|
||||
"python.testing.pytestArgs": [
|
||||
"tests"
|
||||
],
|
||||
"python.testing.autoTestDiscoverOnSaveEnabled": true,
|
||||
"python.testing.cwd": "${workspaceFolder}",
|
||||
"[python]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
}
|
||||
},
|
||||
"postCreateCommand": "sh -c \"mkdir -p ~/.pip && echo '[global]\nprefer-ipv4 = true' > ~/.pip/pip.conf\"",
|
||||
"postStartCommand": "echo 'Container started successfully!'"
|
||||
}
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
steps:
|
||||
# Step 1: Checkout the repository
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v6.0.2
|
||||
|
||||
# Step 2: Set up Python
|
||||
- name: Setup Python
|
||||
@@ -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
|
||||
@@ -42,3 +44,4 @@ jobs:
|
||||
speckle_function_id: ${{ secrets.SPECKLE_FUNCTION_ID }}
|
||||
speckle_function_input_schema_file_path: ${{ env.FUNCTION_SCHEMA_FILE_NAME }}
|
||||
speckle_function_command: 'python -u main.py run'
|
||||
speckle_function_recommended_memory_mi: 5000
|
||||
|
||||
Generated
+3
@@ -4,4 +4,7 @@
|
||||
<option name="sdkName" value="WSL Checker" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="uv (Checker)" project-jdk-type="Python SDK" />
|
||||
<component name="PythonCompatibilityInspectionAdvertiser">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
</project>
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.12
|
||||
3.13
|
||||
|
||||
Vendored
+24
-2
@@ -5,5 +5,27 @@
|
||||
"stringcase",
|
||||
"typer"
|
||||
],
|
||||
"python.defaultInterpreterPath": ".venv/bin/python"
|
||||
}
|
||||
"python.defaultInterpreterPath": ".venv/bin/python",
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.nosetestsEnabled": false,
|
||||
"python.testing.pytestArgs": [
|
||||
"tests"
|
||||
],
|
||||
"python.testing.autoTestDiscoverOnSaveEnabled": true,
|
||||
"python.testing.cwd": "${workspaceFolder}",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.rulers": [
|
||||
79
|
||||
],
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
"[python]": {
|
||||
"editor.defaultFormatter": "charliermarsh.ruff",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports.ruff": "explicit"
|
||||
}
|
||||
}
|
||||
}
|
||||
+349
-28
@@ -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
|
||||
+9
-10
@@ -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
|
||||
# Use the official Python 3.13 slim image as the base
|
||||
FROM python:3.13-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"]
|
||||
|
||||
@@ -1,36 +1,130 @@
|
||||
# Public Function: Checker
|
||||
# Model Checker
|
||||
|
||||
Validate Speckle objects against configurable rules using spreadsheet definitions.
|
||||
Model Checker is an Automate function that validates Speckle objects against configurable rules. This approach provides
|
||||
a flexible way to implement quality checks and maintain consistent standards across projects.
|
||||
|
||||
## 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 Model Checker allows you to:
|
||||
|
||||
## Rule Types
|
||||
- Define validation rules for your objects
|
||||
- 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. Access the Model Checker Application
|
||||
|
||||
- WARNING: Issues that should be reviewed
|
||||
- ERROR: Critical issues requiring attention
|
||||
1. Go to the [Model Checker Application](https://model-checker.speckle.systems)
|
||||
2. Sign in with your Speckle account
|
||||
3. Create and manage your validation rules through the intuitive web interface
|
||||
|
||||
### 2. Create an Automation
|
||||
|
||||
1. Go to your workspace project in [Speckle](https://app.speckle.systems/)
|
||||
2. Create a new Automation
|
||||
3. Select the Model Checker function
|
||||
4. Configure the function:
|
||||
- Set minimum severity level to report
|
||||
- Configure other options as needed
|
||||
5. Save and run your automation
|
||||
|
||||
## Rule Definition Format
|
||||
|
||||
Rules are defined with the following components:
|
||||
|
||||
| Logic | Property Name | Predicate | Value | Message | Report Severity |
|
||||
|-------|---------------|--------------|-----------|----------------------|-----------------|
|
||||
| WHERE | category | matches | Walls | Wall thickness check | ERROR |
|
||||
| CHECK | Width | greater than | 200 | | |
|
||||
| WHERE | category | matches | Columns | Column height check | WARNING |
|
||||
| AND | height | in range | 2500,4000 | | |
|
||||
|
||||
### Component Explanation
|
||||
|
||||
- **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 Model 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: WHERE category equals "Walls" AND width less than "200"
|
||||
Message: "Walls must have width of at least 200."
|
||||
Severity: ERROR
|
||||
```
|
||||
|
||||
### Door Naming Convention
|
||||
|
||||
```
|
||||
Rule: WHERE category equals "Doors" AND name is not like "^D\d{3}$"
|
||||
Message: "All doors must have a name that follows the format "D" followed by three digits."
|
||||
Severity: WARNING
|
||||
```
|
||||
|
||||
### Structural Column Height Range
|
||||
|
||||
```
|
||||
Rule: WHERE category equals "Columns" AND is_structural is true AND height not in range "2400,4000"
|
||||
Message: "Structural columns must have a height between 2400 and 4000."
|
||||
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/).
|
||||
|
||||
### Alternative: TSV File Format
|
||||
|
||||
While the Model Checker Application is the recommended way to create and manage rules, you can also create compatible
|
||||
TSV (Tab-Separated Values) files manually. This can be useful for:
|
||||
|
||||
- Programmatically generating rules
|
||||
- Version controlling rules in a text format
|
||||
- Integrating with existing workflows
|
||||
- Creating rules in bulk
|
||||
|
||||
The TSV file should follow the same structure as shown in the table above, with columns separated by tabs. The file will
|
||||
then need to be hosted somewhere and served with MIME-type of `text/tab-separated-values` and the URL used in the
|
||||
automation configuration.
|
||||
|
||||
+40
-24
@@ -1,41 +1,57 @@
|
||||
[project]
|
||||
name = "speckle-automate-checker"
|
||||
version = "0.1.0"
|
||||
version = "3.0.0"
|
||||
description = "Allows for QAQC property checking with Speckle"
|
||||
authors = ["Jonathon Broughton <jonathon@speckle.systems>"]
|
||||
authors = [{ name = "Jonathon Broughton", email = "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",
|
||||
"more-itertools>=10.6.0",
|
||||
"pandas>=2.2.3",
|
||||
"pydantic==2.10.6",
|
||||
"python-dotenv>=1.0.1",
|
||||
"python-levenshtein>=0.26.1",
|
||||
"specklepy>=3.0.0",
|
||||
"pydantic-settings>=2.7.1",
|
||||
]
|
||||
|
||||
|
||||
[dependency-groups]
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest-assertcount>=1.0.0",
|
||||
"black>=25.1.0",
|
||||
"mypy>=1.15.0",
|
||||
"pydantic-settings>=2.7.1",
|
||||
"pytest>=8.3.4",
|
||||
"ruff>=0.9.6",
|
||||
"mypy>=1.15.0",
|
||||
"pytest>=8.3.4",
|
||||
"pytest-assertcount>=1.0.0",
|
||||
"ruff==0.11.12",
|
||||
]
|
||||
|
||||
|
||||
[tool.ruff]
|
||||
select = [
|
||||
"E", # pycodestyle
|
||||
"F", # pyflakes
|
||||
"UP", # pyupgrade
|
||||
"D", # pydocstyle
|
||||
"I", # isort
|
||||
"E", # pycodestyle
|
||||
"F", # pyflakes
|
||||
"UP", # pyupgrade
|
||||
"D", # pydocstyle
|
||||
"I", # isort
|
||||
]
|
||||
line-length = 120
|
||||
ignore = ["F401", "F403"]
|
||||
ignore = ["F401", "F403", "E501"]
|
||||
exclude = [".venv", "**/*.yml"]
|
||||
line-length = 79
|
||||
|
||||
[tool.ruff.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "double"
|
||||
indent-style = "space"
|
||||
line-ending = "auto"
|
||||
docstring-code-format = true
|
||||
docstring-code-line-length = 79
|
||||
|
||||
[tool.ruff.isort]
|
||||
known-first-party = ["src"]
|
||||
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
[tool.setuptools]
|
||||
py-modules = []
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
argcomplete==3.6.2
|
||||
click==8.1.8
|
||||
colorama==0.4.6
|
||||
coverage==7.8.2
|
||||
flake8==7.2.0
|
||||
iniconfig==2.1.0
|
||||
isort==6.0.1
|
||||
mccabe==0.7.0
|
||||
mypy_extensions==1.1.0
|
||||
packaging==24.2
|
||||
pathspec==0.12.1
|
||||
pipx==1.7.1
|
||||
platformdirs==4.3.7
|
||||
pluggy==1.6.0
|
||||
pycodestyle==2.13.0
|
||||
pyflakes==3.3.2
|
||||
Pygments==2.19.1
|
||||
pytest>=8.3.4
|
||||
pytest-assertcount>=1.0.0
|
||||
pytest-cov==6.1.1
|
||||
ruff==0.11.12
|
||||
userpath==1.9.2
|
||||
mypy>=1.15.0
|
||||
+6
-53
@@ -1,54 +1,7 @@
|
||||
annotated-types==0.7.0
|
||||
anyio==4.8.0
|
||||
appdirs==1.4.4
|
||||
attrs==23.2.0
|
||||
backoff==2.2.1
|
||||
black==25.1.0
|
||||
certifi==2025.1.31
|
||||
charset-normalizer==3.4.1
|
||||
click==8.1.8
|
||||
colorama==0.4.6
|
||||
deprecated==1.2.18
|
||||
gql==3.5.0
|
||||
graphql-core==3.2.6
|
||||
h11==0.14.0
|
||||
httpcore==1.0.7
|
||||
httpx==0.25.2
|
||||
idna==3.10
|
||||
iniconfig==2.0.0
|
||||
levenshtein==0.26.1
|
||||
more-itertools==10.6.0
|
||||
multidict==6.1.0
|
||||
mypy==1.15.0
|
||||
mypy-extensions==1.0.0
|
||||
numpy==2.2.3
|
||||
packaging==24.2
|
||||
pandas==2.2.3
|
||||
pathspec==0.12.1
|
||||
platformdirs==4.3.6
|
||||
pluggy==1.5.0
|
||||
propcache==0.2.1
|
||||
more-itertools>=10.6.0
|
||||
pandas>=2.2.3
|
||||
pydantic==2.10.6
|
||||
pydantic-core==2.27.2
|
||||
pydantic-settings==2.7.1
|
||||
pytest==8.3.4
|
||||
pytest-assertcount==1.0.0
|
||||
python-dateutil==2.9.0.post0
|
||||
python-dotenv==1.0.1
|
||||
python-levenshtein==0.26.1
|
||||
pytz==2025.1
|
||||
rapidfuzz==3.12.1
|
||||
requests==2.32.3
|
||||
requests-toolbelt==1.0.0
|
||||
ruff==0.9.6
|
||||
six==1.17.0
|
||||
sniffio==1.3.1
|
||||
specklepy==2.21.3
|
||||
stringcase==1.2.0
|
||||
typing-extensions==4.12.2
|
||||
tzdata==2025.1
|
||||
ujson==5.10.0
|
||||
urllib3==2.3.0
|
||||
websockets==11.0.3
|
||||
wrapt==1.17.2
|
||||
yarl==1.18.3
|
||||
python-dotenv>=1.0.1
|
||||
python-levenshtein>=0.26.1
|
||||
specklepy>=3.0.0
|
||||
pydantic-settings>=2.7.1
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Store the current Python environment
|
||||
CURRENT_ENV=$(pip freeze)
|
||||
|
||||
# Remove dev dependencies
|
||||
pip uninstall -y pytest pytest-cov isort flake8 ruff
|
||||
|
||||
# Generate production requirements
|
||||
pip freeze > requirements.txt
|
||||
|
||||
# Reinstall dev dependencies
|
||||
pip install pytest pytest-cov isort flake8 ruff
|
||||
|
||||
# Generate dev requirements
|
||||
pip freeze > requirements-dev.txt
|
||||
|
||||
# Restore the original environment
|
||||
pip uninstall -y pytest pytest-cov isort flake8 ruff
|
||||
echo "$CURRENT_ENV" | pip install -r /dev/stdin
|
||||
|
||||
echo "Requirements files have been updated successfully!"
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"files.autoSave": "onFocusChange",
|
||||
"editor.defaultFormatter": null,
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
}
|
||||
+93
-25
@@ -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,98 @@ 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."
|
||||
f"Successfully applied {len(grouped_rules)} rules to "
|
||||
f"{len(flat_list_of_objects)} version {VERSION} objects."
|
||||
)
|
||||
|
||||
+57
-26
@@ -4,7 +4,7 @@ from collections.abc import Generator, Iterable
|
||||
from typing import Any
|
||||
|
||||
from specklepy.objects import Base
|
||||
from specklepy.objects.other import Instance, Transform
|
||||
from specklepy.objects.proxies import InstanceProxy as Instance
|
||||
|
||||
|
||||
def speckle_print(log_string: str = "banana") -> None:
|
||||
@@ -27,7 +27,8 @@ def get_item(obj: Base | dict[str, Any], key, default=None):
|
||||
return obj.get(key, default)
|
||||
elif hasattr(obj, key): # If it's an object with the attribute
|
||||
return getattr(obj, key, default)
|
||||
return default # Return default if it's neither a dict nor an object with the attribute
|
||||
return default # Return default if it's neither a dict nor an object with
|
||||
# the attribute
|
||||
|
||||
|
||||
def has_item(obj: Base | dict[str, Any], key: str) -> bool:
|
||||
@@ -39,7 +40,9 @@ def has_item(obj: Base | dict[str, Any], key: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def flatten_base_thorough(base: Base, parent_type: str = None) -> Iterable[Base]:
|
||||
def flatten_base_thorough(
|
||||
base: Base, parent_type: str | None = None
|
||||
) -> Iterable[Base]:
|
||||
"""Take a base and flatten it to an iterable of bases.
|
||||
|
||||
Args:
|
||||
@@ -69,7 +72,9 @@ def flatten_base_thorough(base: Base, parent_type: str = None) -> Iterable[Base]
|
||||
print(category)
|
||||
if category.startswith("@"):
|
||||
category_object: Base = getattr(base, category)[0]
|
||||
yield from flatten_base_thorough(category_object, category_object.speckle_type)
|
||||
yield from flatten_base_thorough(
|
||||
category_object, category_object.speckle_type
|
||||
)
|
||||
|
||||
except KeyError:
|
||||
pass
|
||||
@@ -80,52 +85,78 @@ def flatten_base_thorough(base: Base, parent_type: str = None) -> Iterable[Base]
|
||||
def extract_base_and_transform(
|
||||
base: Base,
|
||||
inherited_instance_id: str | None = None,
|
||||
transform_list: list[Transform] | None = None,
|
||||
transform_list: list[list[float]] | None = None,
|
||||
) -> Generator[
|
||||
Base | str | list[Transform] | None | tuple[Base, Any | None, list[Transform] | None | list[Any]], Any | None, None
|
||||
Base
|
||||
| str
|
||||
| list[list[float]]
|
||||
| None
|
||||
| tuple[Base, Any | None, list[list[float]] | None | list[Any]],
|
||||
Any | None,
|
||||
]:
|
||||
"""Traverses Speckle object hierarchies to yield `Base` objects and their transformations.
|
||||
"""Traverses Speckle object hierarchies to yield `Base`s and transformas.
|
||||
|
||||
Tailored to Speckle's AEC data structures, it covers the newer hierarchical structures
|
||||
with Collections and also with patterns found in older Revit specific data.
|
||||
Tailored to Speckle's AEC data structures, it covers the newer
|
||||
hierarchical structures with Collections and also with patterns found in
|
||||
older Revit specific data.
|
||||
|
||||
Parameters:
|
||||
- base (Base): The starting point `Base` object for traversal.
|
||||
- inherited_instance_id (str, optional): The inherited identifier for `Base` objects without a unique ID.
|
||||
- transform_list (List[Transform], optional): Accumulated list of transformations from parent to child objects.
|
||||
- inherited_instance_id (str, optional): The inherited identifier for
|
||||
`Base` objects without a unique ID.
|
||||
- transform_list (List[List[float]], optional): Accumulated list of
|
||||
transformations from parent to child objects.
|
||||
|
||||
Yields:
|
||||
- tuple: A `Base` object, its identifier, and a list of applicable `Transform` objects or None.
|
||||
- tuple: A `Base` object, its identifier, and a list of applicable
|
||||
transformations or None.
|
||||
|
||||
The id of the `Base` object is either the inherited identifier for a definition from an instance
|
||||
or the one defined in the object.
|
||||
The id of the `Base` object is either the inherited identifier for a
|
||||
definition from an instance or the one defined in the object.
|
||||
"""
|
||||
# Derive the identifier for the current `Base` object, defaulting to an inherited one if needed.
|
||||
# Derive the identifier for the current `Base` object, defaulting to an
|
||||
# inherited one if needed.
|
||||
current_id = getattr(base, "id", inherited_instance_id)
|
||||
transform_list = transform_list or []
|
||||
|
||||
if isinstance(base, Instance):
|
||||
# Append transformation data and dive into the definition of `Instance` objects.
|
||||
# Append transformation data and dive into the definition of `Instance`
|
||||
# objects.
|
||||
if base.transform:
|
||||
transform_list.append(base.transform)
|
||||
if base.definition:
|
||||
yield from extract_base_and_transform(base.definition, current_id, transform_list.copy())
|
||||
yield from extract_base_and_transform(
|
||||
base.definition, current_id, transform_list.copy()
|
||||
)
|
||||
else:
|
||||
# Initial yield for the current `Base` object.
|
||||
yield base, current_id, transform_list
|
||||
|
||||
# Process 'elements' and '@elements', typical containers for `Base` objects in AEC models.
|
||||
elements_attr = getattr(base, "elements", []) or getattr(base, "@elements", [])
|
||||
# Process 'elements' and '@elements', typical containers for `Base`
|
||||
# objects in AEC models.
|
||||
elements_attr = getattr(base, "elements", []) or getattr(
|
||||
base, "@elements", []
|
||||
)
|
||||
for element in elements_attr:
|
||||
if isinstance(element, Base):
|
||||
# Recurse into each `Base` object within 'elements' or '@elements'.
|
||||
yield from extract_base_and_transform(element, current_id, transform_list.copy())
|
||||
# Recurse into each `Base` object within 'elements' or
|
||||
# '@elements'.
|
||||
yield from extract_base_and_transform(
|
||||
element, current_id, transform_list.copy()
|
||||
)
|
||||
|
||||
# Recursively process '@'-prefixed properties that are Base objects with 'elements'.
|
||||
# This is a common pattern in older Speckle data models, such as those used for Revit commits.
|
||||
# Recursively process '@'-prefixed properties that are Base objects
|
||||
# with 'elements'.
|
||||
# This is a common pattern in older Speckle data models, such as those
|
||||
# used for Revit commits.
|
||||
for attr_name in dir(base):
|
||||
if attr_name.startswith("@"):
|
||||
attr_value = getattr(base, attr_name)
|
||||
# If the attribute is a Base object containing 'elements', recurse into it.
|
||||
if isinstance(attr_value, Base) and hasattr(attr_value, "elements"):
|
||||
yield from extract_base_and_transform(attr_value, current_id, transform_list.copy())
|
||||
# If the attribute is a Base object containing 'elements',
|
||||
# recurse into it.
|
||||
if isinstance(attr_value, Base) and hasattr(
|
||||
attr_value, "elements"
|
||||
):
|
||||
yield from extract_base_and_transform(
|
||||
attr_value, current_id, transform_list.copy()
|
||||
)
|
||||
|
||||
+38
-16
@@ -1,3 +1,8 @@
|
||||
"""This file contains the inputs for the function.
|
||||
|
||||
It is used to define the inputs for the function and to validate them.
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import Field
|
||||
@@ -5,9 +10,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 +33,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.'
|
||||
# )
|
||||
|
||||
+6
-3
@@ -1,19 +1,22 @@
|
||||
"""Configuration module defining mappings between spreadsheet predicates and rule methods."""
|
||||
"""Defines mappings between spreadsheet predicates and rule methods."""
|
||||
|
||||
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__
|
||||
),
|
||||
}
|
||||
|
||||
+370
-87
@@ -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,230 @@ 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 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.
|
||||
def validate_rule_structure(rule_group: pd.DataFrame) -> None:
|
||||
"""Validates the structure and logic of a rule group.
|
||||
|
||||
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 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_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.
|
||||
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:
|
||||
"""Evaluates a single condition against a Speckle object.
|
||||
|
||||
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:
|
||||
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"]
|
||||
property_name = condition.get(
|
||||
"Property Name", condition.get("Property Path")
|
||||
)
|
||||
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,40 +254,97 @@ 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 rules to objects and updates the automate context 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
|
||||
if "Message" not in rule_group.columns or "Report Severity" not in rule_group.columns:
|
||||
if "Message" not in rule_group.columns or (
|
||||
"Report Severity" not in rule_group.columns
|
||||
and "Severity" not in rule_group.columns
|
||||
):
|
||||
continue # Or raise an exception if these columns are mandatory
|
||||
|
||||
# Get the severity level for this rule
|
||||
rule_severity = get_severity(rule_group.iloc[-1])
|
||||
rule_severity_level = severity_levels[
|
||||
MinimumSeverity(rule_severity.value)
|
||||
]
|
||||
|
||||
# Check if the rule severity level meets the minimum severity level
|
||||
# no point in processing lower severity rules
|
||||
if rule_severity_level < min_severity_level:
|
||||
continue
|
||||
|
||||
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)
|
||||
# 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 len(fail_objects) and 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
|
||||
):
|
||||
speckle_print(f"Rule {rule_id_str} Skipped")
|
||||
|
||||
newBase = Base()
|
||||
newBase.id = "123"
|
||||
|
||||
if len(pass_objects) == 0 and len(fail_objects) == 0:
|
||||
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
|
||||
affected_objects=[newBase],
|
||||
# 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 +353,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,21 +367,33 @@ 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 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
|
||||
"""
|
||||
severity = rule_info.get("Report Severity") # Extract severity from input data
|
||||
- Whitespace handling
|
||||
- Default fallback to ERROR for invalid input
|
||||
|
||||
# If severity is None or not a string (e.g., numeric input), default to ERROR
|
||||
Args:
|
||||
rule_info: Series containing rule information with 'Report Severity'
|
||||
key
|
||||
|
||||
Returns:
|
||||
Appropriate SeverityLevel enum value
|
||||
"""
|
||||
severity = rule_info.get("Report Severity") or rule_info.get(
|
||||
"Severity"
|
||||
) # Extract severity from input data
|
||||
|
||||
# If severity is None or not a string (e.g., numeric input),
|
||||
# default to ERROR
|
||||
if not isinstance(severity, str):
|
||||
return SeverityLevel.ERROR
|
||||
|
||||
severity = severity.strip().upper() # Remove leading/trailing spaces & normalize case
|
||||
severity = (
|
||||
severity.strip().upper()
|
||||
) # Remove leading/trailing spaces & normalize case
|
||||
|
||||
# Define a mapping for shorthand or alternate spellings
|
||||
alias_map = {
|
||||
@@ -174,7 +403,8 @@ def get_severity(rule_info: pd.Series) -> SeverityLevel:
|
||||
# Replace shorthand values if applicable
|
||||
severity = alias_map.get(severity, severity)
|
||||
|
||||
# Attempt to match with an existing SeverityLevel enum value (case-insensitive)
|
||||
# Attempt to match with an existing SeverityLevel enum value
|
||||
# (case-insensitive)
|
||||
return next(
|
||||
(level for level in SeverityLevel if level.value.upper() == severity),
|
||||
SeverityLevel.ERROR, # Default to ERROR if no match is found
|
||||
@@ -182,17 +412,45 @@ def get_severity(rule_info: pd.Series) -> SeverityLevel:
|
||||
|
||||
|
||||
def get_metadata(
|
||||
rule_id: str, rule_info: pd.Series, passed: bool, speckle_objects: list[Base]
|
||||
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 +460,20 @@ 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 +481,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"])
|
||||
@@ -230,7 +493,7 @@ def attach_results(
|
||||
)
|
||||
context.attach_result_to_objects(
|
||||
category=f"Rule {rule_id}",
|
||||
object_ids=[speckle_object.id for speckle_object in speckle_objects],
|
||||
affected_objects=speckle_objects,
|
||||
message=message,
|
||||
level=severity,
|
||||
metadata=metadata,
|
||||
@@ -238,7 +501,27 @@ def attach_results(
|
||||
else:
|
||||
context.attach_info_to_objects(
|
||||
category=f"Rule {rule_id}",
|
||||
object_ids=[speckle_object.id for speckle_object in speckle_objects],
|
||||
affected_objects=speckle_objects,
|
||||
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
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
@@ -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, fall back to "Rule #"
|
||||
group_rule_num = (
|
||||
group_slice["Rule Number"].iloc[0] if not pd.isna(group_slice["Rule Number"].iloc[0]) else "Rule #"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
+41
-78
@@ -2,67 +2,6 @@ import pytest
|
||||
from specklepy.objects.base import Base
|
||||
|
||||
|
||||
@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"
|
||||
wall.elementId = "4479852"
|
||||
wall.worksetId = "0"
|
||||
wall.structural = True
|
||||
wall.baseOffset = -2000
|
||||
wall.topOffset = -600
|
||||
|
||||
# Create base line geometry
|
||||
wall.baseLine = Base()
|
||||
wall.baseLine.start = Base()
|
||||
wall.baseLine.start.x = 22400.000000000007
|
||||
wall.baseLine.start.y = 15199.999999999998
|
||||
wall.baseLine.start.z = -2000.0000000000002
|
||||
wall.baseLine.end = Base()
|
||||
wall.baseLine.end.x = 22400.000000000015
|
||||
wall.baseLine.end.y = 20500
|
||||
wall.baseLine.end.z = -2000.0000000000002
|
||||
wall.baseLine.units = "mm"
|
||||
wall.baseLine.length = 5300.000000000002
|
||||
|
||||
# Create parameters structure
|
||||
wall.parameters = Base()
|
||||
|
||||
# Standard parameter
|
||||
wall.parameters["WALL_ATTR_WIDTH_PARAM"] = Base()
|
||||
wall.parameters["WALL_ATTR_WIDTH_PARAM"].name = "Width"
|
||||
wall.parameters["WALL_ATTR_WIDTH_PARAM"].value = 300
|
||||
wall.parameters["WALL_ATTR_WIDTH_PARAM"].units = "mm"
|
||||
|
||||
# Parameter with GUID key
|
||||
wall.parameters["ee1f33e1-5506-4a64-b87b-7b98d30aea52"] = Base()
|
||||
wall.parameters["ee1f33e1-5506-4a64-b87b-7b98d30aea52"].name = "符号"
|
||||
wall.parameters["ee1f33e1-5506-4a64-b87b-7b98d30aea52"].value = "W30"
|
||||
wall.parameters["ee1f33e1-5506-4a64-b87b-7b98d30aea52"].isShared = True
|
||||
wall.parameters[
|
||||
"ee1f33e1-5506-4a64-b87b-7b98d30aea52"
|
||||
].internalDefinitionName = "ee1f33e1-5506-4a64-b87b-7b98d30aea52"
|
||||
|
||||
wall.parameters["STRUCTURAL_MATERIAL_PARAM"] = Base()
|
||||
wall.parameters["STRUCTURAL_MATERIAL_PARAM"].name = "Structural Material"
|
||||
wall.parameters["STRUCTURAL_MATERIAL_PARAM"].value = "Fc24"
|
||||
|
||||
# Create basic level reference
|
||||
wall.level = Base()
|
||||
wall.level.name = "1FL"
|
||||
wall.level.elevation = 0
|
||||
wall.level.units = "mm"
|
||||
|
||||
return wall
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def v3_wall():
|
||||
"""Creates a v3-style Speckle wall object."""
|
||||
@@ -79,25 +18,47 @@ def v3_wall():
|
||||
|
||||
# Create location geometry
|
||||
wall.location = Base()
|
||||
wall.location.id = "9c76b8de34382c9052965ee463f8374b"
|
||||
wall.location.start = Base()
|
||||
wall.location.start.x = 22400.000000000007
|
||||
wall.location.start.y = 15199.999999999998
|
||||
wall.location.start.z = 0
|
||||
wall.location.start.id = "d0c4fdb2e11cc825e7f05f9dc88a0be1"
|
||||
wall.location.start.units = "mm"
|
||||
wall.location.start.speckle_type = "Objects.Geometry.Point"
|
||||
wall.location.end = Base()
|
||||
wall.location.end.x = 22400.000000000015
|
||||
wall.location.end.y = 20500
|
||||
wall.location.end.z = 0
|
||||
wall.location.end.id = "3455575bfd8939f264d295b61e74156f"
|
||||
wall.location.end.units = "mm"
|
||||
wall.location.end.speckle_type = "Objects.Geometry.Point"
|
||||
wall.location.units = "mm"
|
||||
wall.location.domain = Base()
|
||||
wall.location.domain.id = "3b97feaad2dbcc2d894c9cec024a9bf2"
|
||||
wall.location.domain.end = 17.388451443569522
|
||||
wall.location.domain.start = -3.552713668866051e-14
|
||||
wall.location.domain.speckle_type = "Objects.Primitive.Interval"
|
||||
wall.location.length = 5300.000000000002
|
||||
wall.location.speckle_type = "Objects.Geometry.Line"
|
||||
|
||||
# Create nested properties structure
|
||||
# Create level references
|
||||
wall.level = Base()
|
||||
wall.level.name = "1FL"
|
||||
wall.level.units = "mm"
|
||||
wall.level.elevation = 0
|
||||
|
||||
wall.topLevel = Base()
|
||||
wall.topLevel.name = "1FL"
|
||||
wall.topLevel.units = "mm"
|
||||
wall.topLevel.elevation = 0
|
||||
|
||||
# Create properties structure
|
||||
wall.properties = Base()
|
||||
wall.properties.Parameters = Base()
|
||||
|
||||
# Type Parameters
|
||||
wall.properties.Parameters["Type Parameters"] = Base()
|
||||
|
||||
# Add Text section with GUID parameter
|
||||
# Add Text section
|
||||
wall.properties.Parameters["Type Parameters"].Text = Base()
|
||||
wall.properties.Parameters["Type Parameters"].Text["符号"] = {
|
||||
"name": "符号",
|
||||
@@ -105,6 +66,7 @@ def v3_wall():
|
||||
"internalDefinitionName": "ee1f33e1-5506-4a64-b87b-7b98d30aea52",
|
||||
}
|
||||
|
||||
# Add Structure section
|
||||
wall.properties.Parameters["Type Parameters"].Structure = Base()
|
||||
wall.properties.Parameters["Type Parameters"].Structure["Fc24 (0)"] = {
|
||||
"units": "mm",
|
||||
@@ -113,20 +75,21 @@ def v3_wall():
|
||||
"thickness": 300,
|
||||
}
|
||||
|
||||
# Instance Parameters
|
||||
# Add Construction section
|
||||
wall.properties.Parameters["Type Parameters"].Construction = Base()
|
||||
wall.properties.Parameters["Type Parameters"].Construction.Width = {
|
||||
"name": "Width",
|
||||
"units": "Millimeters",
|
||||
"value": 300,
|
||||
"internalDefinitionName": "WALL_ATTR_WIDTH_PARAM",
|
||||
}
|
||||
|
||||
# Add Instance Parameters
|
||||
wall.properties.Parameters["Instance Parameters"] = Base()
|
||||
wall.properties.Parameters["Instance Parameters"].Structural = Base()
|
||||
wall.properties.Parameters["Instance Parameters"].Structural.Structural = {"name": "Structural", "value": "Yes"}
|
||||
|
||||
# Create basic level references
|
||||
wall.level = Base()
|
||||
wall.level.name = "1FL"
|
||||
wall.level.elevation = 0
|
||||
wall.level.units = "mm"
|
||||
|
||||
wall.topLevel = Base()
|
||||
wall.topLevel.name = "1FL"
|
||||
wall.topLevel.elevation = 0
|
||||
wall.topLevel.units = "mm"
|
||||
wall.properties.Parameters["Instance Parameters"].Structural.Structural = {
|
||||
"name": "Structural",
|
||||
"value": "Yes",
|
||||
}
|
||||
|
||||
return wall
|
||||
|
||||
@@ -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
|
||||
+32
-22
@@ -10,32 +10,42 @@ from speckle_automate.fixtures import * # noqa: F401, F403
|
||||
|
||||
from src.function import automate_function
|
||||
from src.helpers import speckle_print
|
||||
from src.inputs import FunctionInputs
|
||||
from src.inputs import FunctionInputs, MinimumSeverity
|
||||
|
||||
|
||||
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://model-checker.speckle.systems/r/7YhnQyQNP_Ydv97QCwHbj7BWHrNkG022bez_jVkxbYs/tsv"
|
||||
|
||||
assert automate_sdk.run_status == AutomationStatus.SUCCEEDED
|
||||
automate_sdk = run_function(
|
||||
automation_context,
|
||||
automate_function,
|
||||
FunctionInputs(
|
||||
spreadsheet_url=default_url,
|
||||
minimum_severity=MinimumSeverity.INFO,
|
||||
hide_skipped=True,
|
||||
),
|
||||
)
|
||||
|
||||
assert automate_sdk.run_status == AutomationStatus.SUCCEEDED
|
||||
|
||||
+498
-378
@@ -1,404 +1,524 @@
|
||||
import os
|
||||
from typing import Any
|
||||
"""Test suite for parameter handling functionality."""
|
||||
|
||||
import pytest
|
||||
from dotenv import load_dotenv
|
||||
from speckle_automate import AutomationContext, AutomationRunData # noqa: F401, F403
|
||||
|
||||
# from speckle_automate.fixtures import * # noqa: F401, F403
|
||||
from specklepy.api.client import SpeckleClient
|
||||
from specklepy.core.api import operations
|
||||
from specklepy.objects.base import Base
|
||||
from specklepy.transports.server import ServerTransport
|
||||
|
||||
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")
|
||||
@pytest.fixture
|
||||
def test_objects(self) -> Base:
|
||||
"""Pytest fixture to provide test objects."""
|
||||
# Create a mock Base object with the required structure
|
||||
v3_obj = Base()
|
||||
v3_obj.properties = {
|
||||
"Parameters": {
|
||||
"category": "Walls",
|
||||
"Width": 300,
|
||||
"Construction": {"Width": 300},
|
||||
"Instance Parameters": {
|
||||
"Dimensions": {"Length": 5300.000000000001},
|
||||
"Structural": {"Structural": {"value": "Yes"}},
|
||||
"Room Bounding": {"value": "Yes"},
|
||||
"top is attached": {"value": "No"},
|
||||
},
|
||||
"Type Parameters": {
|
||||
"Structure": {"Fc24 (0)": {"thickness": 300}},
|
||||
"Text": {"符号": {"value": "W30"}},
|
||||
},
|
||||
"Type": "W30(Fc24)",
|
||||
}
|
||||
}
|
||||
v3_obj.speckle_type = "Revit"
|
||||
return v3_obj
|
||||
|
||||
client.authenticate_with_token(os.getenv("SPECKLE_TOKEN"))
|
||||
def test_deserialization_structure(self, test_objects):
|
||||
"""Test that objects are properly deserialized with correct structure."""
|
||||
v3_obj = test_objects
|
||||
|
||||
transport = ServerTransport(client=client, stream_id=os.getenv("SPECKLE_PROJECT_ID"))
|
||||
# Check base class type
|
||||
assert isinstance(v3_obj, Base), f"Expected {v3_obj} to be an instance of Base"
|
||||
|
||||
speckle_print(v2_wall)
|
||||
v2_obj = operations.receive("cdb18060dc48281909e94f0f1d8d3cc0", transport)
|
||||
v3_obj = operations.receive("46f06fef727d64a0bbcbd7ced51e0cd2", transport)
|
||||
# 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"
|
||||
)
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
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 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']"
|
||||
|
||||
|
||||
@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
|
||||
("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",
|
||||
[
|
||||
# Test direct value access
|
||||
(
|
||||
"location.length",
|
||||
"location.length",
|
||||
),
|
||||
# Test .value key access
|
||||
(
|
||||
"Type Parameters.Text.符号",
|
||||
"Type Parameters.Text.符号.value",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_v3_parameter_search_equivalence(
|
||||
self,
|
||||
v3_wall,
|
||||
param_name_1,
|
||||
param_name_2,
|
||||
):
|
||||
"""Test parameter existence checking equivalence in v3 objects."""
|
||||
assert PropertyRules.get_parameter_value(
|
||||
v3_wall, param_name_1
|
||||
) == PropertyRules.get_parameter_value(v3_wall, param_name_2)
|
||||
|
||||
@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_value, default_value",
|
||||
[
|
||||
# Test direct parameters
|
||||
("category", "Walls", None),
|
||||
# Test nested parameters - using both internal and friendly names
|
||||
("Construction.Width", 300, None),
|
||||
# Test parameters with units
|
||||
(
|
||||
"Instance Parameters.Dimensions.Length",
|
||||
5300.000000000001,
|
||||
None,
|
||||
),
|
||||
# Test non-existent parameters with a default value
|
||||
(
|
||||
"properties.Parameters.non_existent",
|
||||
"default",
|
||||
"default",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_parameter_value_retrieval(
|
||||
self,
|
||||
test_objects,
|
||||
param_name,
|
||||
expected_value,
|
||||
default_value,
|
||||
):
|
||||
"""Test parameter value retrieval from v3 objects."""
|
||||
v3_obj = test_objects
|
||||
result = PropertyRules.get_parameter_value(
|
||||
v3_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
|
||||
("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(
|
||||
"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(
|
||||
"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_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, 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, 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",
|
||||
[
|
||||
("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(
|
||||
"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(
|
||||
"attribute, value, expected",
|
||||
[
|
||||
# Test numeric value comparisons
|
||||
(
|
||||
"Type Parameters.Structure.Fc24 (0).thickness",
|
||||
300,
|
||||
True,
|
||||
),
|
||||
(
|
||||
"Instance Parameters.Dimensions.Length",
|
||||
5300.000000000002,
|
||||
True,
|
||||
),
|
||||
(
|
||||
"Instance Parameters.Dimensions.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,
|
||||
test_objects,
|
||||
attribute,
|
||||
value,
|
||||
expected,
|
||||
):
|
||||
"""Test value comparisons using v3 wall parameters."""
|
||||
assert PropertyRules.is_equal_value(test_objects, attribute, value) == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"wall, attribute, value, expected",
|
||||
[
|
||||
# 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,
|
||||
False,
|
||||
),
|
||||
(
|
||||
"v3_wall",
|
||||
"location.length",
|
||||
5300,
|
||||
False,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_identical_comparisons(
|
||||
self,
|
||||
test_objects,
|
||||
wall,
|
||||
attribute,
|
||||
value,
|
||||
expected,
|
||||
):
|
||||
"""Test identical value comparisons on v3 wall."""
|
||||
if attribute == "type":
|
||||
# Use case-insensitive comparison for type parameter
|
||||
assert (
|
||||
PropertyRules.is_equal_value(
|
||||
test_objects,
|
||||
attribute,
|
||||
value,
|
||||
)
|
||||
== expected
|
||||
)
|
||||
else:
|
||||
# Use strict comparison for other parameters
|
||||
assert (
|
||||
PropertyRules.is_identical_value(
|
||||
test_objects,
|
||||
attribute,
|
||||
value,
|
||||
)
|
||||
== expected
|
||||
)
|
||||
|
||||
@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(
|
||||
"wall, attribute, 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,
|
||||
test_objects,
|
||||
wall,
|
||||
attribute,
|
||||
value,
|
||||
):
|
||||
"""Test not equal comparisons on v3 wall."""
|
||||
assert PropertyRules.is_not_equal_value(test_objects, 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,
|
||||
test_objects,
|
||||
attribute,
|
||||
value,
|
||||
expected_equal,
|
||||
expected_identical,
|
||||
):
|
||||
"""Test conversion of Yes/No strings to boolean values."""
|
||||
assert (
|
||||
PropertyRules.is_equal_value(test_objects, attribute, value)
|
||||
== expected_equal
|
||||
)
|
||||
assert (
|
||||
PropertyRules.is_identical_value(test_objects, attribute, value)
|
||||
== expected_identical
|
||||
)
|
||||
|
||||
@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(
|
||||
"wall, attribute, expected_value",
|
||||
[
|
||||
# V3 wall tests
|
||||
(
|
||||
"v3_wall",
|
||||
"Type Parameters.Structure.Fc24 (0).thickness",
|
||||
"300",
|
||||
),
|
||||
(
|
||||
"v3_wall",
|
||||
"Instance Parameters.Dimensions.Length",
|
||||
"5300.000000000002",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_numeric_string_handling(
|
||||
self,
|
||||
test_objects,
|
||||
wall,
|
||||
attribute,
|
||||
expected_value,
|
||||
):
|
||||
"""Test handling of numeric strings in v3 wall."""
|
||||
assert PropertyRules.is_equal_value(
|
||||
test_objects,
|
||||
attribute,
|
||||
expected_value,
|
||||
)
|
||||
|
||||
|
||||
@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, 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(
|
||||
"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(
|
||||
"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(
|
||||
"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",
|
||||
True,
|
||||
), # Should pass as it does not contain Revit
|
||||
(
|
||||
"speckle_type",
|
||||
"NotPresent",
|
||||
True,
|
||||
), # Should pass as it doesn't contain
|
||||
(
|
||||
"speckle_type",
|
||||
"",
|
||||
False,
|
||||
), # Should fail as empty string is contained in any string
|
||||
(
|
||||
"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."""
|
||||
v3_obj = test_objects
|
||||
assert (
|
||||
PropertyRules.is_parameter_value_not_containing(
|
||||
v3_obj,
|
||||
param_name,
|
||||
substring,
|
||||
)
|
||||
== expected_result
|
||||
)
|
||||
|
||||
@@ -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", "", ""],
|
||||
}
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
]
|
||||
Reference in New Issue
Block a user