diff --git a/README.md b/README.md index eaa4260..fc6fff5 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ This package is a simple order book matching engine implementation in Python. It - price-time priority - limit and market orders - order cancellation and expiration -- conversion into pandas DataFrame of orders, executed trades, order book summary +- conversion into polars LazyFrame of orders, executed trades, order book summary ## Install @@ -30,7 +30,7 @@ pip install order-matching ```python >>> from datetime import datetime, timedelta >>> from pprint import pp ->>> import pandas as pd +>>> import polars as pl >>> from order_matching.matching_engine import MatchingEngine >>> from order_matching.order import LimitOrder diff --git a/openspec/changes/archive/2026-07-02-migrate-pandas-to-polars/.openspec.yaml b/openspec/changes/archive/2026-07-02-migrate-pandas-to-polars/.openspec.yaml new file mode 100644 index 0000000..8e26fbe --- /dev/null +++ b/openspec/changes/archive/2026-07-02-migrate-pandas-to-polars/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-02 diff --git a/openspec/changes/archive/2026-07-02-migrate-pandas-to-polars/design.md b/openspec/changes/archive/2026-07-02-migrate-pandas-to-polars/design.md new file mode 100644 index 0000000..46e7c54 --- /dev/null +++ b/openspec/changes/archive/2026-07-02-migrate-pandas-to-polars/design.md @@ -0,0 +1,160 @@ +## Context + +The OrderBookMatchingEngine currently uses pandas for DataFrame operations in three key areas: +1. `Orders.to_frame()` - converting order collections to DataFrames +2. `ExecutedTrades.to_frame()` - converting trade collections to DataFrames +3. `OrderBook.summary()` - generating order book summary DataFrames + +All DataFrames are validated using pandera schemas (`OrderDataSchema`, `TradeDataSchema`, `OrderBookSummarySchema`) that define column types, constraints, and validation rules. These schemas are critical for downstream code and analysis pipelines. + +**Current Implementation:** +- Uses `pandas.DataFrame.from_records()` to convert dataclass instances +- Uses pandas-specific operations like `.assign()` with lambda functions +- Type hints use `pandera.typing.pandas.DataFrame[Schema]` +- Schema validation uses pandera's pandas backend + +**Constraints:** +- Must maintain 100% backward compatibility with existing DataFrame schemas +- Cannot break existing tests or downstream code that consumes these DataFrames + +## Goals / Non-Goals + +**Goals:** +- Replace pandas with polars for all DataFrame conversion operations +- Maintain identical DataFrame schemas (column names, types, constraints) +- Preserve all pandera validation behavior +- Improve performance through polars' optimized operations +- Update type hints to reflect polars usage while keeping schema validation +- Use LazyFrame and lazy evaluation whenever possible + +**Non-Goals:** +- Changing the pandera schema definitions or validation rules +- Modifying the API surface (method signatures stay the same) +- Migrating internal data structures from dataclasses +- Supporting both pandas and polars simultaneously (full migration only) +- Changing behavior of existing methods beyond the DataFrame type + +## Decisions + +### Decision 1: Use polars DataFrame construction directly +**Rationale:** Polars provides native DataFrame construction from dictionaries and records. Instead of using pandas `from_records()`, we'll use `pl.LazyFrame()` constructor. + +**Implementation:** +```python +# Before (pandas): +pd.DataFrame.from_records([asdict(order) for order in orders]) + +# After (polars): +pl.LazyFrame([asdict(order) for order in orders]) +``` + +**Alternatives considered:** +- Converting pandas to polars via arrow - rejected as unnecessary intermediate step + +### Decision 2: Handle type conversions with polars expressions +**Rationale:** Pandas uses `.assign()` with lambda functions for type conversions. Polars uses `.with_columns()` and expressions. + +**Implementation:** +```python +# Before (pandas): +.assign( + side=lambda df: df['side'].astype(str), + timestamp=lambda df: pd.to_datetime(df['timestamp']) +) + +# After (polars): +.with_columns([ + pl.col('side').cast(pl.Utf8), + pl.col('timestamp').cast(pl.Datetime) +]) +``` + +**Alternatives considered:** +- Keep using assign-style API via polars extensions - rejected for being less idiomatic + +### Decision 3: Update pandera imports to support polars +**Rationale:** Pandera 0.17.0+ supports both pandas and polars backends through separate import paths. + +**Implementation:** +```python +# Before: +from pandera.pandas import DataFrameModel +from pandera.typing.pandas import DataFrame + +# After: +from pandera.polars import DataFrameModel +from pandera.typing.polars import DataFrame +from pandera.typing.polars import LazyFrame +``` + +**Alternatives considered:** +- Maintaining dual compatibility - rejected per non-goals (full migration only) +- Creating polars-specific schema copies - rejected to avoid duplication + +### Decision 4: Handle empty DataFrames with explicit schema +**Rationale:** Empty polars DataFrames need explicit schema definition to maintain type information. + +**Implementation:** +```python +# Empty DataFrame case: +if len(collection) == 0: + return pl.DataFrame(schema={ + 'column1': pl.Float64, + 'column2': pl.Utf8, + # ... all columns + }) +``` + +**Alternatives considered:** +- Returning empty DataFrame without schema - rejected because pandera validation would fail +- Using schema inference from empty records - rejected as unreliable + +## Risks / Trade-offs + +**Risk:** Pandera polars support may have edge cases or bugs +- **Mitigation:** Comprehensive test suite covers all schema validation scenarios. Run full test suite before merging. + +**Risk:** Polars API differences may cause subtle behavioral changes +- **Mitigation:** Carefully review all DataFrame operations and validate output schemas match exactly. Add explicit type casts where needed. + +**Risk:** Breaking change for downstream code expecting pandas DataFrames +- **Mitigation:** This is intentional per requirements. Document clearly in CHANGELOG and update README examples. + +**Trade-off:** Lose pandas ecosystem compatibility +- **Impact:** Code using these DataFrames must work with polars instead of pandas +- **Benefit:** Significantly better performance, especially for large order books + +**Trade-off:** Increased dependency on polars library maturity +- **Impact:** Polars is newer than pandas, potential for API changes +- **Benefit:** Polars is stable (1.0+ released), actively developed, and widely adopted + +## Migration Plan + +**Phase 1: Update schemas** +1. Change imports in `src/order_matching/schemas.py` from pandas to polars +2. Update type hints to use `pandera.typing.polars.DataFrame` +3. Verify schemas still define the same constraints + +**Phase 2: Update conversion methods** +1. Update `Orders.to_frame()` in `orders.py` +2. Update `ExecutedTrades.to_frame()` in `executed_trades.py` +3. Update `OrderBook.summary()` in `order_book.py` +4. Ensure each method handles empty cases with explicit schemas + +**Phase 3: Update tests** +1. Run existing test suite - most should still pass +2. Fix any tests that check for pandas-specific behavior +3. Add new tests if needed for polars-specific edge cases + +**Phase 4: Validate** +1. Run full test suite: `uv run pytest` +2. Run linter and formatter: `uv run prek run -v --show-diff-on-failure --all-files` + +**Rollback strategy:** +- If tests fail after updates, revert changes file by file +- Git branch allows clean rollback to pandas implementation +- No database or persistent state changes, so rollback is safe + +## Open Questions + +None - requirements and approach are clear. diff --git a/openspec/changes/archive/2026-07-02-migrate-pandas-to-polars/proposal.md b/openspec/changes/archive/2026-07-02-migrate-pandas-to-polars/proposal.md new file mode 100644 index 0000000..43c370a --- /dev/null +++ b/openspec/changes/archive/2026-07-02-migrate-pandas-to-polars/proposal.md @@ -0,0 +1,36 @@ +## Why + +The project currently uses pandas for DataFrame operations in orders, trades, and order book summaries. +Polars offers significantly better performance and a more modern API, but the migration must maintain 100% schema compatibility with existing pandera validations to avoid breaking downstream code or analysis pipelines. + +## What Changes + +- Replace pandas DataFrame operations with polars equivalents across all data conversion methods +- Maintain pandera schema validation compatibility - all schemas must be converted to work polars DataFrames +- Update `to_frame()` methods in `Orders`, `ExecutedTrades`, and `OrderBook` classes to return polars DataFrames +- Ensure column names, dtypes, and constraints remain identical to current pandera-validated schemas +- Update type hints from `pandera.typing.pandas.DataFrame` to support polars while preserving schema validation + +## Capabilities + +### New Capabilities +- `polars-dataframe-conversion`: Converting internal data structures (Orders, ExecutedTrades, OrderBook) to polars DataFrames with schema validation +- `pandera-polars-validation`: Schema validation using pandera with polars backend to ensure schema compatibility + +### Modified Capabilities + +## Impact + +**Code:** +- `src/order_matching/orders.py`: Update `to_frame()` method to use polars +- `src/order_matching/executed_trades.py`: Update `to_frame()` method to use polars +- `src/order_matching/order_book.py`: Update `summary()` method to use polars +- `src/order_matching/schemas.py`: Update schema definitions to support polars DataFrames + +**Tests:** +- All existing tests that validate DataFrame schemas must continue to pass +- Tests in `tests/test_order.py`, `tests/test_trade.py`, `tests/test_executed_trades.py` that check DataFrame output +- Documentation examples in README.md that demonstrate DataFrame usage + +**API:** +- Return type changes from pandas DataFrame to polars DataFrame, but schema validation ensures column names and dtypes remain compatible diff --git a/openspec/changes/archive/2026-07-02-migrate-pandas-to-polars/specs/pandera-polars-validation/spec.md b/openspec/changes/archive/2026-07-02-migrate-pandas-to-polars/specs/pandera-polars-validation/spec.md new file mode 100644 index 0000000..9c871ce --- /dev/null +++ b/openspec/changes/archive/2026-07-02-migrate-pandas-to-polars/specs/pandera-polars-validation/spec.md @@ -0,0 +1,51 @@ +## ADDED Requirements + +### Requirement: Validate polars DataFrames with pandera schemas +The system SHALL use pandera to validate polars DataFrames against existing schema definitions (`OrderDataSchema`, `TradeDataSchema`, `OrderBookSummarySchema`). + +#### Scenario: Valid polars DataFrame passes schema validation +- **WHEN** a polars DataFrame with correct schema is validated against `OrderDataSchema`, `TradeDataSchema`, or `OrderBookSummarySchema` +- **THEN** validation succeeds without errors + +#### Scenario: Invalid polars DataFrame fails schema validation +- **WHEN** a polars DataFrame with incorrect schema (wrong columns, wrong types, or constraint violations) is validated +- **THEN** validation raises a pandera validation error with details + +### Requirement: Maintain schema constraint validation +The system SHALL validate all pandera Field constraints (gt, ge, isin, unique, nullable) on polars DataFrames. + +#### Scenario: Price constraint validation +- **WHEN** a polars DataFrame contains price values <= 0 +- **THEN** validation against schemas with `price: Field(gt=0)` fails + +#### Scenario: Enum value constraint validation +- **WHEN** a polars DataFrame contains invalid enum values in side/execution/status columns +- **THEN** validation against schemas with `Field(isin=[...])` fails + +#### Scenario: Unique constraint validation +- **WHEN** a polars DataFrame for OrderBookSummary contains duplicate price values +- **THEN** validation against `OrderBookSummarySchema` with `price: Field(unique=True)` fails + +#### Scenario: Nullable constraint validation +- **WHEN** a polars DataFrame contains null values in non-nullable columns +- **THEN** validation fails +- **WHEN** a polars DataFrame contains null values in nullable columns (e.g., expiration) +- **THEN** validation succeeds + +### Requirement: Update schema type hints for polars +The system SHALL update type hints in schema definitions to support both pandas and polars DataFrames via pandera's typing system. + +#### Scenario: Schema works with pandera.typing.polars.DataFrame and pandera.typing.polars.LazyFrame +- **WHEN** schemas are imported and used with polars DataFrames +- **THEN** type hints correctly indicate polars DataFrame compatibility + +### Requirement: Preserve strict mode behavior +The system SHALL maintain strict mode validation configured in schema Config classes when validating polars DataFrames. + +#### Scenario: Strict schema rejects extra columns +- **WHEN** a polars DataFrame with extra columns is validated against a schema with `strict = True` +- **THEN** validation fails + +#### Scenario: Strict schema accepts exact columns +- **WHEN** a polars DataFrame with exact required columns is validated against a schema with `strict = True` +- **THEN** validation succeeds diff --git a/openspec/changes/archive/2026-07-02-migrate-pandas-to-polars/specs/polars-dataframe-conversion/spec.md b/openspec/changes/archive/2026-07-02-migrate-pandas-to-polars/specs/polars-dataframe-conversion/spec.md new file mode 100644 index 0000000..25f1790 --- /dev/null +++ b/openspec/changes/archive/2026-07-02-migrate-pandas-to-polars/specs/polars-dataframe-conversion/spec.md @@ -0,0 +1,58 @@ +## ADDED Requirements + +### Requirement: Convert Orders to polars DataFrame +The system SHALL convert `Orders` objects to polars DataFrames using the `to_frame()` method with all columns matching the `OrderDataSchema`. + +#### Scenario: Convert empty Orders to DataFrame +- **WHEN** `to_frame()` is called on an empty `Orders` object +- **THEN** system returns an empty polars DataFrame + +#### Scenario: Convert populated Orders to DataFrame +- **WHEN** `to_frame()` is called on `Orders` containing one or more orders +- **THEN** system returns a polars DataFrame with columns: timestamp, expiration, order_id, trader_id, side, execution, status, price, size, price_number_of_digits +- **THEN** all string enum fields (side, execution, status) are converted to string representations +- **THEN** timestamp and expiration columns are datetime type + +### Requirement: Convert ExecutedTrades to polars DataFrame +The system SHALL convert `ExecutedTrades` objects to polars DataFrames using the `to_frame()` method with all columns matching the `TradeDataSchema`. + +#### Scenario: Convert empty ExecutedTrades to DataFrame +- **WHEN** `to_frame()` is called on an empty `ExecutedTrades` object +- **THEN** system returns an empty polars DataFrame + +#### Scenario: Convert populated ExecutedTrades to DataFrame +- **WHEN** `to_frame()` is called on `ExecutedTrades` containing one or more trades +- **THEN** system returns a polars DataFrame with columns: timestamp, incoming_order_id, book_order_id, trade_id, side, execution, price, size +- **THEN** string enum fields (side, execution) are converted to string representations +- **THEN** timestamp column is datetime type + +### Requirement: Convert OrderBook to polars DataFrame summary +The system SHALL convert `OrderBook` objects to polars DataFrames using the `summary()` method with all columns matching the `OrderBookSummarySchema`. + +#### Scenario: Convert empty OrderBook to summary DataFrame +- **WHEN** `summary()` is called on an empty `OrderBook` +- **THEN** system returns an empty polars DataFrame + +#### Scenario: Convert OrderBook with bids and offers to summary +- **WHEN** `summary()` is called on `OrderBook` containing bids and/or offers +- **THEN** system returns a polars DataFrame with columns: side, price, size, count +- **THEN** each unique price level is represented as a single row +- **THEN** count column is integer type +- **THEN** side column contains "BUY" or "SELL" string values + +### Requirement: Maintain DataFrame schema compatibility +The system SHALL ensure polars DataFrames produced by conversion methods have identical column names, dtypes, and value formats as the pandas DataFrames they replace. + +#### Scenario: Column names match exactly +- **WHEN** any conversion method produces a polars DataFrame +- **THEN** column names MUST match the corresponding pandera schema field names exactly + +#### Scenario: Data types are compatible +- **WHEN** any conversion method produces a polars DataFrame +- **THEN** numeric columns (price, size, count) use appropriate numeric types +- **THEN** datetime columns use polars datetime type +- **THEN** string columns (order_id, trader_id, trade_id, side, execution, status) use string type + +#### Scenario: Empty DataFrames have correct schema +- **WHEN** conversion methods are called on empty collections +- **THEN** resulting empty DataFrames MUST still have the correct column names and types defined diff --git a/openspec/changes/archive/2026-07-02-migrate-pandas-to-polars/tasks.md b/openspec/changes/archive/2026-07-02-migrate-pandas-to-polars/tasks.md new file mode 100644 index 0000000..a9569f8 --- /dev/null +++ b/openspec/changes/archive/2026-07-02-migrate-pandas-to-polars/tasks.md @@ -0,0 +1,57 @@ +## 1. Update Schema Definitions + +- [x] 1.1 Update imports in src/order_matching/schemas.py from `pandera.pandas` to `pandera.polars` +- [x] 1.2 Update DataFrame type import from `pandera.typing.pandas` to `pandera.typing.polars` +- [x] 1.3 Ensure all Field constraints (gt, ge, isin, unique, nullable) are preserved +- [x] 1.4 Verify Config classes with strict=True remain unchanged +- [x] 1.5 Run mypy to check type annotations are correct + +## 2. Update Orders.to_frame() Method + +- [x] 2.1 Replace pandas import with polars in src/order_matching/orders.py +- [x] 2.2 Update empty DataFrame case to return polars DataFrame with explicit OrderDataSchema schema +- [x] 2.3 Replace `pd.DataFrame.from_records()` with `polars.DataFrame()` constructor +- [x] 2.4 Replace `.assign()` with `.with_columns()` for type conversions (side, execution, status to string) +- [x] 2.5 Update timestamp and expiration columns to use polars datetime casting +- [x] 2.6 Update return type hint to `DataFrame[OrderDataSchema]` using polars typing +- [x] 2.7 Verify method signature remains unchanged (backward compatible API) + +## 3. Update ExecutedTrades.to_frame() Method + +- [x] 3.1 Replace pandas import with polars in src/order_matching/executed_trades.py +- [x] 3.2 Update empty DataFrame case to return polars DataFrame with explicit TradeDataSchema schema +- [x] 3.3 Replace `pd.DataFrame.from_records()` with `polars.DataFrame()` constructor +- [x] 3.4 Replace `.assign()` with `.with_columns()` for type conversions (side, execution to string) +- [x] 3.5 Update timestamp column to use polars datetime casting +- [x] 3.6 Update return type hint to `DataFrame[TradeDataSchema]` using polars typing +- [x] 3.7 Verify method signature remains unchanged + +## 4. Update OrderBook.summary() Method + +- [x] 4.1 Replace pandas import with polars in src/order_matching/order_book.py +- [x] 4.2 Replace pandas DataFrame construction in summary() with polars DataFrame +- [x] 4.3 Update count column casting to use polars integer type +- [x] 4.4 Replace `pd.concat()` with polars concatenation (use `pl.concat()`) +- [x] 4.5 Update return type hint to `DataFrame[OrderBookSummarySchema]` using polars typing +- [x] 4.6 Verify empty OrderBook case returns empty polars DataFrame +- [x] 4.7 Update `get_imbalance()` method to work with polars DataFrames + +## 5. Run Tests and Validate + +- [x] 5.1 Run full test suite: `uv run pytest` +- [x] 5.2 Fix any tests in tests/test_order.py that check pandas-specific behavior +- [x] 5.3 Fix any tests in tests/test_trade.py that check pandas-specific behavior +- [x] 5.4 Fix any tests in tests/test_executed_trades.py that check pandas-specific behavior +- [x] 5.5 Verify all pandera schema validations still pass +- [x] 5.6 Check test coverage remains at current level + +## 6. Type Checking and Linting + +- [x] 6.1 Run type checker: `uv run prek run -v --show-diff-on-failure --all-files` +- [x] 6.2 Fix any type errors related to polars DataFrame usage + +## 7. Documentation and Examples + +- [x] 7.1 Update README.md code examples to reflect polars DataFrames +- [x] 7.2 Update any docstrings that mention pandas to reference polars +- [x] 7.3 Verify documentation builds successfully: `uv run mkdocs build` diff --git a/openspec/config.yaml b/openspec/config.yaml new file mode 100644 index 0000000..392946c --- /dev/null +++ b/openspec/config.yaml @@ -0,0 +1,20 @@ +schema: spec-driven + +# Project context (optional) +# This is shown to AI when creating artifacts. +# Add your tech stack, conventions, style guides, domain knowledge, etc. +# Example: +# context: | +# Tech stack: TypeScript, React, Node.js +# We use conventional commits +# Domain: e-commerce platform + +# Per-artifact rules (optional) +# Add custom rules for specific artifacts. +# Example: +# rules: +# proposal: +# - Keep proposals under 500 words +# - Always include a "Non-goals" section +# tasks: +# - Break tasks into chunks of max 2 hours diff --git a/openspec/specs/pandera-polars-validation/spec.md b/openspec/specs/pandera-polars-validation/spec.md new file mode 100644 index 0000000..9c871ce --- /dev/null +++ b/openspec/specs/pandera-polars-validation/spec.md @@ -0,0 +1,51 @@ +## ADDED Requirements + +### Requirement: Validate polars DataFrames with pandera schemas +The system SHALL use pandera to validate polars DataFrames against existing schema definitions (`OrderDataSchema`, `TradeDataSchema`, `OrderBookSummarySchema`). + +#### Scenario: Valid polars DataFrame passes schema validation +- **WHEN** a polars DataFrame with correct schema is validated against `OrderDataSchema`, `TradeDataSchema`, or `OrderBookSummarySchema` +- **THEN** validation succeeds without errors + +#### Scenario: Invalid polars DataFrame fails schema validation +- **WHEN** a polars DataFrame with incorrect schema (wrong columns, wrong types, or constraint violations) is validated +- **THEN** validation raises a pandera validation error with details + +### Requirement: Maintain schema constraint validation +The system SHALL validate all pandera Field constraints (gt, ge, isin, unique, nullable) on polars DataFrames. + +#### Scenario: Price constraint validation +- **WHEN** a polars DataFrame contains price values <= 0 +- **THEN** validation against schemas with `price: Field(gt=0)` fails + +#### Scenario: Enum value constraint validation +- **WHEN** a polars DataFrame contains invalid enum values in side/execution/status columns +- **THEN** validation against schemas with `Field(isin=[...])` fails + +#### Scenario: Unique constraint validation +- **WHEN** a polars DataFrame for OrderBookSummary contains duplicate price values +- **THEN** validation against `OrderBookSummarySchema` with `price: Field(unique=True)` fails + +#### Scenario: Nullable constraint validation +- **WHEN** a polars DataFrame contains null values in non-nullable columns +- **THEN** validation fails +- **WHEN** a polars DataFrame contains null values in nullable columns (e.g., expiration) +- **THEN** validation succeeds + +### Requirement: Update schema type hints for polars +The system SHALL update type hints in schema definitions to support both pandas and polars DataFrames via pandera's typing system. + +#### Scenario: Schema works with pandera.typing.polars.DataFrame and pandera.typing.polars.LazyFrame +- **WHEN** schemas are imported and used with polars DataFrames +- **THEN** type hints correctly indicate polars DataFrame compatibility + +### Requirement: Preserve strict mode behavior +The system SHALL maintain strict mode validation configured in schema Config classes when validating polars DataFrames. + +#### Scenario: Strict schema rejects extra columns +- **WHEN** a polars DataFrame with extra columns is validated against a schema with `strict = True` +- **THEN** validation fails + +#### Scenario: Strict schema accepts exact columns +- **WHEN** a polars DataFrame with exact required columns is validated against a schema with `strict = True` +- **THEN** validation succeeds diff --git a/openspec/specs/polars-dataframe-conversion/spec.md b/openspec/specs/polars-dataframe-conversion/spec.md new file mode 100644 index 0000000..25f1790 --- /dev/null +++ b/openspec/specs/polars-dataframe-conversion/spec.md @@ -0,0 +1,58 @@ +## ADDED Requirements + +### Requirement: Convert Orders to polars DataFrame +The system SHALL convert `Orders` objects to polars DataFrames using the `to_frame()` method with all columns matching the `OrderDataSchema`. + +#### Scenario: Convert empty Orders to DataFrame +- **WHEN** `to_frame()` is called on an empty `Orders` object +- **THEN** system returns an empty polars DataFrame + +#### Scenario: Convert populated Orders to DataFrame +- **WHEN** `to_frame()` is called on `Orders` containing one or more orders +- **THEN** system returns a polars DataFrame with columns: timestamp, expiration, order_id, trader_id, side, execution, status, price, size, price_number_of_digits +- **THEN** all string enum fields (side, execution, status) are converted to string representations +- **THEN** timestamp and expiration columns are datetime type + +### Requirement: Convert ExecutedTrades to polars DataFrame +The system SHALL convert `ExecutedTrades` objects to polars DataFrames using the `to_frame()` method with all columns matching the `TradeDataSchema`. + +#### Scenario: Convert empty ExecutedTrades to DataFrame +- **WHEN** `to_frame()` is called on an empty `ExecutedTrades` object +- **THEN** system returns an empty polars DataFrame + +#### Scenario: Convert populated ExecutedTrades to DataFrame +- **WHEN** `to_frame()` is called on `ExecutedTrades` containing one or more trades +- **THEN** system returns a polars DataFrame with columns: timestamp, incoming_order_id, book_order_id, trade_id, side, execution, price, size +- **THEN** string enum fields (side, execution) are converted to string representations +- **THEN** timestamp column is datetime type + +### Requirement: Convert OrderBook to polars DataFrame summary +The system SHALL convert `OrderBook` objects to polars DataFrames using the `summary()` method with all columns matching the `OrderBookSummarySchema`. + +#### Scenario: Convert empty OrderBook to summary DataFrame +- **WHEN** `summary()` is called on an empty `OrderBook` +- **THEN** system returns an empty polars DataFrame + +#### Scenario: Convert OrderBook with bids and offers to summary +- **WHEN** `summary()` is called on `OrderBook` containing bids and/or offers +- **THEN** system returns a polars DataFrame with columns: side, price, size, count +- **THEN** each unique price level is represented as a single row +- **THEN** count column is integer type +- **THEN** side column contains "BUY" or "SELL" string values + +### Requirement: Maintain DataFrame schema compatibility +The system SHALL ensure polars DataFrames produced by conversion methods have identical column names, dtypes, and value formats as the pandas DataFrames they replace. + +#### Scenario: Column names match exactly +- **WHEN** any conversion method produces a polars DataFrame +- **THEN** column names MUST match the corresponding pandera schema field names exactly + +#### Scenario: Data types are compatible +- **WHEN** any conversion method produces a polars DataFrame +- **THEN** numeric columns (price, size, count) use appropriate numeric types +- **THEN** datetime columns use polars datetime type +- **THEN** string columns (order_id, trader_id, trade_id, side, execution, status) use string type + +#### Scenario: Empty DataFrames have correct schema +- **WHEN** conversion methods are called on empty collections +- **THEN** resulting empty DataFrames MUST still have the correct column names and types defined diff --git a/pyproject.toml b/pyproject.toml index 8e2e29e..ca17706 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,8 +19,9 @@ classifiers = [ requires-python = ">=3.10" dependencies = [ "faker>=33.0.0", - "pandas>=2.2.3", - "pandera>=0.21.0", + "numpy>=2.2.6", + "pandera[polars]>=0.21.0", + "polars>=1.42.1", ] [dependency-groups] diff --git a/src/order_matching/executed_trades.py b/src/order_matching/executed_trades.py index 1f0be48..e84f9bc 100644 --- a/src/order_matching/executed_trades.py +++ b/src/order_matching/executed_trades.py @@ -5,8 +5,8 @@ from datetime import datetime from typing import cast -import pandas as pd -from pandera.typing.pandas import DataFrame +import polars as pl +from pandera.typing.polars import LazyFrame from order_matching.schemas import TradeDataSchema from order_matching.trade import Trade @@ -57,27 +57,23 @@ def get(self, timestamp: datetime) -> list[Trade]: """ return self._trades[timestamp] - def to_frame(self) -> DataFrame[TradeDataSchema]: - """Get pandas DataFrame of all stored trades. + def to_frame(self) -> LazyFrame[TradeDataSchema]: + """Get polars DataFrame of all stored trades. Returns ------- DataFrame[TradeDataSchema] - pandas DataFrame of all stored trades + polars DataFrame of all stored trades """ trades = self.trades if len(trades) == 0: - return cast(DataFrame[TradeDataSchema], pd.DataFrame()) + return cast(LazyFrame[TradeDataSchema], TradeDataSchema.empty().lazy()) else: - return cast( - DataFrame[TradeDataSchema], - pd.DataFrame.from_records([asdict(trade) for trade in trades]).assign( - **{ - TradeDataSchema.side: lambda df: df[TradeDataSchema.side].astype(str), - TradeDataSchema.execution: lambda df: df[TradeDataSchema.execution].astype(str), - } - ), - ) + data = [asdict(trade) for trade in trades] + for d in data: + d[TradeDataSchema.side] = d[TradeDataSchema.side].name + d[TradeDataSchema.execution] = d[TradeDataSchema.execution].name + return cast(LazyFrame[TradeDataSchema], pl.LazyFrame(data)) def __add__(self, other: ExecutedTrades) -> ExecutedTrades: trades = ExecutedTrades() diff --git a/src/order_matching/order_book.py b/src/order_matching/order_book.py index dd2dc3d..d21585b 100644 --- a/src/order_matching/order_book.py +++ b/src/order_matching/order_book.py @@ -2,8 +2,8 @@ from datetime import datetime from typing import cast -import pandas as pd -from pandera.typing import DataFrame +import polars as pl +from pandera.typing.polars import LazyFrame from order_matching.order import Order from order_matching.orders import Orders @@ -49,37 +49,54 @@ def remove(self, incoming_order: Order) -> None: if len(self.orders_by_expiration[incoming_order.expiration]) == 0: self.orders_by_expiration.pop(incoming_order.expiration) - def summary(self) -> DataFrame[OrderBookSummarySchema]: - """Summary of the order book as a pandas DataFrame. + def summary(self) -> LazyFrame[OrderBookSummarySchema]: + """Summary of the order book as a polars LazyFrame. Returns ------- - DataFrame[OrderBookSummarySchema] - Summary of the order book as a pandas DataFrame + LazyFrame[OrderBookSummarySchema] + Summary of the order book as a polars LazyFrame """ - bids = pd.DataFrame( - { - OrderBookSummarySchema.side: Side.BUY.name, - OrderBookSummarySchema.price: self._get_bid_prices(), - OrderBookSummarySchema.size: self._get_bid_sizes(), - OrderBookSummarySchema.count: self._get_bid_counts(), - } + bid_prices = self._get_bid_prices() + offer_prices = self._get_offer_prices() + empty_df = OrderBookSummarySchema.empty().lazy() + + if len(bid_prices) == 0 and len(offer_prices) == 0: + return cast(LazyFrame[OrderBookSummarySchema], empty_df) + + dtypes = [ + pl.col(OrderBookSummarySchema.price).cast(pl.Float64), + pl.col(OrderBookSummarySchema.size).cast(pl.Float64), + pl.col(OrderBookSummarySchema.count).cast(pl.Int64), + ] + bids = ( + pl.LazyFrame( + { + OrderBookSummarySchema.side: [Side.BUY.name] * len(bid_prices), + OrderBookSummarySchema.price: bid_prices, + OrderBookSummarySchema.size: self._get_bid_sizes(), + OrderBookSummarySchema.count: self._get_bid_counts(), + } + ).with_columns(dtypes) + if len(bid_prices) > 0 + else empty_df ) - offers = pd.DataFrame( - { - OrderBookSummarySchema.side: Side.SELL.name, - OrderBookSummarySchema.price: self._get_offer_prices(), - OrderBookSummarySchema.size: self._get_offer_sizes(), - OrderBookSummarySchema.count: self._get_offer_counts(), - } - ) - return cast( - DataFrame[OrderBookSummarySchema], - pd.concat([bids, offers], ignore_index=True).assign( - **{OrderBookSummarySchema.count: lambda df: df[OrderBookSummarySchema.count].astype(int)} - ), + + offers = ( + pl.LazyFrame( + { + OrderBookSummarySchema.side: [Side.SELL.name] * len(offer_prices), + OrderBookSummarySchema.price: offer_prices, + OrderBookSummarySchema.size: self._get_offer_sizes(), + OrderBookSummarySchema.count: self._get_offer_counts(), + } + ).with_columns(dtypes) + if len(offer_prices) > 0 + else empty_df ) + return cast(LazyFrame[OrderBookSummarySchema], pl.concat([bids, offers], how="vertical")) + @property def current_price(self) -> float: """Current market price.""" @@ -186,23 +203,36 @@ def get_imbalance(self, price_range: float = 0.1) -> float: Market imbalance indicator """ summary = self.summary() - if summary.empty: + if self._is_empty(summary): return 0 - elif summary[summary[OrderBookSummarySchema.side] == Side.SELL.name].empty: + elif self._is_empty(summary.filter(pl.col(OrderBookSummarySchema.side) == Side.SELL.name)): return 1 - elif summary[summary[OrderBookSummarySchema.side] == Side.BUY.name].empty: + elif self._is_empty(summary.filter(pl.col(OrderBookSummarySchema.side) == Side.BUY.name)): return -1 else: return self._get_non_trivial_imbalance(price_range=price_range) + @staticmethod + def _is_empty(df: pl.LazyFrame) -> bool: + return df.select(pl.col(OrderBookSummarySchema.side).count()).collect().item() == 0 + def _get_non_trivial_imbalance(self, price_range: float) -> float: schema = OrderBookSummarySchema upper_bound = self.current_price + price_range lower_bound = self.current_price - price_range - summary_subset = self.summary().pipe(lambda df: df[df[schema.price].between(lower_bound, upper_bound)]) - summary_subset = cast(pd.DataFrame, summary_subset) - buy_volume = summary_subset.loc[summary_subset[schema.side] == Side.BUY.name, schema.size].sum() - sell_volume = summary_subset.loc[summary_subset[schema.side] == Side.SELL.name, schema.size].sum() + summary_subset = self.summary().filter(pl.col(schema.price).is_between(lower_bound, upper_bound)) + buy_volume = ( + summary_subset.filter(pl.col(schema.side) == Side.BUY.name) + .select(pl.col(schema.size).sum()) + .collect() + .item() + ) + sell_volume = ( + summary_subset.filter(pl.col(schema.side) == Side.SELL.name) + .select(pl.col(schema.size).sum()) + .collect() + .item() + ) if buy_volume + sell_volume > 0: return (buy_volume - sell_volume) / (buy_volume + sell_volume) else: diff --git a/src/order_matching/orders.py b/src/order_matching/orders.py index 156b454..89054dc 100644 --- a/src/order_matching/orders.py +++ b/src/order_matching/orders.py @@ -3,8 +3,8 @@ from dataclasses import asdict from typing import Iterator, Sequence, cast -import pandas as pd -from pandera.typing.pandas import DataFrame +import polars as pl +from pandera.typing.polars import LazyFrame from order_matching.order import Order from order_matching.schemas import OrderDataSchema @@ -55,32 +55,22 @@ def remove(self, orders: list[Order]) -> None: if order_to_remove.order_id in self._order_ids: self.orders.remove(self._get_order(order_id=order_to_remove.order_id)) - def to_frame(self) -> DataFrame[OrderDataSchema]: - """Get pandas DataFrame with all orders in the storage. + def to_frame(self) -> LazyFrame[OrderDataSchema]: + """Get polars LazyFrame with all orders in the storage. Returns ------- - DataFrame[OrderDataSchema] + LazyFrame[OrderDataSchema] """ if len(self.orders) == 0: - return cast(DataFrame[OrderDataSchema], pd.DataFrame()) + return cast(LazyFrame[OrderDataSchema], OrderDataSchema.empty().lazy()) else: - return cast( - DataFrame[OrderDataSchema], - pd.DataFrame.from_records([asdict(order) for order in self.orders]).assign( - **{ - OrderDataSchema.side: lambda df: df[OrderDataSchema.side].astype(str), - OrderDataSchema.execution: lambda df: df[OrderDataSchema.execution].astype(str), - OrderDataSchema.status: lambda df: df[OrderDataSchema.status].astype(str), - OrderDataSchema.timestamp: lambda df: pd.to_datetime( - df[OrderDataSchema.timestamp], errors="coerce" - ), - OrderDataSchema.expiration: lambda df: pd.to_datetime( - df[OrderDataSchema.expiration], errors="coerce" - ), - } - ), - ) + data = [asdict(order) for order in self.orders] + for d in data: + d[OrderDataSchema.side] = d[OrderDataSchema.side].name + d[OrderDataSchema.execution] = d[OrderDataSchema.execution].name + d[OrderDataSchema.status] = d[OrderDataSchema.status].name + return cast(LazyFrame[OrderDataSchema], pl.LazyFrame(data)) @property def is_empty(self) -> bool: diff --git a/src/order_matching/schemas.py b/src/order_matching/schemas.py index a842622..8f3a8d2 100644 --- a/src/order_matching/schemas.py +++ b/src/order_matching/schemas.py @@ -1,7 +1,7 @@ from datetime import datetime -from pandera.pandas import DataFrameModel, Field -from pandera.typing import Series +from pandera.polars import DataFrameModel, Field +from pandera.typing.polars import Series from order_matching.execution import Execution from order_matching.side import Side diff --git a/tests/conftest.py b/tests/conftest.py index e6ceb33..9bf85a3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ +from datetime import datetime, timedelta + import numpy as np -import pandas as pd import pytest from order_matching.order import LimitOrder @@ -14,7 +15,7 @@ def random_orders() -> Orders: rng = get_random_generator(seed=42) orders_per_timestamp, number_of_time_points = 100, 10 time_intervals = np.array(rng.uniform(low=0, high=1, size=number_of_time_points)) - random_timestamps = pd.Timestamp(2023, 1, 1) + pd.to_timedelta(time_intervals.cumsum(), unit="D") + random_timestamps = [datetime(2023, 1, 1) + timedelta(days=float(days)) for days in time_intervals.cumsum()] orders = list() for timestamp in random_timestamps: prices = rng.lognormal(mean=1, size=orders_per_timestamp).round(decimals=1) diff --git a/tests/test_executed_trades.py b/tests/test_executed_trades.py index e33d722..2928deb 100644 --- a/tests/test_executed_trades.py +++ b/tests/test_executed_trades.py @@ -1,8 +1,6 @@ from copy import deepcopy from datetime import datetime, timedelta -import pandas as pd - from order_matching.executed_trades import ExecutedTrades from order_matching.execution import Execution from order_matching.schemas import TradeDataSchema @@ -55,7 +53,7 @@ def test_get(self) -> None: def test_to_frame(self) -> None: executed_trades = ExecutedTrades() - pd.testing.assert_frame_equal(executed_trades.to_frame(), pd.DataFrame()) + assert executed_trades.to_frame().collect().equals(TradeDataSchema.empty()) first_trade, second_trade = self._get_sample_trades() executed_trades.add(trades=[first_trade, second_trade]) diff --git a/tests/test_order.py b/tests/test_order.py index d3b895f..06671ee 100644 --- a/tests/test_order.py +++ b/tests/test_order.py @@ -1,6 +1,6 @@ from datetime import datetime -import pandas as pd +import polars as pl import pytest from order_matching.execution import Execution @@ -13,8 +13,19 @@ class TestOrder: @pytest.mark.parametrize("side", [Side.BUY, Side.SELL]) @pytest.mark.parametrize("price", [1.2, 2.4]) @pytest.mark.parametrize("size", [10, 4.1]) - @pytest.mark.parametrize("timestamp", pd.date_range(start="2022", periods=3)) - @pytest.mark.parametrize("expiration", [None, *pd.date_range(start="2023", periods=3)]) + @pytest.mark.parametrize( + "timestamp", + pl.datetime_range(start=datetime(2022, 1, 1), end=datetime(2022, 1, 3), interval="1d", eager=True).to_list(), + ) + @pytest.mark.parametrize( + "expiration", + [ + None, + *pl.datetime_range( + start=datetime(2023, 1, 1), end=datetime(2023, 1, 3), interval="1d", eager=True + ).to_list(), + ], + ) @pytest.mark.parametrize("order_id", ["a", "b"]) @pytest.mark.parametrize("trader_id", ["x", "y"]) @pytest.mark.parametrize("execution", [Execution.LIMIT, Execution.MARKET]) @@ -61,7 +72,10 @@ class TestLimitOrder: @pytest.mark.parametrize("side", [Side.BUY, Side.SELL]) @pytest.mark.parametrize("price", [1.2, 2.4]) @pytest.mark.parametrize("size", [10, 4.1]) - @pytest.mark.parametrize("timestamp", pd.date_range(start="2022", periods=3)) + @pytest.mark.parametrize( + "timestamp", + pl.datetime_range(start=datetime(2022, 1, 1), end=datetime(2022, 1, 3), interval="1d", eager=True).to_list(), + ) @pytest.mark.parametrize("order_id", ["a", "b"]) @pytest.mark.parametrize("trader_id", ["x", "y"]) @pytest.mark.parametrize("price_number_of_digits", [1, 3]) @@ -98,7 +112,10 @@ def test_order_required_defaults( class TestMarketOrder: @pytest.mark.parametrize("side", [Side.BUY, Side.SELL]) @pytest.mark.parametrize("size", [10, 4.1]) - @pytest.mark.parametrize("timestamp", pd.date_range(start="2022", periods=3)) + @pytest.mark.parametrize( + "timestamp", + pl.datetime_range(start=datetime(2022, 1, 1), end=datetime(2022, 1, 3), interval="1d", eager=True).to_list(), + ) @pytest.mark.parametrize("order_id", ["a", "b"]) @pytest.mark.parametrize("trader_id", ["x", "y"]) def test_order_required_defaults( diff --git a/tests/test_order_book.py b/tests/test_order_book.py index 25d1cce..09661eb 100644 --- a/tests/test_order_book.py +++ b/tests/test_order_book.py @@ -144,8 +144,7 @@ def test_order_book_summary(self) -> None: summary = order_book.summary() OrderBookSummarySchema.validate(summary, lazy=True) - assert summary.index.is_monotonic_increasing - assert summary[OrderBookSummarySchema.price].is_monotonic_increasing + assert summary.collect()[OrderBookSummarySchema.price].is_sorted() assert order_book.current_price == 2.3 def test_order_book_imbalance_one_buy_order(self) -> None: diff --git a/tests/test_trade.py b/tests/test_trade.py index 9999a5e..5d13988 100644 --- a/tests/test_trade.py +++ b/tests/test_trade.py @@ -1,7 +1,7 @@ from dataclasses import asdict from datetime import datetime -import pandas as pd +import polars as pl import pytest from order_matching.execution import Execution @@ -14,7 +14,10 @@ class TestTrade: @pytest.mark.parametrize("side", [Side.BUY, Side.SELL]) @pytest.mark.parametrize("price", [1.2, 2.4]) @pytest.mark.parametrize("size", [10.0, 4.1]) - @pytest.mark.parametrize("timestamp", pd.date_range(start="2022", periods=3)) + @pytest.mark.parametrize( + "timestamp", + pl.datetime_range(start=datetime(2022, 1, 1), end=datetime(2022, 1, 3), interval="1d", eager=True).to_list(), + ) @pytest.mark.parametrize("incoming_order_id", ["a", "abc"]) @pytest.mark.parametrize("book_order_id", ["a", "abc"]) @pytest.mark.parametrize("trade_id", ["t", "tr"]) @@ -48,10 +51,8 @@ def test_trade_required_defaults( assert trade.book_order_id == book_order_id assert trade.execution == execution assert trade.trade_id == trade_id - trades = pd.DataFrame(asdict(trade), index=pd.Index([0])).assign( - **{ - TradeDataSchema.side: lambda df: df[TradeDataSchema.side].astype(str), - TradeDataSchema.execution: lambda df: df[TradeDataSchema.execution].astype(str), - } - ) + trade_dict = asdict(trade) + trade_dict[TradeDataSchema.side] = trade_dict[TradeDataSchema.side].name + trade_dict[TradeDataSchema.execution] = trade_dict[TradeDataSchema.execution].name + trades = pl.LazyFrame([trade_dict]) TradeDataSchema.validate(trades, lazy=True) diff --git a/uv.lock b/uv.lock index bd11148..dcdec96 100644 --- a/uv.lock +++ b/uv.lock @@ -881,9 +881,11 @@ version = "0.3.16" source = { editable = "." } dependencies = [ { name = "faker" }, - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "pandera" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "pandera", extra = ["polars"] }, + { name = "polars" }, ] [package.dev-dependencies] @@ -909,8 +911,9 @@ test = [ [package.metadata] requires-dist = [ { name = "faker", specifier = ">=33.0.0" }, - { name = "pandas", specifier = ">=2.2.3" }, - { name = "pandera", specifier = ">=0.21.0" }, + { name = "numpy", specifier = ">=2.2.6" }, + { name = "pandera", extras = ["polars"], specifier = ">=0.21.0" }, + { name = "polars", specifier = ">=1.42.1" }, ] [package.metadata.requires-dev] @@ -951,142 +954,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, ] -[[package]] -name = "pandas" -version = "2.3.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "python-dateutil", marker = "python_full_version < '3.11'" }, - { name = "pytz", marker = "python_full_version < '3.11'" }, - { name = "tzdata", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, - { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, - { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, - { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, - { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, - { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, - { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, - { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, - { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, - { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, - { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, - { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, - { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, - { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, - { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, - { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, - { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, - { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, - { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, - { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, - { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, - { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, - { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, - { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, - { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, - { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, - { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, - { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, - { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, - { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, - { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, - { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, - { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, - { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, - { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, - { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, - { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, - { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, - { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, - { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, -] - -[[package]] -name = "pandas" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] -dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "numpy", version = "2.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, - { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/16/b5c76b838fd9bf6ce84d3a53346b8874ec05c5f0040d75ef2c320100cd2a/pandas-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:455f6f8139d4282188f526868dbc3c828470e88a3d9d59a891bd46a455f21b98", size = 10338495, upload-time = "2026-05-11T18:52:11.558Z" }, - { url = "https://files.pythonhosted.org/packages/5a/b0/a4ffc4ae74d2d822200dcc46898987d8eb6032d1e2b219cae39da6f5cbcc/pandas-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4e15135e2ee5df1063313e2425ceef8ac0f4ae775893815b0923651b806a5639", size = 9938250, upload-time = "2026-05-11T18:52:17.005Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b2/3323601a52caee42c019e370090ca4544b241437240ca04f786cce82b0cf/pandas-3.0.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05f1f1752b8533ea03f7f39a9c15b1a058d067bb48f4748948e7a8691e0510f2", size = 10770558, upload-time = "2026-05-11T18:52:19.865Z" }, - { url = "https://files.pythonhosted.org/packages/32/f1/bbecd2f867b97abebe0f9b53d750f862251b40337e061b36676ded3d920f/pandas-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a1e45c80cceb3b4a21bc5939d52e8cbd8d9b7305309219d59e9754d9ce09e27", size = 11274611, upload-time = "2026-05-11T18:52:22.622Z" }, - { url = "https://files.pythonhosted.org/packages/7f/4f/eafabf2d5fae5adf143b4d18d3706c5efdc368a7c4eb1ee8a3eddabbd0f6/pandas-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:14da8316da4d0c5a77618425996bfb1248ca87fc2c1486e6fde4652bd18b5824", size = 11784670, upload-time = "2026-05-11T18:52:25.4Z" }, - { url = "https://files.pythonhosted.org/packages/49/44/1eb20389301b57b19cc099a1c2f662501f72f08a65f912d05822613c1532/pandas-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a55066a0505dae0ba2b50a46637db34b46f9094c65c5d4800794ef6335010938", size = 12353708, upload-time = "2026-05-11T18:52:28.139Z" }, - { url = "https://files.pythonhosted.org/packages/eb/62/c321f13b5ba1819fc8dca456c7fce578da2dcfecff1abbf0eaddf8406c0f/pandas-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6674ab18ad8c57802867264b00e15e7bb904700cdd9046e3b2fa1fce237439ea", size = 9907609, upload-time = "2026-05-11T18:52:30.982Z" }, - { url = "https://files.pythonhosted.org/packages/53/85/1b7f563ebc6357c27233a02a96b589bcce1fa9c6eb89fb4f0e56421d277e/pandas-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:5cc09a68b3120e0f54870dede8287a7bb1fa463907e4fcec1ea77cab6179bf7a", size = 9165596, upload-time = "2026-05-11T18:52:33.334Z" }, - { url = "https://files.pythonhosted.org/packages/24/f1/392f8c5bfc16f66a0d2d41561c01627c228fe7ed2a0d056ef11315042570/pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09", size = 10357846, upload-time = "2026-05-11T18:52:36.143Z" }, - { url = "https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4", size = 9899550, upload-time = "2026-05-11T18:52:38.976Z" }, - { url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965, upload-time = "2026-05-11T18:52:41.915Z" }, - { url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600, upload-time = "2026-05-11T18:52:45.02Z" }, - { url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824, upload-time = "2026-05-11T18:52:48.058Z" }, - { url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889, upload-time = "2026-05-11T18:52:50.905Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc", size = 9755463, upload-time = "2026-05-11T18:52:53.386Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/fda8f9705b1b09c6ebe14bfc0fa0e4ec8584d54ea673628f157ff55131af/pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49", size = 9066158, upload-time = "2026-05-11T18:52:56.038Z" }, - { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" }, - { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" }, - { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" }, - { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" }, - { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" }, - { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" }, - { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" }, - { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" }, - { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" }, - { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" }, - { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" }, - { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" }, - { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" }, - { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" }, - { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" }, - { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" }, - { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" }, - { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" }, - { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" }, - { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" }, - { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" }, - { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" }, - { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" }, - { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" }, - { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" }, - { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" }, -] - [[package]] name = "pandera" version = "0.32.1" @@ -1103,6 +970,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/d0/411c82285a7586e97326020f6b5ecbc2f2ffcbef72aa108c897de1b0a540/pandera-0.32.1-py3-none-any.whl", hash = "sha256:1a17a3ffa906174d19207715f4f082ec3db3709647927ad8c095c147d74d8454", size = 447840, upload-time = "2026-06-29T16:01:46.877Z" }, ] +[package.optional-dependencies] +polars = [ + { name = "polars" }, +] + [[package]] name = "pathspec" version = "1.1.1" @@ -1130,6 +1002,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "polars" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "polars-runtime-32" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/99/fe77f10a13a778705ef05b499fc708c9a0b0a3680d9eb6bc6e1b6a6b9914/polars-1.42.1.tar.gz", hash = "sha256:2fe94f3059334650bd850ae19a9c165dcd5d9cb12cd95ea04de2201662e70e8a", size = 741532, upload-time = "2026-06-30T04:57:51.504Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/6a/edd939cc6fa04b6415aaa9bf19720fc74ead81234b3d38542e0005816d4d/polars-1.42.1-py3-none-any.whl", hash = "sha256:3c0c65cdfa21a621650c4bdcbbccf93964d052fd766c3e70e84a55d961c259fd", size = 837622, upload-time = "2026-06-30T04:56:34.686Z" }, +] + +[[package]] +name = "polars-runtime-32" +version = "1.42.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/59/15bcc4dac380c6d63efa5446d8317f22671cbd6c9dadd576bd17a334c45a/polars_runtime_32-1.42.1.tar.gz", hash = "sha256:4d4809e1c1b9a6611f6944f27b24abea902b5159e6b6fa262fd716e947af5afd", size = 3045460, upload-time = "2026-06-30T04:57:52.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/29/16ff6e4e91d71e530d3581f45e342a9cc35072ac6b31dcbc2fa33de2569e/polars_runtime_32-1.42.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bbdc26d68ee5b23b0ce227fa0599220aa35b77c826b6b0a6b2d8e7f6c1c36974", size = 53117325, upload-time = "2026-06-30T04:56:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/04/8e/4f8296fcfd1347f1351342fecf13bf2430d7efbae2f1f45964ec7930a99e/polars_runtime_32-1.42.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f6c0288be940b607dc4a7476c01e67fb6bbee93f5f1dd42c64970274c71008ba", size = 47446251, upload-time = "2026-06-30T04:56:41.459Z" }, + { url = "https://files.pythonhosted.org/packages/88/2e/0d66a7deadc453b890c3391034ca8ab4b05d0beaebbb92a7d65199fba61b/polars_runtime_32-1.42.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635d9dbcae2302ae223afb395d5cd220bffa61a53d0ab6871d17c8bc830101cf", size = 51359402, upload-time = "2026-06-30T04:56:44.595Z" }, + { url = "https://files.pythonhosted.org/packages/5d/10/ffb85fa380bc9c9000dc35f40f44954dde49023018501c54faab94b3a39e/polars_runtime_32-1.42.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d059e8e53cc114ff82f9bd791fd341dc53534a2c745e6f6aa37594c3a93f01fe", size = 57302723, upload-time = "2026-06-30T04:56:47.609Z" }, + { url = "https://files.pythonhosted.org/packages/c2/63/ca50adc62e44224ca5c622a842ba6f35ee87d1d40ef0df7ea2ed6c6edb08/polars_runtime_32-1.42.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f91f0b13588324905682809d270e1de5f1990c908721c8527657d77a044c9919", size = 51515673, upload-time = "2026-06-30T04:56:50.88Z" }, + { url = "https://files.pythonhosted.org/packages/63/c3/08fbbf38deaa17bf34a601d327cb7451074098673c78b7c1a8538dde9794/polars_runtime_32-1.42.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b8bf972d99d48aaaa2582e2bce966a6f43bc815bd8725d15f5cab9e2fb15d17", size = 55217259, upload-time = "2026-06-30T04:56:53.834Z" }, + { url = "https://files.pythonhosted.org/packages/d1/0e/51db89361668fe077a835fc579277f824ba526e7daf7b94d23d25439e0d0/polars_runtime_32-1.42.1-cp310-abi3-win_amd64.whl", hash = "sha256:e9364c26da389a8b7339e4d29e20a3d12af730247e6ed3b7804bddce2477f428", size = 52715432, upload-time = "2026-06-30T04:56:57.109Z" }, + { url = "https://files.pythonhosted.org/packages/76/c5/2fb8592d691bd114de25d9c84300b23541dca7060eac11d7b4bed0327786/polars_runtime_32-1.42.1-cp310-abi3-win_arm64.whl", hash = "sha256:7051226e6b42ffc395a7a9190377cd28649fbfb991b8f85c6271f4e1cfb736fb", size = 46718300, upload-time = "2026-06-30T04:56:59.855Z" }, +] + [[package]] name = "prek" version = "0.4.5" @@ -1437,15 +1337,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] -[[package]] -name = "pytz" -version = "2026.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, -] - [[package]] name = "pyyaml" version = "6.0.3"