From 0916869798a2eac0f788fe900c3d25651f1c0cbf Mon Sep 17 00:00:00 2001 From: GitHub Actions Date: Thu, 16 Apr 2026 09:09:56 +0000 Subject: [PATCH 1/4] Auto commit from main repo: manual-sync --- .github/workflows/lint.yml | 5 +- armis_sdk/__init__.py | 4 +- armis_sdk/clients/assets_client.py | 74 ++-- armis_sdk/clients/collectors_client.py | 24 +- armis_sdk/clients/data_export_client.py | 36 +- .../device_custom_properties_client.py | 29 +- armis_sdk/clients/sites_client.py | 18 +- armis_sdk/core/armis_auth.py | 27 +- armis_sdk/core/armis_client.py | 32 +- armis_sdk/core/armis_error.py | 21 +- armis_sdk/core/armis_sdk.py | 17 +- armis_sdk/core/base_entity.py | 2 +- armis_sdk/core/base_entity_client.py | 17 +- armis_sdk/core/client_credentials.py | 12 +- armis_sdk/core/response_utils.py | 7 +- armis_sdk/entities/asq_rule.py | 13 +- armis_sdk/entities/asset.py | 12 +- armis_sdk/entities/data_export/application.py | 17 +- .../data_export/base_exported_entity.py | 7 +- armis_sdk/entities/data_export/data_export.py | 4 +- armis_sdk/entities/data_export/risk_factor.py | 40 +- .../entities/data_export/vulnerability.py | 23 +- armis_sdk/entities/device.py | 69 ++-- armis_sdk/entities/device_custom_property.py | 18 +- armis_sdk/entities/download_progress.py | 2 +- armis_sdk/entities/network_interface.py | 27 +- armis_sdk/entities/site.py | 47 +-- armis_sdk/types/asset_id_source.py | 1 + armis_sdk/types/collector_image_type.py | 1 + poetry.lock | 300 +++------------ pylintrc | 11 - pyproject.toml | 8 +- ruff.toml | 347 ++++++++++++++++++ tests/armis_sdk/armis_sdk_test.py | 1 + tests/armis_sdk/clients/assets_client_test.py | 74 +--- tests/armis_sdk/clients/assets_test_data.py | 1 + .../clients/collectors_client_test.py | 9 +- .../clients/data_export_client_test.py | 13 +- .../device_custom_properties_client_test.py | 13 +- tests/armis_sdk/clients/sites_client_test.py | 13 +- tests/armis_sdk/core/armis_client_test.py | 17 +- tests/armis_sdk/entities/asq_rule_test.py | 12 + .../entities/data_export/application_test.py | 1 + .../entities/data_export/risk_factor_test.py | 1 + .../data_export/vulnerability_test.py | 1 + tests/armis_sdk/entities/device_test.py | 24 ++ tests/armis_sdk/entities/site_test.py | 13 + tests/plugins/auto_setup_plugin.py | 3 +- 48 files changed, 805 insertions(+), 663 deletions(-) delete mode 100644 pylintrc create mode 100644 ruff.toml create mode 100644 tests/armis_sdk/entities/asq_rule_test.py create mode 100644 tests/armis_sdk/entities/device_test.py create mode 100644 tests/armis_sdk/entities/site_test.py diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index fa8541e..565aa0b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,4 +16,7 @@ jobs: - uses: ./.github/workflows/install_deps - name: Run linter - run: poetry run pylint . + run: poetry run ruff check . + + - name: Check formatting + run: poetry run ruff format --check . diff --git a/armis_sdk/__init__.py b/armis_sdk/__init__.py index 6cc01ba..67329af 100644 --- a/armis_sdk/__init__.py +++ b/armis_sdk/__init__.py @@ -1,2 +1,2 @@ -from armis_sdk.core.armis_sdk import ArmisSdk # noqa: F401 -from armis_sdk.core.client_credentials import ClientCredentials # noqa: F401 +from armis_sdk.core.armis_sdk import ArmisSdk +from armis_sdk.core.client_credentials import ClientCredentials diff --git a/armis_sdk/clients/assets_client.py b/armis_sdk/clients/assets_client.py index 21eb22d..9aed6b1 100644 --- a/armis_sdk/clients/assets_client.py +++ b/armis_sdk/clients/assets_client.py @@ -1,7 +1,9 @@ +from __future__ import annotations + import datetime -from typing import AsyncIterator from typing import Optional -from typing import Type +from typing import Type # noqa: UP035 # TODO: fix UP035 (deprecated import, use updated module) +from typing import TYPE_CHECKING from typing import Union import universalasync @@ -18,9 +20,12 @@ from armis_sdk.types.asset_id_source import AssetIdSource +if TYPE_CHECKING: + from collections.abc import AsyncIterator + + @universalasync.wrap -class AssetsClient(BaseEntityClient): # pylint: disable=too-few-public-methods - # pylint: disable=line-too-long +class AssetsClient(BaseEntityClient): """ A client for interacting with assets. @@ -31,10 +36,10 @@ class AssetsClient(BaseEntityClient): # pylint: disable=too-few-public-methods async def list_by_asset_id( self, - asset_class: Type[AssetT], - asset_ids: Union[list[int], list[str]], + asset_class: type[AssetT], + asset_ids: list[int] | list[str], asset_id_source: AssetIdSource = "ASSET_ID", - fields: Optional[list[str]] = None, + fields: list[str] | None = None, ) -> AsyncIterator[AssetT]: """List assets by asset ID or other identifiers. @@ -54,6 +59,7 @@ async def list_by_asset_id( from armis_sdk.clients.assets_client import AssetsClient from armis_sdk.entities.device import Device + async def main(): assets_client = AssetsClient() @@ -68,6 +74,7 @@ async def main(): async for device in assets_client.list_by_asset_id(Device, ipv4_addresses, asset_id_source="IPV4_ADDRESS"): print(device) + asyncio.run(main()) ``` """ @@ -81,9 +88,9 @@ async def main(): async def list_by_last_seen( self, - asset_class: Type[AssetT], - last_seen: Union[datetime.datetime, datetime.timedelta], - fields: Optional[list[str]] = None, + asset_class: type[AssetT], + last_seen: datetime.datetime | datetime.timedelta, + fields: list[str] | None = None, ) -> AsyncIterator[AssetT]: """List assets by last seen timestamp. @@ -106,6 +113,7 @@ async def list_by_last_seen( from armis_sdk.clients.assets_client import AssetsClient from armis_sdk.entities.device import Device + async def main(): assets_client = AssetsClient() @@ -117,10 +125,11 @@ async def main(): async for device in assets_client.list_by_last_seen(Device, datetime.datetime(2025, 12, 8)): print(device) + asyncio.run(main()) ``` """ - filter_: dict[str, Union[str, int]] = {"filter_criteria": "LAST_SEEN"} + filter_: dict[str, str | int] = {"filter_criteria": "LAST_SEEN"} if isinstance(last_seen, datetime.datetime): filter_["last_seen_ge"] = last_seen.isoformat() @@ -132,9 +141,7 @@ async def main(): async for item in self._list_assets(asset_class, fields, filter_): yield item - async def list_fields( - self, asset_class: Type[AssetT] - ) -> AsyncIterator[AssetFieldDescription]: + async def list_fields(self, asset_class: type[AssetT]) -> AsyncIterator[AssetFieldDescription]: """List all available fields for a given asset class. Args: @@ -150,12 +157,14 @@ async def list_fields( from armis_sdk.clients.assets_client import AssetsClient from armis_sdk.entities.device import Device + async def main(): assets_client = AssetsClient() async for field in assets_client.list_fields(Device): print(f"{field.name}: {field.type}") + asyncio.run(main()) ``` """ @@ -174,7 +183,6 @@ async def update( fields: list[str], asset_id_source: AssetIdSource = "ASSET_ID", ) -> None: - # pylint: disable=line-too-long """Bulk update assets. Args: @@ -204,6 +212,7 @@ async def main(): # Update based on the explicit source "IPV4_ADDRESS" await assets_client.update([device], ["custom.MyField"], asset_id_source="IPV4_ADDRESS") + asyncio.run(main()) ``` """ @@ -244,7 +253,7 @@ async def main(): def _create_bulk_update_request( cls, asset: Asset, - asset_id: Union[str, int], + asset_id: str | int, field: str, ): request = {"asset_id": asset_id, "key": field} @@ -266,7 +275,7 @@ def _get_asset_id( asset: Asset, index: int, asset_id_source: AssetIdSource, - ) -> Union[str, int]: + ) -> str | int: if isinstance(asset, Device): return cls._get_device_asset_id(asset, index, asset_id_source) @@ -286,30 +295,22 @@ def _get_device_asset_id( if asset_id_source == "MAC_ADDRESS": if device.mac_addresses is None or len(device.mac_addresses) != 1: - raise ArmisError( - f"Device at index {index} doesn't have exactly one mac address" - ) + raise ArmisError(f"Device at index {index} doesn't have exactly one mac address") return device.mac_addresses[0] if asset_id_source == "IPV4_ADDRESS": if device.ipv4_addresses is None or len(device.ipv4_addresses) != 1: - raise ArmisError( - f"Device at index {index} doesn't have exactly one IPv4 address" - ) + raise ArmisError(f"Device at index {index} doesn't have exactly one IPv4 address") return device.ipv4_addresses[0] if asset_id_source == "IPV6_ADDRESS": if device.ipv6_addresses is None or len(device.ipv6_addresses) != 1: - raise ArmisError( - f"Device at index {index} doesn't have exactly one IPv6 address" - ) + raise ArmisError(f"Device at index {index} doesn't have exactly one IPv6 address") return device.ipv6_addresses[0] if asset_id_source == "SERIAL_NUMBER": if device.serial_numbers is None or len(device.serial_numbers) != 1: - raise ArmisError( - f"Device at index {index} doesn't have exactly one serial number" - ) + raise ArmisError(f"Device at index {index} doesn't have exactly one serial number") return device.serial_numbers[0] raise ArmisError(f"Can't get {asset_id_source!r} of device at index {index}") @@ -324,8 +325,8 @@ def _is_integration_field(cls, field: str) -> bool: async def _list_assets( self, - asset_class: Type[AssetT], - fields: Optional[list[str]], + asset_class: type[AssetT], + fields: list[str] | None, filter_: dict, ) -> AsyncIterator[AssetT]: fields = fields or sorted(asset_class.all_fields()) @@ -345,15 +346,12 @@ def _validate_asset_class(cls, assets: list[AssetT]): asset_types = {type(asset) for asset in assets} if len(asset_types) > 1: asset_types_str = ", ".join(sorted(repr(at.__name__) for at in asset_types)) - raise ArmisError( - "All assets must be of the same type, " - f"got {len(asset_types)} types: {asset_types_str}" - ) + raise ArmisError(f"All assets must be of the same type, got {len(asset_types)} types: {asset_types_str}") @classmethod def _validate_fields( cls, - asset_class: Type[AssetT], + asset_class: type[AssetT], fields: list[str], allow_model_members=True, ): @@ -373,6 +371,4 @@ def _validate_fields( if invalid_fields: fields_str = ", ".join(map(repr, invalid_fields)) - raise ArmisError( - f"The following fields are not supported with this operation: {fields_str}" - ) + raise ArmisError(f"The following fields are not supported with this operation: {fields_str}") diff --git a/armis_sdk/clients/collectors_client.py b/armis_sdk/clients/collectors_client.py index 612a39a..8940319 100644 --- a/armis_sdk/clients/collectors_client.py +++ b/armis_sdk/clients/collectors_client.py @@ -1,7 +1,8 @@ +from __future__ import annotations + import contextlib from typing import IO -from typing import AsyncIterator -from typing import Generator +from typing import TYPE_CHECKING from typing import Union import httpx @@ -14,9 +15,13 @@ from armis_sdk.types.collector_image_type import CollectorImageType +if TYPE_CHECKING: + from collections.abc import AsyncIterator + from collections.abc import Generator + + @universalasync.wrap class CollectorsClient(BaseEntityClient): - # pylint: disable=line-too-long """ A client for interacting with Armis collectors. @@ -25,7 +30,7 @@ class CollectorsClient(BaseEntityClient): async def download_image( self, - destination: Union[str, IO[bytes]], + destination: str | IO[bytes], image_type: CollectorImageType = "OVA", ) -> AsyncIterator[DownloadProgress]: """Download a collector image to a specified destination path / file. @@ -56,6 +61,7 @@ async def main(): async for progress in armis_sdk.collectors.download_image(file): print(progress.percent) + asyncio.run(main()) ``` Will output: @@ -71,7 +77,6 @@ async def main(): async with client.stream("GET", collector_image.url) as response: response.raise_for_status() total_size = int(response.headers.get("Content-Length", "0")) - # pylint: disable-next=contextmanager-generator-missing-cleanup with self.open_file(destination) as file: async for chunk in response.aiter_bytes(): file.write(chunk) @@ -97,6 +102,7 @@ async def main(): collectors_client = CollectorsClient() print(await collectors_client.get_image(image_type="OVA")) + asyncio.run(main()) ``` Will output: @@ -105,17 +111,13 @@ async def main(): ``` """ async with self._armis_client.client() as client: - response = await client.get( - "/v3/collectors/_image", params={"image_type": image_type} - ) + response = await client.get("/v3/collectors/_image", params={"image_type": image_type}) data = response_utils.get_data_dict(response) return CollectorImage.model_validate(data) @classmethod @contextlib.contextmanager - def open_file( - cls, destination: Union[str, IO[bytes]] - ) -> Generator[IO[bytes], None, None]: + def open_file(cls, destination: str | IO[bytes]) -> Generator[IO[bytes], None, None]: if isinstance(destination, str): with open(destination, "wb") as file: yield file diff --git a/armis_sdk/clients/data_export_client.py b/armis_sdk/clients/data_export_client.py index 66d3142..7c45267 100644 --- a/armis_sdk/clients/data_export_client.py +++ b/armis_sdk/clients/data_export_client.py @@ -1,7 +1,7 @@ import asyncio +from collections.abc import AsyncIterator from typing import Any -from typing import AsyncIterator -from typing import Type +from typing import Type # noqa: UP035 # TODO: fix UP035 (deprecated import, use updated module) import pandas import universalasync @@ -16,8 +16,7 @@ @universalasync.wrap class DataExportClient(BaseEntityClient): - - async def disable(self, entity: Type[BaseExportedEntity]): + async def disable(self, entity: type[BaseExportedEntity]): """Disable data export of the entity. Args: @@ -35,12 +34,13 @@ async def main(): data_export_client = DataExportClient() await data_export_client.disable(Application) + asyncio.run(main()) ``` """ await self.toggle(entity, False) - async def enable(self, entity: Type[BaseExportedEntity]): + async def enable(self, entity: type[BaseExportedEntity]): """Enable data export of the entity. Args: @@ -58,13 +58,13 @@ async def main(): data_export_client = DataExportClient() await data_export_client.enable(Application) + asyncio.run(main()) ``` """ await self.toggle(entity, True) - async def iterate(self, entity: Type[T], **kwargs: Any) -> AsyncIterator[T]: - # pylint: disable=line-too-long + async def iterate(self, entity: type[T], **kwargs: Any) -> AsyncIterator[T]: """Iterate over the exported data. Args: @@ -90,6 +90,7 @@ async def main(): async for row in data_export_client.iterate(Application): print(type(row)) + asyncio.run(main()) ``` Will output: @@ -113,30 +114,27 @@ async def main(): async for row in data_export_client.iterate( Application, columns=["device_id", "vendor", "name", "version"], - filters=[("vendor", "in", ["Google", "Microsoft"])] + filters=[("vendor", "in", ["Google", "Microsoft"])], ): print(row.device_id, row.vendor, row.name, row.version) + asyncio.run(main()) ``` """ data_export = await self.get(entity) if not data_export.enabled: - raise ArmisError( - "Data export is disabled for this entity, please enable it first." - ) + raise ArmisError("Data export is disabled for this entity, please enable it first.") if data_export.file_format != "parquet": raise ArmisError("Only parquet files supported") for url in data_export.urls: - data_frame: pandas.DataFrame = await asyncio.to_thread( - pandas.read_parquet, url, **kwargs - ) + data_frame: pandas.DataFrame = await asyncio.to_thread(pandas.read_parquet, url, **kwargs) for _, row in data_frame.iterrows(): yield entity.series_to_model(row) - async def get(self, entity: Type[BaseExportedEntity]) -> DataExport: + async def get(self, entity: type[BaseExportedEntity]) -> DataExport: """Get the `DataExport` of the entity Args: @@ -157,6 +155,7 @@ async def main(): data_export_client = DataExportClient() print(await data_export_client.get(Application)) + asyncio.run(main()) ``` Will output: @@ -169,7 +168,7 @@ async def main(): data = response_utils.get_data_dict(response) return DataExport.model_validate(data) - async def toggle(self, entity: Type[BaseExportedEntity], enabled: bool): + async def toggle(self, entity: type[BaseExportedEntity], enabled: bool): """Enable / disable export of an entity. Args: @@ -191,12 +190,11 @@ async def main(): data_export_client = DataExportClient() await data_export_client.toggle(Application, True) + asyncio.run(main()) ``` """ data = {"enabled": enabled} async with self._armis_client.client() as client: - response = await client.patch( - f"/v3/data-export/{entity.entity_name}", json=data - ) + response = await client.patch(f"/v3/data-export/{entity.entity_name}", json=data) response_utils.raise_for_status(response) diff --git a/armis_sdk/clients/device_custom_properties_client.py b/armis_sdk/clients/device_custom_properties_client.py index 3d435ec..ff88b2d 100644 --- a/armis_sdk/clients/device_custom_properties_client.py +++ b/armis_sdk/clients/device_custom_properties_client.py @@ -1,4 +1,4 @@ -from typing import AsyncIterator +from collections.abc import AsyncIterator import universalasync @@ -18,7 +18,6 @@ class DeviceCustomPropertiesClient(BaseEntityClient): """ async def create(self, property_: DeviceCustomProperty) -> DeviceCustomProperty: - # pylint: disable=line-too-long """Create a `DeviceCustomProperty`. Args: @@ -41,6 +40,7 @@ async def main(): property_ = DeviceCustomProperty(name="MyConfig", type="string") print(await client.create(property_)) + asyncio.run(main()) ``` Will output: @@ -50,8 +50,7 @@ async def main(): """ if property_.id is not None: raise ArmisError( - "Can't create a property that already has an id. " - "Did you mean to call `.update(property_)`?", + "Can't create a property that already has an id. Did you mean to call `.update(property_)`?", ) if not property_.name: @@ -63,14 +62,11 @@ async def main(): payload = property_.model_dump(exclude_none=True) async with self._armis_client.client() as client: - response = await client.post( - "/v3/settings/device-custom-properties", json=payload - ) + response = await client.post("/v3/settings/device-custom-properties", json=payload) data = response_utils.get_data_dict(response) return DeviceCustomProperty.model_validate(data) async def delete(self, property_: DeviceCustomProperty): - # pylint: disable=line-too-long """Delete a `DeviceCustomProperty`. Args: @@ -90,6 +86,7 @@ async def main(): property_ = DeviceCustomProperty(id=1, name="MyConfig", type="string") await client.delete(property_) + asyncio.run(main()) ``` """ @@ -97,13 +94,10 @@ async def main(): raise ArmisError("Can't delete a property without an id.") async with self._armis_client.client() as client: - response = await client.delete( - f"/v3/settings/device-custom-properties/{property_.id}" - ) + response = await client.delete(f"/v3/settings/device-custom-properties/{property_.id}") response_utils.raise_for_status(response) async def get(self, property_id: int) -> DeviceCustomProperty: - # pylint: disable=line-too-long """Get a `DeviceCustomProperty` by its ID. Args: @@ -124,6 +118,7 @@ async def main(): client = DeviceCustomPropertiesClient() print(await client.get(1)) + asyncio.run(main()) ``` Will output: @@ -132,14 +127,11 @@ async def main(): ``` """ async with self._armis_client.client() as client: - response = await client.get( - f"/v3/settings/device-custom-properties/{property_id}" - ) + response = await client.get(f"/v3/settings/device-custom-properties/{property_id}") data = response_utils.get_data_dict(response) return DeviceCustomProperty.model_validate(data) async def list(self) -> AsyncIterator[DeviceCustomProperty]: - # pylint: disable=line-too-long """List all the tenant's `DeviceCustomProperty`s. This method takes care of pagination, so you don't have to deal with it. @@ -175,7 +167,6 @@ async def main(): yield DeviceCustomProperty.model_validate(item) async def update(self, property_: DeviceCustomProperty) -> DeviceCustomProperty: - # pylint: disable=line-too-long """Update a `DeviceCustomProperty`. Only `description` and `allowed_values` are updatable. @@ -203,13 +194,13 @@ async def main(): ) await client.update(property_) + asyncio.run(main()) ``` """ if property_.id is None: raise ArmisError( - "Can't update a property without an id. " - "Did you mean to call `.create(property_)`?", + "Can't update a property without an id. Did you mean to call `.create(property_)`?", ) data = property_.model_dump( diff --git a/armis_sdk/clients/sites_client.py b/armis_sdk/clients/sites_client.py index eac1e30..40ec06d 100644 --- a/armis_sdk/clients/sites_client.py +++ b/armis_sdk/clients/sites_client.py @@ -1,4 +1,4 @@ -from typing import AsyncIterator +from collections.abc import AsyncIterator import universalasync @@ -10,7 +10,6 @@ @universalasync.wrap class SitesClient(BaseEntityClient): - # pylint: disable=line-too-long """ A client for interacting with sites. @@ -40,6 +39,7 @@ async def main(): site_to_create = Site(name="my site") print(await sites_client.create(site_to_create)) + asyncio.run(main()) ``` Will output: @@ -48,10 +48,7 @@ async def main(): ``` """ if site.id is not None: - raise ArmisError( - "Can't create a site that already has an id. " - "Did you mean to call `.update(site)`?" - ) + raise ArmisError("Can't create a site that already has an id. Did you mean to call `.update(site)`?") if not site.name: raise ArmisError("Can't create a site without a name.") @@ -85,6 +82,7 @@ async def main(): site = Site(id=1) await sites_client.delete(site) + asyncio.run(main()) ``` """ @@ -116,6 +114,7 @@ async def main(): sites_client = SitesClient() print(await sites_client.get("1")) + asyncio.run(main()) ``` Will output: @@ -146,6 +145,7 @@ async def main(): sites_client = SitesClient() print(await sites_client.hierarchy()) + asyncio.run(main()) ``` Will output this structure (depending on the actual data): @@ -223,14 +223,12 @@ async def main(): site = Site(id=1, location="new location") await sites_client.update(site) + asyncio.run(main()) ``` """ if site.id is None: - raise ArmisError( - "Can't update a site without an id. " - "Did you mean to call `.create(site)`?" - ) + raise ArmisError("Can't update a site without an id. Did you mean to call `.create(site)`?") data = site.model_dump( exclude={"children", "id"}, diff --git a/armis_sdk/core/armis_auth.py b/armis_sdk/core/armis_auth.py index 3596f3f..b089a5a 100644 --- a/armis_sdk/core/armis_auth.py +++ b/armis_sdk/core/armis_auth.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime import typing from typing import Optional @@ -8,6 +10,7 @@ from armis_sdk.core.armis_error import ArmisError from armis_sdk.core.client_credentials import ClientCredentials + AUTHORIZATION = "Authorization" @@ -28,24 +31,16 @@ class ArmisAuth(httpx.Auth): def __init__(self, base_url: str, credentials: ClientCredentials): self._base_url = base_url self._credentials = credentials - self._access_token: Optional[str] = None - self._expires_at: Optional[datetime.datetime] = None - - def auth_flow( - self, request: httpx.Request - ) -> typing.Generator[httpx.Request, httpx.Response, None]: - if ( - self._access_token is None - or self._expires_at is None - or self._expires_at < datetime.datetime.now() - ): + self._access_token: str | None = None + self._expires_at: datetime.datetime | None = None + + def auth_flow(self, request: httpx.Request) -> typing.Generator[httpx.Request, httpx.Response, None]: + if self._access_token is None or self._expires_at is None or self._expires_at < datetime.datetime.now(): access_token_response = yield self._build_access_token_request() self._update_access_token(access_token_response) if self._access_token is None: - raise ArmisError( - "Something went wrong, there is no access token available." - ) + raise ArmisError("Something went wrong, there is no access token available.") request.headers[AUTHORIZATION] = f"Bearer {self._access_token}" response = yield request @@ -74,6 +69,4 @@ def _build_access_token_request(self): def _update_access_token(self, response: httpx.Response): data = response_utils.get_data_dict(response) self._access_token = data["access_token"] - self._expires_at = datetime.datetime.now() + datetime.timedelta( - seconds=data["expires_in"] - ) + self._expires_at = datetime.datetime.now() + datetime.timedelta(seconds=data["expires_in"]) diff --git a/armis_sdk/core/armis_client.py b/armis_sdk/core/armis_client.py index 1e909c2..287b26c 100644 --- a/armis_sdk/core/armis_client.py +++ b/armis_sdk/core/armis_client.py @@ -1,19 +1,26 @@ +from __future__ import annotations + import importlib.metadata import os import platform -from typing import AsyncIterator from typing import Optional +from typing import TYPE_CHECKING from typing import TypeVar import httpx -import universalasync from httpx_retries import Retry from httpx_retries import RetryTransport +import universalasync from armis_sdk.core import response_utils from armis_sdk.core.armis_auth import ArmisAuth from armis_sdk.core.client_credentials import ClientCredentials + +if TYPE_CHECKING: + from collections.abc import AsyncIterator + + API_BASE_URL = "https://api.armis.com" ARMIS_CLIENT_ID = "ARMIS_CLIENT_ID" ARMIS_CLIENT_SECRET = "ARMIS_CLIENT_SECRET" @@ -38,7 +45,7 @@ @universalasync.wrap -class ArmisClient: # pylint: disable=too-few-public-methods +class ArmisClient: """ A class that provides easy access to the Armis API, taking care of: @@ -48,7 +55,7 @@ class ArmisClient: # pylint: disable=too-few-public-methods 4. Proxy configuration via HTTPS_PROXY and HTTP_PROXY environment variables. """ - def __init__(self, credentials: Optional[ClientCredentials] = None): + def __init__(self, credentials: ClientCredentials | None = None): credentials = self._get_credentials(credentials) self._auth = ArmisAuth(API_BASE_URL, credentials) self._user_agent = " ".join(USER_AGENT_PARTS) @@ -61,7 +68,7 @@ def __init__(self, credentials: Optional[ClientCredentials] = None): except ValueError: self._default_backoff = 0 - def client(self, retries: Optional[int] = None, backoff: Optional[float] = None): + def client(self, retries: int | None = None, backoff: float | None = None): retries = retries if retries is not None else self._default_retries backoff = backoff if backoff is not None else self._default_backoff retry = Retry(total=retries, backoff_factor=backoff) @@ -82,7 +89,7 @@ def client(self, retries: Optional[int] = None, backoff: Optional[float] = None) trust_env=True, ) - async def list(self, url: str, body: Optional[dict] = None) -> AsyncIterator[dict]: + async def list(self, url: str, body: dict | None = None) -> AsyncIterator[dict]: """List all items from a paginated endpoint. Args: @@ -104,6 +111,7 @@ async def main(): async for item in armis_client.list("/v3/settings/sites"): print(item) + asyncio.run(main()) ``` Will output: @@ -130,20 +138,14 @@ async def main(): break @classmethod - def _get_credentials( - cls, credentials: Optional[ClientCredentials] - ) -> ClientCredentials: + def _get_credentials(cls, credentials: ClientCredentials | None) -> ClientCredentials: credentials = credentials or ClientCredentials() credentials.vendor_id = credentials.vendor_id or os.getenv(ARMIS_VENDOR_ID) credentials.audience = credentials.audience or os.getenv(ARMIS_AUDIENCE) credentials.client_id = credentials.client_id or os.getenv(ARMIS_CLIENT_ID) - credentials.client_secret = credentials.client_secret or os.getenv( - ARMIS_CLIENT_SECRET - ) + credentials.client_secret = credentials.client_secret or os.getenv(ARMIS_CLIENT_SECRET) env_scopes = os.getenv(ARMIS_SCOPES) - credentials.scopes = credentials.scopes or ( - env_scopes.split(",") if env_scopes else [] - ) + credentials.scopes = credentials.scopes or (env_scopes.split(",") if env_scopes else []) if not credentials.audience: raise ValueError( diff --git a/armis_sdk/core/armis_error.py b/armis_sdk/core/armis_error.py index c6aea88..897f4da 100644 --- a/armis_sdk/core/armis_error.py +++ b/armis_sdk/core/armis_error.py @@ -3,29 +3,31 @@ while interacting with the SDK. """ +from __future__ import annotations + import json from typing import Optional +from typing import TYPE_CHECKING from typing import Union -from httpx import HTTPStatusError from pydantic import BaseModel +if TYPE_CHECKING: + from httpx import HTTPStatusError + + class DetailItem(BaseModel): - loc: list[Union[str, int]] + loc: list[str | int] msg: str type: str def __str__(self): - return ( - f"Type: {self.type}\n" - f"Message: {self.msg}\n" - f"Location: {json.dumps(self.loc)}" - ) + return f"Type: {self.type}\nMessage: {self.msg}\nLocation: {json.dumps(self.loc)}" class ErrorBody(BaseModel): - detail: Union[str, list[DetailItem]] + detail: str | list[DetailItem] class ArmisError(Exception): @@ -53,7 +55,6 @@ def __init__(self, items: list[BulkUpdateItemError]): class ResponseError(ArmisError): - # pylint: disable=line-too-long """ A class for all errors raised following a non-successful response from the Armis API. For example, if the server returns 400 for invalid input, an instance of this class will be raised. @@ -62,7 +63,7 @@ class ResponseError(ArmisError): def __init__( self, error_body: ErrorBody, - response_errors: Optional[list[HTTPStatusError]] = None, + response_errors: list[HTTPStatusError] | None = None, ): super().__init__(self._get_message(error_body)) self.response_errors = response_errors diff --git a/armis_sdk/core/armis_sdk.py b/armis_sdk/core/armis_sdk.py index e4653d4..8f4ffde 100644 --- a/armis_sdk/core/armis_sdk.py +++ b/armis_sdk/core/armis_sdk.py @@ -1,18 +1,17 @@ +from __future__ import annotations + from typing import Optional from armis_sdk.clients.assets_client import AssetsClient from armis_sdk.clients.collectors_client import CollectorsClient from armis_sdk.clients.data_export_client import DataExportClient -from armis_sdk.clients.device_custom_properties_client import ( - DeviceCustomPropertiesClient, -) +from armis_sdk.clients.device_custom_properties_client import DeviceCustomPropertiesClient from armis_sdk.clients.sites_client import SitesClient from armis_sdk.core.armis_client import ArmisClient from armis_sdk.core.client_credentials import ClientCredentials -class ArmisSdk: # pylint: disable=too-few-public-methods - # pylint: disable=line-too-long +class ArmisSdk: """ The `ArmisSdk` class provides access to the Armis API, while conveniently wraps common actions like authentication, pagination, parsing etc. @@ -33,20 +32,20 @@ class ArmisSdk: # pylint: disable=too-few-public-methods armis_sdk = ArmisSdk() + async def main(): async for site in armis_sdk.sites.list(): print(site) + asyncio.run(main()) ``` """ - def __init__(self, credentials: Optional[ClientCredentials] = None): + def __init__(self, credentials: ClientCredentials | None = None): self.client: ArmisClient = ArmisClient(credentials=credentials) self.assets: AssetsClient = AssetsClient(self.client) self.collectors: CollectorsClient = CollectorsClient(self.client) self.data_export: DataExportClient = DataExportClient(self.client) - self.device_custom_properties: DeviceCustomPropertiesClient = ( - DeviceCustomPropertiesClient(self.client) - ) + self.device_custom_properties: DeviceCustomPropertiesClient = DeviceCustomPropertiesClient(self.client) self.sites: SitesClient = SitesClient(self.client) diff --git a/armis_sdk/core/base_entity.py b/armis_sdk/core/base_entity.py index d10b4a3..b08137d 100644 --- a/armis_sdk/core/base_entity.py +++ b/armis_sdk/core/base_entity.py @@ -1,8 +1,8 @@ from typing import TypeVar +from pydantic import alias_generators from pydantic import BaseModel from pydantic import ConfigDict -from pydantic import alias_generators class BaseEntity(BaseModel): diff --git a/armis_sdk/core/base_entity_client.py b/armis_sdk/core/base_entity_client.py index 0ae2327..bc2933c 100644 --- a/armis_sdk/core/base_entity_client.py +++ b/armis_sdk/core/base_entity_client.py @@ -1,6 +1,8 @@ -from typing import AsyncIterator +from __future__ import annotations + from typing import Optional -from typing import Type +from typing import Type # noqa: UP035 # TODO: fix UP035 (deprecated import, use updated module) +from typing import TYPE_CHECKING import universalasync @@ -8,14 +10,15 @@ from armis_sdk.core.base_entity import BaseEntityT -class BaseEntityClient: # pylint: disable=too-few-public-methods +if TYPE_CHECKING: + from collections.abc import AsyncIterator + - def __init__(self, armis_client: Optional[ArmisClient] = None) -> None: +class BaseEntityClient: + def __init__(self, armis_client: ArmisClient | None = None) -> None: self._armis_client = armis_client or ArmisClient() @universalasync.async_to_sync_wraps - async def _list( - self, url: str, model: Type[BaseEntityT] - ) -> AsyncIterator[BaseEntityT]: + async def _list(self, url: str, model: type[BaseEntityT]) -> AsyncIterator[BaseEntityT]: async for item in self._armis_client.list(url): yield model.model_validate(item) diff --git a/armis_sdk/core/client_credentials.py b/armis_sdk/core/client_credentials.py index e187d6e..385775e 100644 --- a/armis_sdk/core/client_credentials.py +++ b/armis_sdk/core/client_credentials.py @@ -1,11 +1,13 @@ +from __future__ import annotations + import dataclasses from typing import Optional @dataclasses.dataclass class ClientCredentials: - audience: Optional[str] = None - client_id: Optional[str] = None - client_secret: Optional[str] = None - vendor_id: Optional[str] = None - scopes: Optional[list[str]] = None + audience: str | None = None + client_id: str | None = None + client_secret: str | None = None + vendor_id: str | None = None + scopes: list[str] | None = None diff --git a/armis_sdk/core/response_utils.py b/armis_sdk/core/response_utils.py index 3732dbf..0ee5526 100644 --- a/armis_sdk/core/response_utils.py +++ b/armis_sdk/core/response_utils.py @@ -1,6 +1,6 @@ import json from json import JSONDecodeError -from typing import Type +from typing import Type # noqa: UP035 # TODO: fix UP035 (deprecated import, use updated module) from typing import TypeVar import httpx @@ -13,12 +13,13 @@ from armis_sdk.core.armis_error import NotFoundError from armis_sdk.core.armis_error import ResponseError + DataTypeT = TypeVar("DataTypeT", dict, list) def get_data( response: httpx.Response, - data_type: Type[DataTypeT], + data_type: type[DataTypeT], ) -> DataTypeT: raise_for_status(response) data = parse_response(response, dict) @@ -36,7 +37,7 @@ def get_data_dict(response: httpx.Response): def parse_response( response: httpx.Response, - data_type: Type[DataTypeT], + data_type: type[DataTypeT], ) -> DataTypeT: try: response_data = response.json() diff --git a/armis_sdk/entities/asq_rule.py b/armis_sdk/entities/asq_rule.py index 8b2b5a0..51d83b3 100644 --- a/armis_sdk/entities/asq_rule.py +++ b/armis_sdk/entities/asq_rule.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Optional from typing import Union @@ -40,22 +42,25 @@ class AsqRule(BaseEntity): ``` """ - and_: Optional[list[Union[str, "AsqRule"]]] = Field(alias="and", default=None) + and_: list[str | AsqRule] | None = Field(alias="and", default=None) """Rules that all must match.""" - or_: Optional[list[Union[str, "AsqRule"]]] = Field(alias="or", default=None) + or_: list[str | AsqRule] | None = Field(alias="or", default=None) """Rules that at least one of them must match.""" @classmethod - def from_asq(cls, asq: str) -> "AsqRule": + def from_asq(cls, asq: str) -> AsqRule: """ Create a `AsqRule` object from a single ASQ string. """ return AsqRule(or_=[asq]) @model_validator(mode="after") - def validate_structure(self) -> "AsqRule": + def validate_structure(self) -> AsqRule: if (self.and_ is None) == (self.or_ is None): raise ValueError("Only one of 'and_' or 'or_' must be specified.") return self + + +AsqRule.model_rebuild() diff --git a/armis_sdk/entities/asset.py b/armis_sdk/entities/asset.py index 6442bc7..cc9dd70 100644 --- a/armis_sdk/entities/asset.py +++ b/armis_sdk/entities/asset.py @@ -1,15 +1,17 @@ import collections from typing import Any from typing import ClassVar -from typing import DefaultDict +from typing import DefaultDict # noqa: UP035 # TODO: fix UP035 (deprecated import, use updated module) from typing import Literal -from typing import Type +from typing import Type # noqa: UP035 # TODO: fix UP035 (deprecated import, use updated module) from typing import TypeVar from pydantic import Field +from typing_extensions import Self from armis_sdk.core.base_entity import BaseEntity + AssetT = TypeVar("AssetT", bound="Asset") @@ -26,8 +28,8 @@ class Asset(BaseEntity): """Integration properties of the asset. Values can by anything.""" @classmethod - def from_search_result(cls: Type[AssetT], data: dict) -> AssetT: - fields: DefaultDict[str, Any] = collections.defaultdict(dict) + def from_search_result(cls, data: dict) -> Self: + fields: collections.defaultdict[str, Any] = collections.defaultdict(dict) for key, value in data["fields"].items(): if len(parts := key.split(".", 1)) > 1: part1, part2 = parts @@ -44,4 +46,4 @@ def all_fields(cls) -> set[str]: return set(cls.model_fields.keys()) - { "custom", "integration", - } # pylint: disable=no-member + } diff --git a/armis_sdk/entities/data_export/application.py b/armis_sdk/entities/data_export/application.py index f9016b7..a014e9e 100644 --- a/armis_sdk/entities/data_export/application.py +++ b/armis_sdk/entities/data_export/application.py @@ -1,12 +1,17 @@ +from __future__ import annotations + import datetime from typing import ClassVar from typing import Optional - -import pandas +from typing import TYPE_CHECKING from armis_sdk.entities.data_export.base_exported_entity import BaseExportedEntity +if TYPE_CHECKING: + import pandas + + class Application(BaseExportedEntity): """ This class represents an application row that was exported using the data export API. @@ -38,7 +43,7 @@ class Application(BaseExportedEntity): **Example**: `30.0.1599.40` """ - cpe: Optional[str] + cpe: str | None """ The CPE (Common Platform Enumeration) of the application @@ -52,15 +57,13 @@ class Application(BaseExportedEntity): """When the application was last seen on the device""" @classmethod - def series_to_model(cls, series: pandas.Series) -> "Application": + def series_to_model(cls, series: pandas.Series) -> Application: return Application( device_id=series.loc["device_id"], vendor=series.loc["vendor"], name=series.loc["name"], version=series.loc["version"], - cpe=cls._value_or_none( - series.loc["cpe"] if "cpe" in series.index else None - ), + cpe=cls._value_or_none(series.loc["cpe"] if "cpe" in series.index else None), first_seen=series.loc["first_seen"].to_pydatetime(), last_seen=series["last_seen"].to_pydatetime(), ) diff --git a/armis_sdk/entities/data_export/base_exported_entity.py b/armis_sdk/entities/data_export/base_exported_entity.py index 13ed734..c9ab2ff 100644 --- a/armis_sdk/entities/data_export/base_exported_entity.py +++ b/armis_sdk/entities/data_export/base_exported_entity.py @@ -1,17 +1,18 @@ import abc -from typing import Type +from typing import Type # noqa: UP035 # TODO: fix UP035 (deprecated import, use updated module) from typing import TypeVar import pandas from pydantic import BaseModel + T = TypeVar("T", bound="BaseExportedEntity") class BaseExportedEntity(BaseModel, abc.ABC): @classmethod @abc.abstractmethod - def series_to_model(cls: Type[T], series: pandas.Series) -> T: ... + def series_to_model(cls: type[T], series: pandas.Series) -> T: ... @property @abc.abstractmethod @@ -23,7 +24,7 @@ def _to_list(cls, value) -> list: @classmethod def _value_or_none(cls, value): - if not value or pandas.isnull(value) or value == "N/A": + if not value or pandas.isnull(value) or value == "N/A": # noqa: PD003 # TODO: fix PD003 (use .isna() instead of .isnull()) return None if isinstance(value, pandas.Timestamp): diff --git a/armis_sdk/entities/data_export/data_export.py b/armis_sdk/entities/data_export/data_export.py index e46476d..19a60e1 100644 --- a/armis_sdk/entities/data_export/data_export.py +++ b/armis_sdk/entities/data_export/data_export.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import datetime from typing import Literal from typing import Optional @@ -24,5 +26,5 @@ class DataExport(BaseModel): urls: list[str] """URLs to the files that contain the exported data.""" - urls_creation_time: Optional[datetime.datetime] = Field(strict=False) + urls_creation_time: datetime.datetime | None = Field(strict=False) """The creation time of the URLs.""" diff --git a/armis_sdk/entities/data_export/risk_factor.py b/armis_sdk/entities/data_export/risk_factor.py index 4a86a27..eb1735c 100644 --- a/armis_sdk/entities/data_export/risk_factor.py +++ b/armis_sdk/entities/data_export/risk_factor.py @@ -1,14 +1,20 @@ +from __future__ import annotations + import datetime import json from typing import ClassVar from typing import Optional +from typing import TYPE_CHECKING -import pandas from pydantic import BaseModel from armis_sdk.entities.data_export.base_exported_entity import BaseExportedEntity +if TYPE_CHECKING: + import pandas + + class RiskFactorRecommendedAction(BaseModel): id: int """The id of the recommended action""" @@ -68,7 +74,7 @@ class RiskFactor(BaseExportedEntity): **Example**: `Device Supports SMBv1` """ - score: Optional[int] + score: int | None """The score of the risk factor""" group: str @@ -78,14 +84,14 @@ class RiskFactor(BaseExportedEntity): **Example**: `INSECURE_TRAFFIC_AND_BEHAVIOR` """ - remediation_type: Optional[str] + remediation_type: str | None """ The type of the remediation **Example**: `Disable SMBv1 Protocol` """ - remediation_description: Optional[str] + remediation_description: str | None """ The description of the remediation @@ -110,13 +116,13 @@ class RiskFactor(BaseExportedEntity): **Example**: `OPEN` """ - status_update_time: Optional[datetime.datetime] + status_update_time: datetime.datetime | None """When was the status last changed""" - status_updated_by_user_id: Optional[int] + status_updated_by_user_id: int | None """Which used id last changed the status""" - status_update_reason: Optional[str] + status_update_reason: str | None """ The reason for the status change @@ -124,29 +130,21 @@ class RiskFactor(BaseExportedEntity): """ @classmethod - def series_to_model(cls, series: pandas.Series) -> "RiskFactor": + def series_to_model(cls, series: pandas.Series) -> RiskFactor: return RiskFactor( device_id=series.loc["device_id"], category=series.loc["category"], type=series.loc["type"], description=series.loc["description"], - score=( - int(score) - if (score := cls._value_or_none(series.loc["score"])) - else None - ), + score=(int(score) if (score := cls._value_or_none(series.loc["score"])) else None), status=series.loc["status"], group=series.loc["group"], remediation_type=cls._value_or_none(series.loc["remediation"]), - remediation_description=cls._value_or_none( - series.loc["remediation_description"] - ), + remediation_description=cls._value_or_none(series.loc["remediation_description"]), remediation_recommended_actions=( [ RiskFactorRecommendedAction(**item) - for item in json.loads( - series.loc["remediation_recommended_actions"] - ) + for item in json.loads(series.loc["remediation_recommended_actions"]) ] if series.loc["remediation_recommended_actions"] else [] @@ -154,8 +152,6 @@ def series_to_model(cls, series: pandas.Series) -> "RiskFactor": first_seen=series.loc["first_seen"].to_pydatetime(), last_seen=series.loc["last_seen"].to_pydatetime(), status_update_time=cls._value_or_none(series.loc["status_update_time"]), - status_updated_by_user_id=cls._value_or_none( - series.loc["status_updated_by_user_id"] - ), + status_updated_by_user_id=cls._value_or_none(series.loc["status_updated_by_user_id"]), status_update_reason=cls._value_or_none(series.loc["status_update_reason"]), ) diff --git a/armis_sdk/entities/data_export/vulnerability.py b/armis_sdk/entities/data_export/vulnerability.py index 2327c23..f7b6631 100644 --- a/armis_sdk/entities/data_export/vulnerability.py +++ b/armis_sdk/entities/data_export/vulnerability.py @@ -1,12 +1,17 @@ +from __future__ import annotations + import datetime from typing import ClassVar from typing import Optional - -import pandas +from typing import TYPE_CHECKING from armis_sdk.entities.data_export.base_exported_entity import BaseExportedEntity +if TYPE_CHECKING: + import pandas + + class Vulnerability(BaseExportedEntity): """ This class represents a vulnerability row that was exported using the data export API. @@ -24,7 +29,7 @@ class Vulnerability(BaseExportedEntity): **Example**: `CVE-2025-53799` """ - advisory_id: Optional[str] + advisory_id: str | None """ The id of the advisory @@ -38,7 +43,7 @@ class Vulnerability(BaseExportedEntity): **Example**: `["VERSION_UPDATE"]` """ - avm_rating: Optional[str] + avm_rating: str | None """The Armis AVM (Asset Vulnerability Management) rating of the vulnerability""" match_source: list[str] @@ -55,21 +60,19 @@ class Vulnerability(BaseExportedEntity): **Example**: `Open` """ - status_change_time: Optional[datetime.datetime] + status_change_time: datetime.datetime | None """When was the status last changed""" - status_change_reason: Optional[str] + status_change_reason: str | None """The reason for the status change""" @classmethod - def series_to_model(cls, series: pandas.Series) -> "Vulnerability": + def series_to_model(cls, series: pandas.Series) -> Vulnerability: return Vulnerability( device_id=series.loc["device_id"], cve_uid=series.loc["vulnerability_cve_uid"], advisory_id=cls._value_or_none(series.loc["vulnerability_advisory_id"]), - remediation_types=cls._to_list( - series.loc["vulnerability_remediation_types"] - ), + remediation_types=cls._to_list(series.loc["vulnerability_remediation_types"]), avm_rating=cls._value_or_none(series.loc["avm_rating"]), match_source=cls._to_list(series.loc["match_source"]), status=series.loc["status"], diff --git a/armis_sdk/entities/device.py b/armis_sdk/entities/device.py index 9fba2ad..2c169cd 100644 --- a/armis_sdk/entities/device.py +++ b/armis_sdk/entities/device.py @@ -1,7 +1,8 @@ -import datetime +from __future__ import annotations + +import datetime # noqa: TC003 from typing import ClassVar from typing import Literal -from typing import Optional from pydantic import Field @@ -12,107 +13,109 @@ class Device(Asset): - # pylint: disable=line-too-long asset_type: ClassVar[Literal["DEVICE"]] = "DEVICE" - boundaries: Optional[list[Boundary]] = None + boundaries: list[Boundary] | None = None """The list of boundaries the device belongs to.""" - brand: Optional[str] = None + brand: str | None = None """ The device brand. - + Example: `Apple` """ - category: Optional[str] = None + category: str | None = None """ The device category. - + Example: `Handheld` """ - device_id: Optional[int] = None + device_id: int | None = None """The unique identifier given to the device by thr Armis engine.""" - display: Optional[str] = None + display: str | None = None """ The display text of the device. - + Example: `My iPhone` """ - first_seen: Optional[datetime.datetime] = Field(strict=False, default=None) + first_seen: datetime.datetime | None = Field(strict=False, default=None) """When was the device first seen.""" - ipv4_addresses: Optional[list[str]] = None + ipv4_addresses: list[str] | None = None """The list of IPv4 addresses of the device""" - ipv6_addresses: Optional[list[str]] = None + ipv6_addresses: list[str] | None = None """The list of IPv6 addresses of the device""" - last_seen: Optional[datetime.datetime] = Field(strict=False, default=None) + last_seen: datetime.datetime | None = Field(strict=False, default=None) """When was the device last seen.""" - mac_addresses: Optional[list[str]] = None + mac_addresses: list[str] | None = None """The list of MAC addresses of the device""" - model: Optional[str] = None + model: str | None = None """ The model of the device. - + Example: `iPhone 17` """ - names: Optional[list[str]] = None + names: list[str] | None = None """ List of names of the device - + Example: `["My iPhone 17", "Jane's iPhone"]` """ - network_interfaces: Optional[list[NetworkInterface]] = None + network_interfaces: list[NetworkInterface] | None = None """List of network interfaces detected on the device.""" - os_name: Optional[str] = None + os_name: str | None = None """ The OS name running on the device. - + Example: `iOS` """ - os_version: Optional[str] = None + os_version: str | None = None """ The OS version running on the device. Example: `17` """ - purdue_level: Optional[float] = None + purdue_level: float | None = None """ The purdue level of the devices. See [Wikipedia](https://en.wikipedia.org/wiki/Purdue_Enterprise_Reference_Architecture) article for more details. - + Example: `4` """ - risk_level: Optional[int] = Field(ge=0, le=1000, default=None) + risk_level: int | None = Field(ge=0, le=1000, default=None) """The risk level given to the device by the Armis engine, between `0` and `100`.""" - serial_numbers: Optional[list[str]] = None + serial_numbers: list[str] | None = None """The list of serial numbers of the device""" - site: Optional[Site] = None + site: Site | None = None """The site in which this device was last seen.""" - tags: Optional[list[str]] = None + tags: list[str] | None = None """The tags given to the devices.""" - type: Optional[str] = None + type: str | None = None """ The type of the device. - + Example: `Mobile Phones` """ - visibility: Optional[Literal["Full", "Limited"]] = None + visibility: Literal["Full", "Limited"] | None = None """Whether the device is fully visibly or limited.""" + + +Device.model_rebuild() diff --git a/armis_sdk/entities/device_custom_property.py b/armis_sdk/entities/device_custom_property.py index dec7f24..247a0e2 100644 --- a/armis_sdk/entities/device_custom_property.py +++ b/armis_sdk/entities/device_custom_property.py @@ -1,14 +1,20 @@ -import datetime +from __future__ import annotations + from typing import Literal from typing import Optional +from typing import TYPE_CHECKING from pydantic import Field from armis_sdk.core.base_entity import BaseEntity +if TYPE_CHECKING: + import datetime + + class DeviceCustomProperty(BaseEntity): - id: Optional[int] = None + id: int | None = None """The id of the property.""" name: str = Field(max_length=40, pattern=r"^[\w_]*$") @@ -18,7 +24,7 @@ class DeviceCustomProperty(BaseEntity): Example: `Size` """ - description: Optional[str] = Field(max_length=250, default=None) + description: str | None = Field(max_length=250, default=None) """ The description of the property. @@ -39,15 +45,15 @@ class DeviceCustomProperty(BaseEntity): Example: `enum` """ - allowed_values: Optional[list[str]] = None + allowed_values: list[str] | None = None """ The allowed values of the property when the 'type' is 'enum'. Example: `["s", "m", "l"]` """ - created_by: Optional[str] = Field(max_length=50, default=None) + created_by: str | None = Field(max_length=50, default=None) """Who / what created the property.""" - creation_time: Optional[datetime.datetime] = Field(strict=False, default=None) + creation_time: datetime.datetime | None = Field(strict=False, default=None) """The creation time of the property.""" diff --git a/armis_sdk/entities/download_progress.py b/armis_sdk/entities/download_progress.py index 88ba17a..41ca30a 100644 --- a/armis_sdk/entities/download_progress.py +++ b/armis_sdk/entities/download_progress.py @@ -11,4 +11,4 @@ class DownloadProgress(BaseEntity): @property def percent(self) -> str: """Percentage of progress.""" - return f"{self.downloaded/self.total:.4%}" + return f"{self.downloaded / self.total:.4%}" diff --git a/armis_sdk/entities/network_interface.py b/armis_sdk/entities/network_interface.py index 0f3086b..71da22c 100644 --- a/armis_sdk/entities/network_interface.py +++ b/armis_sdk/entities/network_interface.py @@ -1,49 +1,50 @@ +from __future__ import annotations + from typing import Optional from armis_sdk.core.base_entity import BaseEntity class NetworkInterface(BaseEntity): - # pylint: disable=line-too-long """ A `NetworkInterface` represents a physical network card of a [Device][armis_sdk.entities.device.Device]. """ - alias: Optional[str] + alias: str | None """The alias of the interface.""" - brand: Optional[str] + brand: str | None """The brand of the interface.""" - broadcast_ssid: Optional[str] + broadcast_ssid: str | None """The last SSID broadcasted by the interface.""" channels: list[int] """The channels that the interface uses to transmit.""" - description: Optional[str] + description: str | None """The description of the interface""" - hidden_broadcast_ssid: Optional[bool] + hidden_broadcast_ssid: bool | None """Is the broadcasted SSID hidden.""" - ipv4_address: Optional[str] + ipv4_address: str | None """The last IPv4 address associated with the interface.""" - ipv6_address: Optional[str] + ipv6_address: str | None """The last IPv6 address associated with the interface.""" - last_connected_ssid: Optional[str] + last_connected_ssid: str | None """The SSID the interface last connected to.""" - mac_address: Optional[str] + mac_address: str | None """The MAC address of the interface.""" - name: Optional[str] + name: str | None """The name of the interface.""" - type: Optional[str] + type: str | None """The type of the interface.""" - vlan: Optional[int] + vlan: int | None """The VLAN of the interface.""" diff --git a/armis_sdk/entities/site.py b/armis_sdk/entities/site.py index 9815d4a..a2985b3 100644 --- a/armis_sdk/entities/site.py +++ b/armis_sdk/entities/site.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json from typing import Annotated from typing import Any @@ -23,64 +25,60 @@ class Site(BaseEntity): The `Site` entity represents a physical location at the customer's environment. """ - id: Optional[int] = Field(strict=False, default=None) + id: int | None = Field(strict=False, default=None) """The id of the site.""" - name: Optional[str] = None + name: str | None = None """The name of the site.""" - lat: Optional[float] = Field(frozen=True, default=None) + lat: float | None = Field(frozen=True, default=None) """ The latitude coordinate of the physical location of the site on earth. - This field is read-only and is automatically derived from the + This field is read-only and is automatically derived from the [`location`][armis_sdk.entities.site.Site.location] field. - + Example: `37.7900103` """ - lng: Optional[float] = Field(frozen=True, default=None) + lng: float | None = Field(frozen=True, default=None) """ The longitude coordinate of the physical location of the site on earth. - This field is read-only and is automatically derived from the + This field is read-only and is automatically derived from the [`location`][armis_sdk.entities.site.Site.location] field. - + Example: `-122.4007818` """ - location: Optional[str] = None + location: str | None = None """ The name of the location of the site, such as an address. - + Example: `548 Market Street Suite 97439 San Francisco, CA 94104-5401` - - When this field is set, the [`lat`][armis_sdk.entities.site.Site.lat] and + + When this field is set, the [`lat`][armis_sdk.entities.site.Site.lat] and [`lng`][armis_sdk.entities.site.Site.lng] are automatically derived from it. """ - parent_id: Optional[int] = Field(strict=False, default=None) + parent_id: int | None = Field(strict=False, default=None) """The id of the parent site.""" - tier: Optional[str] = None + tier: str | None = None """The tier of the site.""" - asq_rule: Optional[AsqRule] = Field(default=None) + asq_rule: AsqRule | None = Field(default=None) """The ASQ rule of the site.""" - network_equipment_device_ids: Annotated[ - Optional[list[int]], BeforeValidator(ensure_list_of_ints) - ] = None + network_equipment_device_ids: Annotated[list[int] | None, BeforeValidator(ensure_list_of_ints)] = None """The ids of network equipment devices associated with the site.""" - integration_ids: Annotated[ - Optional[list[int]], BeforeValidator(ensure_list_of_ints) - ] = None + integration_ids: Annotated[list[int] | None, BeforeValidator(ensure_list_of_ints)] = None """The ids of the integration associated with the site.""" - children: list["Site"] = Field(default_factory=list) - """The sub-sites that are directly under this site + children: list[Site] = Field(default_factory=list) + """The sub-sites that are directly under this site (their `parent_id` will match this site's `id`).""" @model_validator(mode="before") @@ -91,3 +89,6 @@ def transform_rule_aql_to_asq_rule(cls, data: Any) -> dict: if "ruleAql" in data: data["asq_rule"] = json.loads(data.pop("ruleAql")) return data + + +Site.model_rebuild() diff --git a/armis_sdk/types/asset_id_source.py b/armis_sdk/types/asset_id_source.py index 3ae0ebd..8902c3d 100644 --- a/armis_sdk/types/asset_id_source.py +++ b/armis_sdk/types/asset_id_source.py @@ -1,5 +1,6 @@ from typing import Literal + AssetIdSource = Literal[ "ASSET_ID", "IPV4_ADDRESS", diff --git a/armis_sdk/types/collector_image_type.py b/armis_sdk/types/collector_image_type.py index eaf844f..3162baa 100644 --- a/armis_sdk/types/collector_image_type.py +++ b/armis_sdk/types/collector_image_type.py @@ -1,5 +1,6 @@ from typing import Literal + CollectorImageType = Literal[ "DARWIN_AMD64_BROKER", "DARWIN_ARM64_BROKER", diff --git a/poetry.lock b/poetry.lock index 06fb033..723dab5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. [[package]] name = "annotated-types" @@ -7,7 +7,6 @@ description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -20,7 +19,6 @@ description = "High level compatibility layer for multiple asynchronous event lo optional = false python-versions = ">=3.9" groups = ["main", "build"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, @@ -34,25 +32,9 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] trio = ["trio (>=0.26.1)"] -[[package]] -name = "astroid" -version = "3.3.11" -description = "An abstract syntax tree for Python with inference support." -optional = false -python-versions = ">=3.9.0" -groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" -files = [ - {file = "astroid-3.3.11-py3-none-any.whl", hash = "sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec"}, - {file = "astroid-3.3.11.tar.gz", hash = "sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} - [[package]] name = "babel" version = "2.17.0" @@ -60,14 +42,13 @@ description = "Internationalization utilities" optional = false python-versions = ">=3.8" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, ] [package.extras] -dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] +dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] [[package]] name = "backrefs" @@ -76,7 +57,6 @@ description = "A wrapper around re and regex that adds additional back reference optional = false python-versions = ">=3.9" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d"}, {file = "backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b"}, @@ -89,54 +69,6 @@ files = [ [package.extras] extras = ["regex"] -[[package]] -name = "black" -version = "25.1.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" -files = [ - {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, - {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, - {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, - {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, - {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, - {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, - {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, - {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, - {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, - {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, - {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, - {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, - {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, - {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, - {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, - {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, - {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"}, - {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"}, - {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"}, - {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"}, - {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, - {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.10)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "certifi" version = "2025.4.26" @@ -144,7 +76,6 @@ description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" groups = ["main", "build", "docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, @@ -157,7 +88,6 @@ description = "The Real First Universal Charset Detector. Open, modern and activ optional = false python-versions = ">=3.7" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, @@ -259,8 +189,7 @@ version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" -groups = ["dev", "docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +groups = ["docs"] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -275,29 +204,12 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["build", "dev", "docs"] +groups = ["build", "docs"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {build = "(python_version <= \"3.11\" or python_version >= \"3.12\") and sys_platform == \"win32\"", dev = "(platform_system == \"Windows\" or sys_platform == \"win32\") and (python_version <= \"3.11\" or python_version >= \"3.12\")", docs = "python_version <= \"3.11\" or python_version >= \"3.12\""} - -[[package]] -name = "dill" -version = "0.4.0" -description = "serialize all of Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" -files = [ - {file = "dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"}, - {file = "dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0"}, -] - -[package.extras] -graph = ["objgraph (>=1.7.2)"] -profile = ["gprof2dot (>=2022.7.29)"] +markers = {build = "sys_platform == \"win32\""} [[package]] name = "exceptiongroup" @@ -325,7 +237,6 @@ description = "Copy your docs directly to the gh-pages branch." optional = false python-versions = "*" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, @@ -344,7 +255,6 @@ description = "Signatures for entire Python programs. Extract the structure, the optional = false python-versions = ">=3.9" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "griffe-1.7.3-py3-none-any.whl", hash = "sha256:c6b3ee30c2f0f17f30bcdef5068d6ab7a2a4f1b8bf1a3e74b56fffd21e1c5f75"}, {file = "griffe-1.7.3.tar.gz", hash = "sha256:52ee893c6a3a968b639ace8015bec9d36594961e156e23315c8e8e51401fa50b"}, @@ -360,7 +270,6 @@ description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" groups = ["main", "build"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, @@ -373,7 +282,6 @@ description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" groups = ["main", "build"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, @@ -396,7 +304,6 @@ description = "The next generation HTTP client." optional = false python-versions = ">=3.8" groups = ["main", "build"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -409,7 +316,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -422,7 +329,6 @@ description = "A retry layer for HTTPX." optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "httpx_retries-0.4.0-py3-none-any.whl", hash = "sha256:61df3e70889e2f74ebcaf1ae4213f1bdb67dc68dfa8b2e9da41a63afd513e3fb"}, {file = "httpx_retries-0.4.0.tar.gz", hash = "sha256:a7aa513e3f1eef347aac69adecfad9f421210a84df88c88f935ec130f98e6642"}, @@ -438,7 +344,6 @@ description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" groups = ["main", "build", "docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -453,8 +358,8 @@ version = "8.7.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.9" -groups = ["dev", "docs"] -markers = "python_version < \"3.10\"" +groups = ["docs"] +markers = "python_version == \"3.9\"" files = [ {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, @@ -464,12 +369,12 @@ files = [ zipp = ">=3.20" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib_resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] @@ -479,32 +384,11 @@ description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" groups = ["build"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] -[[package]] -name = "isort" -version = "6.1.0" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.9.0" -groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" -files = [ - {file = "isort-6.1.0-py3-none-any.whl", hash = "sha256:58d8927ecce74e5087aef019f778d4081a3b6c98f15a80ba35782ca8a2097784"}, - {file = "isort-6.1.0.tar.gz", hash = "sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481"}, -] - -[package.dependencies] -importlib-metadata = {version = ">=4.6.0", markers = "python_version < \"3.10\""} - -[package.extras] -colors = ["colorama"] -plugins = ["setuptools"] - [[package]] name = "jinja2" version = "3.1.6" @@ -512,7 +396,6 @@ description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, @@ -531,7 +414,6 @@ description = "Python implementation of John Gruber's Markdown." optional = false python-versions = ">=3.9" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc"}, {file = "markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f"}, @@ -551,7 +433,6 @@ description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -616,19 +497,6 @@ files = [ {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - [[package]] name = "mergedeep" version = "1.3.4" @@ -636,7 +504,6 @@ description = "A deep merge function for 🐍." optional = false python-versions = ">=3.6" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, @@ -649,7 +516,6 @@ description = "Project documentation with Markdown." optional = false python-versions = ">=3.8" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, @@ -673,7 +539,7 @@ watchdog = ">=2.0" [package.extras] i18n = ["babel (>=2.9.0)"] -min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] [[package]] name = "mkdocs-autorefs" @@ -682,7 +548,6 @@ description = "Automatically link across pages in MkDocs." optional = false python-versions = ">=3.9" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mkdocs_autorefs-1.4.2-py3-none-any.whl", hash = "sha256:83d6d777b66ec3c372a1aad4ae0cf77c243ba5bcda5bf0c6b8a2c5e7a3d89f13"}, {file = "mkdocs_autorefs-1.4.2.tar.gz", hash = "sha256:e2ebe1abd2b67d597ed19378c0fff84d73d1dbce411fce7a7cc6f161888b6749"}, @@ -700,7 +565,6 @@ description = "MkDocs extension that lists all dependencies according to a mkdoc optional = false python-versions = ">=3.8" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, @@ -719,7 +583,6 @@ description = "Documentation that simply works" optional = false python-versions = ">=3.8" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mkdocs_material-9.6.14-py3-none-any.whl", hash = "sha256:3b9cee6d3688551bf7a8e8f41afda97a3c39a12f0325436d76c86706114b721b"}, {file = "mkdocs_material-9.6.14.tar.gz", hash = "sha256:39d795e90dce6b531387c255bd07e866e027828b7346d3eba5ac3de265053754"}, @@ -750,7 +613,6 @@ description = "Extension pack for Python Markdown and MkDocs Material." optional = false python-versions = ">=3.8" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, @@ -763,7 +625,6 @@ description = "Automatic documentation from sources, for MkDocs." optional = false python-versions = ">=3.9" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mkdocstrings-0.29.1-py3-none-any.whl", hash = "sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6"}, {file = "mkdocstrings-0.29.1.tar.gz", hash = "sha256:8722f8f8c5cd75da56671e0a0c1bbed1df9946c0cef74794d6141b34011abd42"}, @@ -790,7 +651,6 @@ description = "A Python handler for mkdocstrings." optional = false python-versions = ">=3.9" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mkdocstrings_python-1.16.11-py3-none-any.whl", hash = "sha256:25d96cc9c1f9c272ea1bd8222c900b5f852bf46c984003e9c7c56eaa4696190f"}, {file = "mkdocstrings_python-1.16.11.tar.gz", hash = "sha256:935f95efa887f99178e4a7becaaa1286fb35adafffd669b04fd611d97c00e5ce"}, @@ -809,7 +669,6 @@ description = "Optional static typing for Python" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mypy-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c"}, {file = "mypy-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571"}, @@ -865,7 +724,6 @@ description = "Type system extensions for programs checked with the mypy type ch optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, @@ -878,7 +736,6 @@ description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" groups = ["main", "dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, @@ -933,8 +790,7 @@ version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["build", "dev", "docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +groups = ["build", "docs"] files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, @@ -947,7 +803,6 @@ description = "Divides large result sets into pages for easier browsing" optional = false python-versions = "*" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, @@ -964,7 +819,6 @@ description = "Powerful data structures for data analysis, time series, and stat optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c"}, {file = "pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a"}, @@ -1026,8 +880,8 @@ files = [ [package.dependencies] numpy = [ {version = ">=1.22.4", markers = "python_version < \"3.11\""}, - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" @@ -1065,7 +919,6 @@ description = "Type annotations for pandas" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pandas_stubs-2.2.2.240807-py3-none-any.whl", hash = "sha256:893919ad82be4275f0d07bb47a95d08bae580d3fdea308a7acfcb3f02e76186e"}, {file = "pandas_stubs-2.2.2.240807.tar.gz", hash = "sha256:64a559725a57a449f46225fbafc422520b7410bff9252b661a225b5559192a93"}, @@ -1082,7 +935,6 @@ description = "Utility library for gitignore style pattern matching of file path optional = false python-versions = ">=3.8" groups = ["dev", "docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -1094,8 +946,7 @@ version = "4.3.8" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.9" -groups = ["dev", "docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +groups = ["docs"] files = [ {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, @@ -1113,7 +964,6 @@ description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" groups = ["build"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, @@ -1130,7 +980,6 @@ description = "Python library for Apache Arrow" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pyarrow-21.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e563271e2c5ff4d4a4cbeb2c83d5cf0d4938b891518e676025f7268c6fe5fe26"}, {file = "pyarrow-21.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:fee33b0ca46f4c85443d6c450357101e47d53e6c3f008d658c27a2d020d44c79"}, @@ -1187,7 +1036,6 @@ description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7"}, {file = "pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a"}, @@ -1201,7 +1049,7 @@ typing-inspection = ">=0.4.0" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" @@ -1210,7 +1058,6 @@ description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, @@ -1323,7 +1170,6 @@ description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" groups = ["build", "docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, @@ -1332,38 +1178,6 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] -[[package]] -name = "pylint" -version = "3.3.7" -description = "python code static checker" -optional = false -python-versions = ">=3.9.0" -groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" -files = [ - {file = "pylint-3.3.7-py3-none-any.whl", hash = "sha256:43860aafefce92fca4cf6b61fe199cdc5ae54ea28f9bf4cd49de267b5195803d"}, - {file = "pylint-3.3.7.tar.gz", hash = "sha256:2b11de8bde49f9c5059452e0c310c079c746a0a8eeaa789e5aa966ecc23e4559"}, -] - -[package.dependencies] -astroid = ">=3.3.8,<=3.4.0.dev0" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -dill = [ - {version = ">=0.2", markers = "python_version < \"3.11\""}, - {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, - {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, -] -isort = ">=4.2.5,<5.13 || >5.13,<7" -mccabe = ">=0.6,<0.8" -platformdirs = ">=2.2" -tomli = {version = ">=1.1", markers = "python_version < \"3.11\""} -tomlkit = ">=0.10.1" -typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} - -[package.extras] -spelling = ["pyenchant (>=3.2,<4.0)"] -testutils = ["gitpython (>3)"] - [[package]] name = "pymdown-extensions" version = "10.15" @@ -1371,7 +1185,6 @@ description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pymdown_extensions-10.15-py3-none-any.whl", hash = "sha256:46e99bb272612b0de3b7e7caf6da8dd5f4ca5212c0b273feb9304e236c484e5f"}, {file = "pymdown_extensions-10.15.tar.gz", hash = "sha256:0e5994e32155f4b03504f939e501b981d306daf7ec2aa1cd2eb6bd300784f8f7"}, @@ -1391,7 +1204,6 @@ description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.9" groups = ["build"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e"}, {file = "pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6"}, @@ -1416,7 +1228,6 @@ description = "Pytest support for asyncio" optional = false python-versions = ">=3.9" groups = ["build"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3"}, {file = "pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f"}, @@ -1437,7 +1248,6 @@ description = "Send responses to httpx." optional = false python-versions = ">=3.9" groups = ["build"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytest_httpx-0.35.0-py3-none-any.whl", hash = "sha256:ee11a00ffcea94a5cbff47af2114d34c5b231c326902458deed73f9c459fd744"}, {file = "pytest_httpx-0.35.0.tar.gz", hash = "sha256:d619ad5d2e67734abfbb224c3d9025d64795d4b8711116b1a13f72a251ae511f"}, @@ -1457,7 +1267,6 @@ description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main", "docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -1473,7 +1282,6 @@ description = "Read key-value pairs from a .env file and set them as environment optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, @@ -1489,7 +1297,6 @@ description = "World timezone definitions, modern and historical" optional = false python-versions = "*" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, @@ -1502,7 +1309,6 @@ description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -1566,7 +1372,6 @@ description = "A custom YAML tag for referencing environment variables in YAML f optional = false python-versions = ">=3.9" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04"}, {file = "pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff"}, @@ -1582,7 +1387,6 @@ description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -1598,6 +1402,34 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "ruff" +version = "0.15.6" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff"}, + {file = "ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3"}, + {file = "ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb"}, + {file = "ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8"}, + {file = "ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e"}, + {file = "ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15"}, + {file = "ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9"}, + {file = "ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab"}, + {file = "ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e"}, + {file = "ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c"}, + {file = "ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512"}, + {file = "ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0"}, + {file = "ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb"}, + {file = "ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0"}, + {file = "ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c"}, + {file = "ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406"}, + {file = "ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837"}, + {file = "ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4"}, +] + [[package]] name = "setuptools" version = "80.9.0" @@ -1605,20 +1437,19 @@ description = "Easily download, build, install, upgrade, and uninstall Python pa optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] -core = ["importlib_metadata (>=6)", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "six" @@ -1627,7 +1458,6 @@ description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main", "docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -1640,7 +1470,6 @@ description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" groups = ["main", "build"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -1689,19 +1518,6 @@ files = [ {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] -[[package]] -name = "tomlkit" -version = "0.13.3" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" -files = [ - {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, - {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, -] - [[package]] name = "types-pytz" version = "2025.2.0.20250809" @@ -1709,7 +1525,6 @@ description = "Typing stubs for pytz" optional = false python-versions = ">=3.9" groups = ["dev"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "types_pytz-2025.2.0.20250809-py3-none-any.whl", hash = "sha256:4f55ed1b43e925cf851a756fe1707e0f5deeb1976e15bf844bcaa025e8fbd0db"}, {file = "types_pytz-2025.2.0.20250809.tar.gz", hash = "sha256:222e32e6a29bb28871f8834e8785e3801f2dc4441c715cd2082b271eecbe21e5"}, @@ -1726,7 +1541,7 @@ files = [ {file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"}, {file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"}, ] -markers = {main = "python_version <= \"3.11\" or python_version >= \"3.12\"", build = "python_version >= \"3.12\" and python_version < \"3.13\" or python_version <= \"3.11\"", dev = "python_version <= \"3.11\" or python_version >= \"3.12\"", docs = "python_version < \"3.11\""} +markers = {build = "python_version < \"3.13\"", docs = "python_version < \"3.11\""} [[package]] name = "typing-inspection" @@ -1735,7 +1550,6 @@ description = "Runtime typing introspection tools" optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, @@ -1751,7 +1565,6 @@ description = "Provider of IANA time zone data" optional = false python-versions = ">=2" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, @@ -1764,7 +1577,6 @@ description = "A library to help automate the creation of universal python libra optional = false python-versions = ">=3.9" groups = ["main"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "universalasync-0.4.0.1-py3-none-any.whl", hash = "sha256:55691b23f8265ff9a658d5eaee5a0375d75ac46432862198178c9a360062ba52"}, {file = "universalasync-0.4.0.1.tar.gz", hash = "sha256:aadf1f20170366b76fb0317a343775c403842c8db25062f7d68184a09b3e882c"}, @@ -1777,14 +1589,13 @@ description = "HTTP library with thread-safe connection pooling, file post, and optional = false python-versions = ">=3.9" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -1796,7 +1607,6 @@ description = "Filesystem events monitoring" optional = false python-versions = ">=3.9" groups = ["docs"] -markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, @@ -1839,22 +1649,22 @@ version = "3.22.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.9" -groups = ["dev", "docs"] -markers = "python_version < \"3.10\"" +groups = ["docs"] +markers = "python_version == \"3.9\"" files = [ {file = "zipp-3.22.0-py3-none-any.whl", hash = "sha256:fe208f65f2aca48b81f9e6fd8cf7b8b32c26375266b009b413d45306b6148343"}, {file = "zipp-3.22.0.tar.gz", hash = "sha256:dd2f28c3ce4bc67507bfd3781d21b7bb2be31103b51a4553ad7d90b84e57ace5"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib_resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +test = ["big-O", "importlib_resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9" -content-hash = "606b6d2564790886f04f7795e7afb893dbd26079c46883561783e9db800b427f" +content-hash = "e5f5e58ecd17b60e3028d811b8188bd87bb8020976980e9d006081e33206e9cd" diff --git a/pylintrc b/pylintrc deleted file mode 100644 index b6cfa48..0000000 --- a/pylintrc +++ /dev/null @@ -1,11 +0,0 @@ -[MAIN] - -ignore-paths= - local, - .venv - -disable= - duplicate-code, - missing-class-docstring, - missing-module-docstring, - missing-function-docstring, diff --git a/pyproject.toml b/pyproject.toml index a450881..123fef2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,10 +13,6 @@ requires-python = ">=3.9" requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" -[tool.isort] -profile = "black" -force_single_line = "true" - [tool.mypy] plugins = ['pydantic.mypy'] @@ -34,12 +30,10 @@ pytest-asyncio = "*" pytest-httpx = "*" [tool.poetry.group.dev.dependencies] -black = "*" -isort = "*" mypy = "*" pandas-stubs = "*" -pylint = "*" python-dotenv = "*" +ruff = "*" setuptools = "*" [tool.poetry.group.docs.dependencies] diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..2ba50b1 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,347 @@ +line-length = 120 +target-version = "py39" + +[lint] + +select = [ + "F", # Pyflakes + "E", # pycodestyle errors + "W", # pycodestyle warnings + "C90", # mccabe complexity + "I", # isort + "N", # pep8-naming + "D", # pydocstyle + "UP", # pyupgrade + "YTT", # flake8-2020 + "ANN", # flake8-annotations + "ASYNC", # flake8-async + "S", # flake8-bandit + "BLE", # flake8-blind-except + "FBT", # flake8-boolean-trap + "B", # flake8-bugbear + "A", # flake8-builtins + "COM", # flake8-commas + "C4", # flake8-comprehensions + "DTZ", # flake8-datetimez + "T10", # flake8-debugger + "EM", # flake8-errmsg + "EXE", # flake8-executable + "FA", # flake8-future-annotations + "ISC", # flake8-implicit-str-concat + "ICN", # flake8-import-conventions + "LOG", # flake8-logging + "G", # flake8-logging-format + "INP", # flake8-no-pep420 + "PIE", # flake8-pie + "T20", # flake8-print + "PYI", # flake8-pyi + "PT", # flake8-pytest-style + "Q", # flake8-quotes + "RSE", # flake8-raise + "RET", # flake8-return + "SLF", # flake8-self + "SLOT", # flake8-slots + "SIM", # flake8-simplify + "TID", # flake8-tidy-imports + "TCH", # flake8-type-checking + "ARG", # flake8-unused-arguments + "PTH", # flake8-use-pathlib + "TD", # flake8-todos + "FIX", # flake8-fixme + "ERA", # eradicate (commented-out code) + "PD", # pandas-vet + "PGH", # pygrep-hooks + "PL", # Pylint (PLC, PLE, PLR, PLW) + "TRY", # tryceratops + "FLY", # flynt + "NPY", # NumPy-specific rules + "PERF", # Perflint + "FURB", # refurb + "RUF", # Ruff-specific rules +] + +ignore = [ + "D", # Disable all docstring rules + "ANN001", # Missing type annotation for function argument + "ANN002", # Missing type annotation for *args + "ANN003", # Missing type annotation for **kwargs + "ANN201", # Missing return type annotation for public function + "ANN202", # Missing return type annotation for private function + "ANN204", # Missing return type annotation for special method + "ANN205", # Missing return type annotation for staticmethod + "ANN206", # Missing return type annotation for classmethod + "ANN401", # Dynamically typed expressions (Any) are disallowed + "TD002", # Missing author in TODO + "TD003", # Missing issue link on the line following TODO + "TD004", # Missing colon in TODO + "TD005", # Missing issue description after TODO + "FIX001", # Line contains FIXME + "FIX002", # Line contains TODO + "FIX003", # Line contains XXX + "FIX004", # Line contains HACK + + "E711", # none-comparison + "B904", # Raise without from + "SIM108", # Use ternary operator + "TRY003", # Avoid specifying long messages outside exception class + "EM101", # Exception must not use a string literal + "EM102", # Exception must not use an f-string literal + + "FBT001", # Boolean positional arg in function definition + "FBT002", # Boolean default value in function definition + "FBT003", # Boolean positional value in function call + + "PLR0904", # Too many public methods + "PLR0911", # Too many return statements + "PLR0912", # Too many branches + "PLR0913", # Too many arguments + "PLR0914", # Too many local variables + "PLR0915", # Too many statements + "PLR0916", # Too many boolean expressions + "PLR0917", # Too many positional arguments + "PLR2004", # Magic value used in comparison + + "BLE001", # Do not catch blind exception: Exception + "TRY002", # Create your own exception + "TRY300", # try-consider-else + "TRY400", # Use logging.exception instead of logging.error + "TRY201", # verbose-raise + + "S101", # Use of assert detected + "S104", # Possible binding to all interfaces + "S105", # Possible hardcoded password + "S106", # Possible hardcoded password assigned to argument + "S107", # Possible hardcoded password assigned to argument + "S108", # Probable insecure usage of temp file/directory + "S311", # Standard pseudo-random generators + "S324", # Probable use of insecure hash function + "S603", # subprocess call + "S607", # Starting a process with a partial executable path + "S608", # hardcoded-sql-expression + "S113", # request-without-timeout + "S501", # request-with-no-cert-validation + "S110", # try-except-pass + "S112", # try-except-continue + + "N", # Disable all pep8-naming rules + + "RET504", # Unnecessary variable assignment before return + "RET505", # Unnecessary else after return + "RET506", # Unnecessary else after raise + "RET507", # Unnecessary else after continue + "RET508", # Unnecessary else after break + "RET503", # implicit-return + + "SIM102", # Use a single if statement instead of nested if + "SIM105", # Use contextlib.suppress instead of try-except-pass + "SIM114", # Combine if branches using logical or + "SIM115", # open-file-with-context-handler + "SIM117", # Use a single with statement + "SIM118", # in-dict-keys + "SIM103", # needless-bool + + "ARG001", # Unused function argument + "ARG002", # Unused method argument + "ARG003", # Unused class method argument + "ARG005", # Unused lambda argument + + "PTH100", # os.path.abspath + "PTH103", # os.makedirs + "PTH107", # os.remove + "PTH109", # os.getcwd + "PTH110", # os.path.exists + "PTH111", # os.path.expanduser + "PTH112", # os.path.isdir + "PTH113", # os.path.isfile + "PTH118", # os.path.join + "PTH119", # os.path.basename + "PTH120", # os.path.dirname + "PTH122", # os.path.splitext + "PTH123", # open() + + "ISC001", # Implicitly concatenated string literals + "ISC002", # Implicitly concatenated string literals over multiple lines + "COM812", # Trailing comma missing + "COM819", # Trailing comma prohibited + + "INP001", # Implicit namespace package + "E501", # Line too long + "ERA001", # Commented-out code + "W291", # trailing-whitespace + "F405", # Undefined from star import + "F401", # unused-import + "W293", # blank-line-with-whitespace + "E402", # module-import-not-at-top + "DTZ005", # datetime.now() without tz + "DTZ901", # datetime.min/max without tz + "DTZ004", # call-datetime-utcfromtimestamp + "DTZ011", # call-date-today + "DTZ002", # call-datetime-today + "UP031", # printf-string-formatting + "DTZ003", # datetime.utcnow() + "PT006", # Wrong parametrize names type + "DTZ001", # datetime() without tz + "C408", # Unnecessary collection call + "RUF013", # Implicit optional + "PT009", # pytest unittest assertion + "B905", # zip-without-explicit-strict + "PLC0415", # import-outside-top-level + "PERF401", # manual-list-comprehension + "RUF012", # mutable-class-default + "RUF005", # collection-literal-concatenation + "PYI024", # collections-named-tuple + "F403", # undefined-local-with-import-star + "RUF015", # unnecessary-iterable-allocation-for-first-element + "PT019", # pytest-fixture-param-without-value + "PT007", # pytest-parametrize-values-wrong-type + "DTZ006", # call-datetime-fromtimestamp + "PT011", # pytest-raises-too-broad + "RUF059", # unused-unpacked-variable + "E701", # multiple-statements-on-one-line-colon + "B007", # unused-loop-control-variable + "LOG015", # root-logger-call + "PT012", # pytest-raises-with-multiple-statements + "G004", # logging-f-string + "A001", # builtin-variable-shadowing + "DTZ007", # call-datetime-strptime-without-zone + "RUF001", # ambiguous-unicode-character-string + "C401", # unnecessary-generator-set + "C405", # unnecessary-literal-set + "TC001", # typing-only-first-party-import + "C416", # unnecessary-comprehension + "A002", # builtin-argument-shadowing + "ICN001", # unconventional-import-alias + "EM103", # dot-format-in-exception + "FLY002", # static-join-to-f-string + "TRY301", # raise-within-try + "EXE001", # shebang-not-executable + "PLR1714", # repeated-equality-comparison + "E721", # type-comparison + "E741", # ambiguous-variable-name + "B008", # function-call-in-default-argument + "PT018", # pytest-composite-assertion + "PERF102", # incorrect-dict-iterator + "ARG004", # unused-static-method-argument + "E731", # lambda-assignment + "EXE002", # shebang-missing-executable-file + "UP008", # super-call-with-parameters + "SIM201", # negate-equal-op + "ASYNC230", # blocking-open-call-in-async-function + "ASYNC109", # async-function-with-timeout + "C417", # unnecessary-map + "UP030", # format-literals + "UP028", # yield-in-for-loop + "UP022", # replace-stdout-stderr + "UP036", # outdated-version-block + "W292", # missing-newline-at-end-of-file + "E101", # mixed-spaces-and-tabs + "COM818", # trailing-comma-on-bare-tuple + "B026", # star-arg-unpacking-after-keyword-arg + "B017", # assert-raises-exception + "B023", # function-uses-loop-variable + "B019", # cached-instance-method + "B024", # abstract-base-class-without-abstract-method + "B027", # empty-method-without-abstract-decorator + "W191", # tab-indentation + + "PERF203", # try-except within a loop + "C901", # Function is too complex + "E712", # Comparison to True/False + + "RUF003", # Ambiguous Unicode in comments + "SIM401", # if-else vs dict.get + "C419", # Unnecessary comprehension in call + "C414", # Unnecessary double cast or process + "PLC0206", # dict-index-missing-items + "C403", # unnecessary-list-comprehension-set + "TRY004", # type-check-without-type-error + "SIM112", # Uncapitalized environment variables + "PIE796", # non-unique-enums + "FURB116", # f-string-number-format + "SIM212", # if-expr-with-twisted-arms + "PIE810", # multiple-starts-ends-with + "FURB162", # fromisoformat-replace-z + "SIM211", # if-expr-with-false-true + "SIM101", # duplicate-isinstance-call + "FURB163", # redundant-log-base +] + +fixable = ["ALL"] +unfixable = [] + +dummy-variable-rgx = "^(_+|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?)|dummy|ignored_.*|unused_.*)$" + +[lint.per-file-ignores] + +"{**/tests/**/*.py,**/test_*.py,**/*_test.py}" = [ + "S101", + "B011", + "PT015", + "PT027", + "PLR2004", + "PLR0913", + "PLR0915", + "D", + "B008", + "PT008", + "PT011", + "SLF001", + "F811", + "F841", +] + +[lint.mccabe] +max-complexity = 10 + +[lint.pylint] +max-args = 8 +max-branches = 12 +max-returns = 6 +max-statements = 50 +max-public-methods = 20 +max-locals = 20 +max-nested-blocks = 5 +max-bool-expr = 5 + +[lint.isort] +force-single-line = true +force-sort-within-sections = true +order-by-type = false +known-local-folder = ["armis_sdk"] +lines-after-imports = 2 +combine-as-imports = false + +[lint.pydocstyle] +convention = "google" + +[lint.flake8-quotes] +inline-quotes = "double" +docstring-quotes = "double" +multiline-quotes = "double" +avoid-escape = true + +[lint.flake8-pytest-style] +fixture-parentheses = false +mark-parentheses = false +parametrize-names-type = "tuple" +parametrize-values-type = "list" +parametrize-values-row-type = "tuple" + +[lint.pycodestyle] +max-line-length = 120 +ignore-overlong-task-comments = true + +[lint.flake8-type-checking] +strict = false +runtime-evaluated-base-classes = [ + "pydantic.BaseModel", + "armis_sdk.entities.data_export.base_exported_entity.BaseExportedEntity", +] + +[format] +quote-style = "double" +indent-style = "space" +line-ending = "lf" +skip-magic-trailing-comma = false +docstring-code-format = true +docstring-code-line-length = 120 diff --git a/tests/armis_sdk/armis_sdk_test.py b/tests/armis_sdk/armis_sdk_test.py index a4e5ae1..363a194 100644 --- a/tests/armis_sdk/armis_sdk_test.py +++ b/tests/armis_sdk/armis_sdk_test.py @@ -5,6 +5,7 @@ from armis_sdk.clients.sites_client import SitesClient from armis_sdk.core.armis_client import ArmisClient + pytest_plugins = ["tests.plugins.setup_plugin"] diff --git a/tests/armis_sdk/clients/assets_client_test.py b/tests/armis_sdk/clients/assets_client_test.py index 3a4ab3e..0e0e97e 100644 --- a/tests/armis_sdk/clients/assets_client_test.py +++ b/tests/armis_sdk/clients/assets_client_test.py @@ -3,13 +3,15 @@ import pytest import pytest_httpx +from tests.armis_sdk.clients import assets_test_data + from armis_sdk.clients.assets_client import AssetsClient from armis_sdk.core.armis_error import ArmisError from armis_sdk.core.armis_error import BulkUpdateError from armis_sdk.entities.asset import Asset from armis_sdk.entities.asset_field_description import AssetFieldDescription from armis_sdk.entities.device import Device -from tests.armis_sdk.clients import assets_test_data + pytest_plugins = ["tests.plugins.auto_setup_plugin"] @@ -31,18 +33,12 @@ async def test_list_by_last_seen_datetime(httpx_mock: pytest_httpx.HTTPXMock): "last_seen_ge": "2025-12-03T00:00:00", }, }, - json={ - "items": [ - {"asset_id": 1, "fields": assets_test_data.MOCK_DEVICE_FULL_RAW_DATA} - ] - }, + json={"items": [{"asset_id": 1, "fields": assets_test_data.MOCK_DEVICE_FULL_RAW_DATA}]}, ) assets_client = AssetsClient() last_seen = datetime.datetime(2025, 12, 3) - devices = [ - device async for device in assets_client.list_by_last_seen(Device, last_seen) - ] + devices = [device async for device in assets_client.list_by_last_seen(Device, last_seen)] assert devices == [assets_test_data.MOCK_DEVICE_FULL] @@ -62,22 +58,13 @@ async def test_list_by_last_seen_datetime_explicit_fields( "last_seen_ge": "2025-12-03T00:00:00", }, }, - json={ - "items": [ - {"asset_id": 1, "fields": assets_test_data.MOCK_DEVICE_PARTIAL_RAW_DATA} - ] - }, + json={"items": [{"asset_id": 1, "fields": assets_test_data.MOCK_DEVICE_PARTIAL_RAW_DATA}]}, ) assets_client = AssetsClient() last_seen = datetime.datetime(2025, 12, 3) fields = ["brand", "custom.MyField1", "custom.MyField2", "purdue_level"] - devices = [ - device - async for device in assets_client.list_by_last_seen( - Device, last_seen, fields=fields - ) - ] + devices = [device async for device in assets_client.list_by_last_seen(Device, last_seen, fields=fields)] assert devices == [assets_test_data.MOCK_DEVICE_PARTIAL] @@ -92,18 +79,12 @@ async def test_list_by_last_seen_timedelta(httpx_mock: pytest_httpx.HTTPXMock): "fields": assets_test_data.ALL_DEVICE_FIELDS, "filter": {"filter_criteria": "LAST_SEEN", "last_seen_seconds": 3600}, }, - json={ - "items": [ - {"asset_id": 1, "fields": assets_test_data.MOCK_DEVICE_FULL_RAW_DATA} - ] - }, + json={"items": [{"asset_id": 1, "fields": assets_test_data.MOCK_DEVICE_FULL_RAW_DATA}]}, ) assets_client = AssetsClient() last_seen = datetime.timedelta(hours=1) - devices = [ - device async for device in assets_client.list_by_last_seen(Device, last_seen) - ] + devices = [device async for device in assets_client.list_by_last_seen(Device, last_seen)] assert devices == [assets_test_data.MOCK_DEVICE_FULL] @@ -120,22 +101,13 @@ async def test_list_by_last_seen_timedelta_explicit_fields( "fields": ["brand", "custom.MyField1", "custom.MyField2", "purdue_level"], "filter": {"filter_criteria": "LAST_SEEN", "last_seen_seconds": 3600}, }, - json={ - "items": [ - {"asset_id": 1, "fields": assets_test_data.MOCK_DEVICE_PARTIAL_RAW_DATA} - ] - }, + json={"items": [{"asset_id": 1, "fields": assets_test_data.MOCK_DEVICE_PARTIAL_RAW_DATA}]}, ) assets_client = AssetsClient() last_seen = datetime.timedelta(hours=1) fields = ["brand", "custom.MyField1", "custom.MyField2", "purdue_level"] - devices = [ - device - async for device in assets_client.list_by_last_seen( - Device, last_seen, fields=fields - ) - ] + devices = [device async for device in assets_client.list_by_last_seen(Device, last_seen, fields=fields)] assert devices == [assets_test_data.MOCK_DEVICE_PARTIAL] @@ -149,9 +121,7 @@ async def test_list_by_last_seen_invalid_fields(): ArmisError, match="The following fields are not supported with this operation: 'foo', 'bar'", ): - async for _ in assets_client.list_by_last_seen( - Device, last_seen, fields=fields - ): + async for _ in assets_client.list_by_last_seen(Device, last_seen, fields=fields): pass @@ -169,11 +139,7 @@ async def test_list_by_asset_id(httpx_mock: pytest_httpx.HTTPXMock): "asset_ids": ["1.1.1.1"], }, }, - json={ - "items": [ - {"asset_id": 1, "fields": assets_test_data.MOCK_DEVICE_FULL_RAW_DATA} - ] - }, + json={"items": [{"asset_id": 1, "fields": assets_test_data.MOCK_DEVICE_FULL_RAW_DATA}]}, ) assets_client = AssetsClient() @@ -204,11 +170,7 @@ async def test_list_by_asset_id_explicit_fields(httpx_mock: pytest_httpx.HTTPXMo "asset_ids": ["1.1.1.1"], }, }, - json={ - "items": [ - {"asset_id": 1, "fields": assets_test_data.MOCK_DEVICE_PARTIAL_RAW_DATA} - ] - }, + json={"items": [{"asset_id": 1, "fields": assets_test_data.MOCK_DEVICE_PARTIAL_RAW_DATA}]}, ) assets_client = AssetsClient() @@ -315,9 +277,7 @@ async def test_update_with_asset_id_source(httpx_mock: pytest_httpx.HTTPXMock): assets_client = AssetsClient() assets = [ - Device( - ipv4_addresses=["1.1.1.1"], custom={"MyField1": "value1", "MyField2": 2} - ), + Device(ipv4_addresses=["1.1.1.1"], custom={"MyField1": "value1", "MyField2": 2}), Device(ipv4_addresses=["2.2.2.2"], custom={"MyField1": "value3"}), ] fields = ["custom.MyField1", "custom.MyField2"] @@ -431,7 +391,5 @@ async def test_list_fields(httpx_mock: pytest_httpx.HTTPXMock): AssetFieldDescription(name="device_id", type="integer", is_list=False), AssetFieldDescription(name="names", type="string", is_list=True), AssetFieldDescription(name="custom.Size", type="enum", is_list=False), - AssetFieldDescription( - name="integration.qualys_agent_id", type="string", is_list=False - ), + AssetFieldDescription(name="integration.qualys_agent_id", type="string", is_list=False), ] diff --git a/tests/armis_sdk/clients/assets_test_data.py b/tests/armis_sdk/clients/assets_test_data.py index 77ad540..4220a3f 100644 --- a/tests/armis_sdk/clients/assets_test_data.py +++ b/tests/armis_sdk/clients/assets_test_data.py @@ -5,6 +5,7 @@ from armis_sdk.entities.network_interface import NetworkInterface from armis_sdk.entities.site import Site + ALL_DEVICE_FIELDS = [ "boundaries", "brand", diff --git a/tests/armis_sdk/clients/collectors_client_test.py b/tests/armis_sdk/clients/collectors_client_test.py index 5e0bb3e..a07d46c 100644 --- a/tests/armis_sdk/clients/collectors_client_test.py +++ b/tests/armis_sdk/clients/collectors_client_test.py @@ -7,6 +7,7 @@ from armis_sdk.entities.collector_image import CollectorImage from armis_sdk.entities.download_progress import DownloadProgress + pytest_plugins = ["tests.plugins.auto_setup_plugin"] @@ -72,9 +73,7 @@ async def test_download_image_to_path(httpx_mock: pytest_httpx.HTTPXMock): collectors_client = CollectorsClient() with tempfile.NamedTemporaryFile() as temp_file: - progress_items = [ - site async for site in collectors_client.download_image(temp_file.name) - ] + progress_items = [site async for site in collectors_client.download_image(temp_file.name)] assert progress_items == [ DownloadProgress(downloaded=16384, total=49151), @@ -103,9 +102,7 @@ async def test_download_image_to_file(httpx_mock: pytest_httpx.HTTPXMock): collectors_client = CollectorsClient() with tempfile.NamedTemporaryFile() as temp_file: - progress_items = [ - site async for site in collectors_client.download_image(temp_file) - ] + progress_items = [site async for site in collectors_client.download_image(temp_file)] assert progress_items == [ DownloadProgress(downloaded=16384, total=49151), diff --git a/tests/armis_sdk/clients/data_export_client_test.py b/tests/armis_sdk/clients/data_export_client_test.py index 8111499..78e1bf3 100644 --- a/tests/armis_sdk/clients/data_export_client_test.py +++ b/tests/armis_sdk/clients/data_export_client_test.py @@ -10,6 +10,7 @@ from armis_sdk.entities.data_export.base_exported_entity import BaseExportedEntity from armis_sdk.entities.data_export.data_export import DataExport + pytest_plugins = ["tests.plugins.auto_setup_plugin"] @@ -21,9 +22,7 @@ class MockEntity(BaseExportedEntity): @classmethod def series_to_model(cls, series: pandas.Series) -> "MockEntity": - return MockEntity( - name=series.loc["name"], description=series.loc["description"] - ) + return MockEntity(name=series.loc["name"], description=series.loc["description"]) async def test_disable(httpx_mock: pytest_httpx.HTTPXMock): @@ -83,9 +82,7 @@ async def test_get(httpx_mock: pytest_httpx.HTTPXMock): @mock.patch.object(pandas, "read_parquet") -async def test_export( - mock_read_parquet: mock.MagicMock, httpx_mock: pytest_httpx.HTTPXMock -): +async def test_export(mock_read_parquet: mock.MagicMock, httpx_mock: pytest_httpx.HTTPXMock): httpx_mock.add_response( url="https://api.armis.com/v3/data-export/mock-entity", json={ @@ -96,9 +93,7 @@ async def test_export( }, ) mock_read_parquet.side_effect = [ - pandas.DataFrame( - {"name": ["table", "chair"], "description": ["round", "high"]} - ), + pandas.DataFrame({"name": ["table", "chair"], "description": ["round", "high"]}), pandas.DataFrame({"name": ["book"], "description": ["hardcover"]}), ] diff --git a/tests/armis_sdk/clients/device_custom_properties_client_test.py b/tests/armis_sdk/clients/device_custom_properties_client_test.py index d3ad0af..1f349e1 100644 --- a/tests/armis_sdk/clients/device_custom_properties_client_test.py +++ b/tests/armis_sdk/clients/device_custom_properties_client_test.py @@ -3,12 +3,11 @@ import pytest import pytest_httpx -from armis_sdk.clients.device_custom_properties_client import ( - DeviceCustomPropertiesClient, -) +from armis_sdk.clients.device_custom_properties_client import DeviceCustomPropertiesClient from armis_sdk.core.armis_error import ArmisError from armis_sdk.entities.device_custom_property import DeviceCustomProperty + pytest_plugins = ["tests.plugins.auto_setup_plugin"] @@ -165,9 +164,7 @@ async def test_get(httpx_mock: pytest_httpx.HTTPXMock): ), ], ) -async def test_list_properties( - from_response, expected, httpx_mock: pytest_httpx.HTTPXMock -): +async def test_list_properties(from_response, expected, httpx_mock: pytest_httpx.HTTPXMock): httpx_mock.add_response( url="https://api.armis.com/v3/settings/device-custom-properties", method="GET", @@ -194,9 +191,7 @@ async def test_update(httpx_mock: pytest_httpx.HTTPXMock): ) client = DeviceCustomPropertiesClient() - property_ = DeviceCustomProperty( - id=1, name="mock_name", type="string", description="new_description" - ) + property_ = DeviceCustomProperty(id=1, name="mock_name", type="string", description="new_description") updated_property = await client.update(property_) assert updated_property == DeviceCustomProperty( diff --git a/tests/armis_sdk/clients/sites_client_test.py b/tests/armis_sdk/clients/sites_client_test.py index 5d1c7c5..c5a6e08 100644 --- a/tests/armis_sdk/clients/sites_client_test.py +++ b/tests/armis_sdk/clients/sites_client_test.py @@ -6,6 +6,7 @@ from armis_sdk.entities.asq_rule import AsqRule from armis_sdk.entities.site import Site + pytest_plugins = ["tests.plugins.auto_setup_plugin"] @@ -76,9 +77,7 @@ async def test_create_without_name(httpx_mock: pytest_httpx.HTTPXMock): async def test_delete(httpx_mock: pytest_httpx.HTTPXMock): - httpx_mock.add_response( - url="https://api.armis.com/v3/settings/sites/1", method="DELETE" - ) + httpx_mock.add_response(url="https://api.armis.com/v3/settings/sites/1", method="DELETE") site = Site(id=1) sites_client = SitesClient() @@ -235,9 +234,7 @@ async def test_list_sites(from_response, expected, httpx_mock: pytest_httpx.HTTP assert sites == [expected] -async def test_list_sites_with_multiple_pages( - monkeypatch, httpx_mock: pytest_httpx.HTTPXMock -): +async def test_list_sites_with_multiple_pages(monkeypatch, httpx_mock: pytest_httpx.HTTPXMock): monkeypatch.setenv("ARMIS_PAGE_SIZE", "2") httpx_mock.add_response( url="https://api.armis.com/v3/settings/sites?limit=2", @@ -310,9 +307,7 @@ async def test_update_simple_properties(httpx_mock: pytest_httpx.HTTPXMock): site = Site(id=1, name="new_name", location="new location", parent_id=2) updated_site = await sites_client.update(site) - assert updated_site == Site( - id=1, name="new_name", location="new location", parent_id=2 - ) + assert updated_site == Site(id=1, name="new_name", location="new location", parent_id=2) async def test_update_without_id(httpx_mock: pytest_httpx.HTTPXMock): diff --git a/tests/armis_sdk/core/armis_client_test.py b/tests/armis_sdk/core/armis_client_test.py index d06cf7d..a7dda44 100644 --- a/tests/armis_sdk/core/armis_client_test.py +++ b/tests/armis_sdk/core/armis_client_test.py @@ -7,6 +7,7 @@ from armis_sdk.core.armis_client import ArmisClient + pytest_plugins = ["tests.plugins.auto_setup_plugin"] try: @@ -19,9 +20,7 @@ async def test_request_headers(httpx_mock: pytest_httpx.HTTPXMock): httpx_mock.add_response( match_headers={ "User-Agent": ( - f"Python/{platform.python_version()} " - f"python-httpx/{httpx.__version__} " - f"ArmisPythonSDK/v{VERSION}" + f"Python/{platform.python_version()} python-httpx/{httpx.__version__} ArmisPythonSDK/v{VERSION}" ), }, url="https://api.armis.com/mock/endpoint", @@ -55,9 +54,7 @@ async def test_retries(monkeypatch, httpx_mock: pytest_httpx.HTTPXMock): assert response.status_code == httpx.codes.OK -async def test_retries_with_eventual_failure( - monkeypatch, httpx_mock: pytest_httpx.HTTPXMock -): +async def test_retries_with_eventual_failure(monkeypatch, httpx_mock: pytest_httpx.HTTPXMock): monkeypatch.setenv("ARMIS_REQUEST_RETRIES", "2") httpx_mock.add_response( url="https://api.armis.com/mock/endpoint", @@ -79,9 +76,7 @@ async def test_retries_with_eventual_failure( assert response.status_code == httpx.codes.GATEWAY_TIMEOUT -async def test_retrie_with_writable_method( - monkeypatch, httpx_mock: pytest_httpx.HTTPXMock -): +async def test_retrie_with_writable_method(monkeypatch, httpx_mock: pytest_httpx.HTTPXMock): monkeypatch.setenv("ARMIS_REQUEST_RETRIES", "2") httpx_mock.add_response( method="POST", @@ -96,9 +91,7 @@ async def test_retrie_with_writable_method( assert response.status_code == httpx.codes.GATEWAY_TIMEOUT -async def test_list_with_multiple_pages( - monkeypatch, httpx_mock: pytest_httpx.HTTPXMock -): +async def test_list_with_multiple_pages(monkeypatch, httpx_mock: pytest_httpx.HTTPXMock): monkeypatch.setenv("ARMIS_PAGE_SIZE", "2") httpx_mock.add_response( url="https://api.armis.com/v3/settings/sites?limit=2", diff --git a/tests/armis_sdk/entities/asq_rule_test.py b/tests/armis_sdk/entities/asq_rule_test.py new file mode 100644 index 0000000..ad1cea7 --- /dev/null +++ b/tests/armis_sdk/entities/asq_rule_test.py @@ -0,0 +1,12 @@ +from armis_sdk.entities.asq_rule import AsqRule + + +def test_asq_rule_nested(): + inner = AsqRule(or_=["asq2", "asq3"]) + outer = AsqRule(and_=["asq1", inner]) + assert outer.and_ == ["asq1", inner] + + +def test_asq_rule_from_asq(): + rule = AsqRule.from_asq("deviceName:MyDevice") + assert rule.or_ == ["deviceName:MyDevice"] diff --git a/tests/armis_sdk/entities/data_export/application_test.py b/tests/armis_sdk/entities/data_export/application_test.py index 1ee4345..0994d9a 100644 --- a/tests/armis_sdk/entities/data_export/application_test.py +++ b/tests/armis_sdk/entities/data_export/application_test.py @@ -5,6 +5,7 @@ from armis_sdk.entities.data_export.application import Application + ApplicationNT = collections.namedtuple( "ApplicationNT", ["device_id", "name", "vendor", "version", "cpe", "first_seen", "last_seen"], diff --git a/tests/armis_sdk/entities/data_export/risk_factor_test.py b/tests/armis_sdk/entities/data_export/risk_factor_test.py index b35102b..446c243 100644 --- a/tests/armis_sdk/entities/data_export/risk_factor_test.py +++ b/tests/armis_sdk/entities/data_export/risk_factor_test.py @@ -7,6 +7,7 @@ from armis_sdk.entities.data_export.risk_factor import RiskFactor from armis_sdk.entities.data_export.risk_factor import RiskFactorRecommendedAction + RiskFactorNT = collections.namedtuple( "RiskFactorNT", [ diff --git a/tests/armis_sdk/entities/data_export/vulnerability_test.py b/tests/armis_sdk/entities/data_export/vulnerability_test.py index 56749e7..a77115e 100644 --- a/tests/armis_sdk/entities/data_export/vulnerability_test.py +++ b/tests/armis_sdk/entities/data_export/vulnerability_test.py @@ -5,6 +5,7 @@ from armis_sdk.entities.data_export.vulnerability import Vulnerability + VulnerabilityNT = collections.namedtuple( "VulnerabilityNT", [ diff --git a/tests/armis_sdk/entities/device_test.py b/tests/armis_sdk/entities/device_test.py new file mode 100644 index 0000000..9cf94c1 --- /dev/null +++ b/tests/armis_sdk/entities/device_test.py @@ -0,0 +1,24 @@ +import datetime + +from armis_sdk.entities.device import Device + + +def test_device_instantiation_with_datetime(): + device = Device(first_seen=datetime.datetime(2025, 1, 1), last_seen=datetime.datetime(2025, 6, 1)) + assert device.first_seen == datetime.datetime(2025, 1, 1) + assert device.last_seen == datetime.datetime(2025, 6, 1) + + +def test_device_instantiation_with_none_datetimes(): + device = Device() + assert device.first_seen is None + assert device.last_seen is None + + +def test_device_model_json_schema_includes_datetime_fields(): + """datetime.datetime fields must appear in the JSON schema (regression: TYPE_CHECKING import drops them).""" + schema = Device.model_json_schema() + props = schema.get("properties", {}) + # Fields use camelCase aliases per BaseEntity's alias_generator + assert "firstSeen" in props, f"firstSeen missing from schema properties: {sorted(props.keys())}" + assert "lastSeen" in props, f"lastSeen missing from schema properties: {sorted(props.keys())}" diff --git a/tests/armis_sdk/entities/site_test.py b/tests/armis_sdk/entities/site_test.py new file mode 100644 index 0000000..64eae0b --- /dev/null +++ b/tests/armis_sdk/entities/site_test.py @@ -0,0 +1,13 @@ +from armis_sdk.entities.site import Site + + +def test_site_with_children(): + child = Site(id=2, name="Child Site") + parent = Site(id=1, name="Parent Site", children=[child]) + assert len(parent.children) == 1 + assert parent.children[0].name == "Child Site" + + +def test_site_no_children(): + site = Site(id=1, name="HQ") + assert site.children == [] diff --git a/tests/plugins/auto_setup_plugin.py b/tests/plugins/auto_setup_plugin.py index c0bc741..6bad542 100644 --- a/tests/plugins/auto_setup_plugin.py +++ b/tests/plugins/auto_setup_plugin.py @@ -1,8 +1,9 @@ import pytest + pytest_plugins = ["tests.plugins.setup_plugin"] @pytest.fixture(autouse=True) -def auto_setup(setup_env_variables, authorized): # pylint: disable=unused-argument +def auto_setup(setup_env_variables, authorized): return From 5a8369713ffa4deaedfd8af9c2d24bfc696556da Mon Sep 17 00:00:00 2001 From: Shai Lachmanovich Date: Thu, 16 Apr 2026 11:32:47 +0200 Subject: [PATCH 2/4] Remove unneeded workflow --- .github/workflows/format.yml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 .github/workflows/format.yml diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml deleted file mode 100644 index 248bd23..0000000 --- a/.github/workflows/format.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Run formatter - -on: - - push - -permissions: - contents: read - -jobs: - format: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - uses: ./.github/workflows/install_deps - - - name: Run formatter - run: poetry run black --check --diff . - - - name: Run isort - run: poetry run isort --check-only --diff . - From 0e624b2c1d8721dd2608600c8255e28431172c58 Mon Sep 17 00:00:00 2001 From: Shai Lachmanovich Date: Thu, 16 Apr 2026 11:35:40 +0200 Subject: [PATCH 3/4] . --- .github/workflows/format.yml | 19 +++++++++++++++++++ .github/workflows/lint.yml | 3 --- 2 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/format.yml diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 0000000..b587f91 --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,19 @@ +name: Run formatter + +on: + - push + +permissions: + contents: read + +jobs: + format: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/workflows/install_deps + + - name: Run formatter + run: poetry run ruff format --check . diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 565aa0b..fe6719e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,6 +17,3 @@ jobs: - name: Run linter run: poetry run ruff check . - - - name: Check formatting - run: poetry run ruff format --check . From fd82821f0a000bac157a4f6a50acb8a5b3b543c4 Mon Sep 17 00:00:00 2001 From: Shai Lachmanovich Date: Thu, 16 Apr 2026 11:45:59 +0200 Subject: [PATCH 4/4] . --- poetry.lock | 116 ++++++++++++++++++++++++++++++++++++++++--------- pyproject.toml | 1 + 2 files changed, 97 insertions(+), 20 deletions(-) diff --git a/poetry.lock b/poetry.lock index 723dab5..cb0a070 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -7,6 +7,7 @@ description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -19,6 +20,7 @@ description = "High level compatibility layer for multiple asynchronous event lo optional = false python-versions = ">=3.9" groups = ["main", "build"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, @@ -32,7 +34,7 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""] +test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] [[package]] @@ -42,13 +44,14 @@ description = "Internationalization utilities" optional = false python-versions = ">=3.8" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, ] [package.extras] -dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] +dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] [[package]] name = "backrefs" @@ -57,6 +60,7 @@ description = "A wrapper around re and regex that adds additional back reference optional = false python-versions = ">=3.9" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d"}, {file = "backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b"}, @@ -76,6 +80,7 @@ description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" groups = ["main", "build", "docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, @@ -88,6 +93,7 @@ description = "The Real First Universal Charset Detector. Open, modern and activ optional = false python-versions = ">=3.7" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, @@ -190,6 +196,7 @@ description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -209,7 +216,23 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {build = "sys_platform == \"win32\""} +markers = {build = "python_version <= \"3.11\" and sys_platform == \"win32\" or python_version >= \"3.12\" and sys_platform == \"win32\"", docs = "python_version <= \"3.11\" or python_version >= \"3.12\""} + +[[package]] +name = "eval-type-backport" +version = "0.3.1" +description = "Like `typing._eval_type`, but lets older Python versions use newer typing features." +optional = false +python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "eval_type_backport-0.3.1-py3-none-any.whl", hash = "sha256:279ab641905e9f11129f56a8a78f493518515b83402b860f6f06dd7c011fdfa8"}, + {file = "eval_type_backport-0.3.1.tar.gz", hash = "sha256:57e993f7b5b69d271e37482e62f74e76a0276c82490cf8e4f0dffeb6b332d5ed"}, +] + +[package.extras] +tests = ["pytest"] [[package]] name = "exceptiongroup" @@ -237,6 +260,7 @@ description = "Copy your docs directly to the gh-pages branch." optional = false python-versions = "*" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, @@ -255,6 +279,7 @@ description = "Signatures for entire Python programs. Extract the structure, the optional = false python-versions = ">=3.9" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "griffe-1.7.3-py3-none-any.whl", hash = "sha256:c6b3ee30c2f0f17f30bcdef5068d6ab7a2a4f1b8bf1a3e74b56fffd21e1c5f75"}, {file = "griffe-1.7.3.tar.gz", hash = "sha256:52ee893c6a3a968b639ace8015bec9d36594961e156e23315c8e8e51401fa50b"}, @@ -270,6 +295,7 @@ description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.8" groups = ["main", "build"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, @@ -282,6 +308,7 @@ description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" groups = ["main", "build"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, @@ -304,6 +331,7 @@ description = "The next generation HTTP client." optional = false python-versions = ">=3.8" groups = ["main", "build"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -316,7 +344,7 @@ httpcore = "==1.*" idna = "*" [package.extras] -brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +brotli = ["brotli", "brotlicffi"] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -329,6 +357,7 @@ description = "A retry layer for HTTPX." optional = false python-versions = ">=3.9" groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "httpx_retries-0.4.0-py3-none-any.whl", hash = "sha256:61df3e70889e2f74ebcaf1ae4213f1bdb67dc68dfa8b2e9da41a63afd513e3fb"}, {file = "httpx_retries-0.4.0.tar.gz", hash = "sha256:a7aa513e3f1eef347aac69adecfad9f421210a84df88c88f935ec130f98e6642"}, @@ -344,6 +373,7 @@ description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" groups = ["main", "build", "docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -359,7 +389,7 @@ description = "Read metadata from Python packages" optional = false python-versions = ">=3.9" groups = ["docs"] -markers = "python_version == \"3.9\"" +markers = "python_version < \"3.10\"" files = [ {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, @@ -369,12 +399,12 @@ files = [ zipp = ">=3.20" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +test = ["flufl.flake8", "importlib_resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] @@ -384,6 +414,7 @@ description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" groups = ["build"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, @@ -396,6 +427,7 @@ description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, @@ -414,6 +446,7 @@ description = "Python implementation of John Gruber's Markdown." optional = false python-versions = ">=3.9" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc"}, {file = "markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f"}, @@ -433,6 +466,7 @@ description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -504,6 +538,7 @@ description = "A deep merge function for 🐍." optional = false python-versions = ">=3.6" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, @@ -516,6 +551,7 @@ description = "Project documentation with Markdown." optional = false python-versions = ">=3.8" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, @@ -539,7 +575,7 @@ watchdog = ">=2.0" [package.extras] i18n = ["babel (>=2.9.0)"] -min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] [[package]] name = "mkdocs-autorefs" @@ -548,6 +584,7 @@ description = "Automatically link across pages in MkDocs." optional = false python-versions = ">=3.9" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mkdocs_autorefs-1.4.2-py3-none-any.whl", hash = "sha256:83d6d777b66ec3c372a1aad4ae0cf77c243ba5bcda5bf0c6b8a2c5e7a3d89f13"}, {file = "mkdocs_autorefs-1.4.2.tar.gz", hash = "sha256:e2ebe1abd2b67d597ed19378c0fff84d73d1dbce411fce7a7cc6f161888b6749"}, @@ -565,6 +602,7 @@ description = "MkDocs extension that lists all dependencies according to a mkdoc optional = false python-versions = ">=3.8" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, @@ -583,6 +621,7 @@ description = "Documentation that simply works" optional = false python-versions = ">=3.8" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mkdocs_material-9.6.14-py3-none-any.whl", hash = "sha256:3b9cee6d3688551bf7a8e8f41afda97a3c39a12f0325436d76c86706114b721b"}, {file = "mkdocs_material-9.6.14.tar.gz", hash = "sha256:39d795e90dce6b531387c255bd07e866e027828b7346d3eba5ac3de265053754"}, @@ -613,6 +652,7 @@ description = "Extension pack for Python Markdown and MkDocs Material." optional = false python-versions = ">=3.8" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, @@ -625,6 +665,7 @@ description = "Automatic documentation from sources, for MkDocs." optional = false python-versions = ">=3.9" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mkdocstrings-0.29.1-py3-none-any.whl", hash = "sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6"}, {file = "mkdocstrings-0.29.1.tar.gz", hash = "sha256:8722f8f8c5cd75da56671e0a0c1bbed1df9946c0cef74794d6141b34011abd42"}, @@ -651,6 +692,7 @@ description = "A Python handler for mkdocstrings." optional = false python-versions = ">=3.9" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mkdocstrings_python-1.16.11-py3-none-any.whl", hash = "sha256:25d96cc9c1f9c272ea1bd8222c900b5f852bf46c984003e9c7c56eaa4696190f"}, {file = "mkdocstrings_python-1.16.11.tar.gz", hash = "sha256:935f95efa887f99178e4a7becaaa1286fb35adafffd669b04fd611d97c00e5ce"}, @@ -669,6 +711,7 @@ description = "Optional static typing for Python" optional = false python-versions = ">=3.9" groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mypy-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c"}, {file = "mypy-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571"}, @@ -724,6 +767,7 @@ description = "Type system extensions for programs checked with the mypy type ch optional = false python-versions = ">=3.8" groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, @@ -736,6 +780,7 @@ description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, @@ -791,6 +836,7 @@ description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["build", "docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, @@ -803,6 +849,7 @@ description = "Divides large result sets into pages for easier browsing" optional = false python-versions = "*" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, @@ -819,6 +866,7 @@ description = "Powerful data structures for data analysis, time series, and stat optional = false python-versions = ">=3.9" groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c"}, {file = "pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a"}, @@ -919,6 +967,7 @@ description = "Type annotations for pandas" optional = false python-versions = ">=3.9" groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pandas_stubs-2.2.2.240807-py3-none-any.whl", hash = "sha256:893919ad82be4275f0d07bb47a95d08bae580d3fdea308a7acfcb3f02e76186e"}, {file = "pandas_stubs-2.2.2.240807.tar.gz", hash = "sha256:64a559725a57a449f46225fbafc422520b7410bff9252b661a225b5559192a93"}, @@ -935,6 +984,7 @@ description = "Utility library for gitignore style pattern matching of file path optional = false python-versions = ">=3.8" groups = ["dev", "docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -947,6 +997,7 @@ description = "A small Python package for determining appropriate platform-speci optional = false python-versions = ">=3.9" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, @@ -964,6 +1015,7 @@ description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" groups = ["build"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, @@ -980,6 +1032,7 @@ description = "Python library for Apache Arrow" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pyarrow-21.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e563271e2c5ff4d4a4cbeb2c83d5cf0d4938b891518e676025f7268c6fe5fe26"}, {file = "pyarrow-21.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:fee33b0ca46f4c85443d6c450357101e47d53e6c3f008d658c27a2d020d44c79"}, @@ -1036,6 +1089,7 @@ description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7"}, {file = "pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a"}, @@ -1049,7 +1103,7 @@ typing-inspection = ">=0.4.0" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] +timezone = ["tzdata"] [[package]] name = "pydantic-core" @@ -1058,6 +1112,7 @@ description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, @@ -1170,6 +1225,7 @@ description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" groups = ["build", "docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, @@ -1185,6 +1241,7 @@ description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pymdown_extensions-10.15-py3-none-any.whl", hash = "sha256:46e99bb272612b0de3b7e7caf6da8dd5f4ca5212c0b273feb9304e236c484e5f"}, {file = "pymdown_extensions-10.15.tar.gz", hash = "sha256:0e5994e32155f4b03504f939e501b981d306daf7ec2aa1cd2eb6bd300784f8f7"}, @@ -1204,6 +1261,7 @@ description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.9" groups = ["build"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e"}, {file = "pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6"}, @@ -1228,6 +1286,7 @@ description = "Pytest support for asyncio" optional = false python-versions = ">=3.9" groups = ["build"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3"}, {file = "pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f"}, @@ -1248,6 +1307,7 @@ description = "Send responses to httpx." optional = false python-versions = ">=3.9" groups = ["build"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytest_httpx-0.35.0-py3-none-any.whl", hash = "sha256:ee11a00ffcea94a5cbff47af2114d34c5b231c326902458deed73f9c459fd744"}, {file = "pytest_httpx-0.35.0.tar.gz", hash = "sha256:d619ad5d2e67734abfbb224c3d9025d64795d4b8711116b1a13f72a251ae511f"}, @@ -1267,6 +1327,7 @@ description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main", "docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -1282,6 +1343,7 @@ description = "Read key-value pairs from a .env file and set them as environment optional = false python-versions = ">=3.9" groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, @@ -1297,6 +1359,7 @@ description = "World timezone definitions, modern and historical" optional = false python-versions = "*" groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, @@ -1309,6 +1372,7 @@ description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -1372,6 +1436,7 @@ description = "A custom YAML tag for referencing environment variables in YAML f optional = false python-versions = ">=3.9" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04"}, {file = "pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff"}, @@ -1387,6 +1452,7 @@ description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -1409,6 +1475,7 @@ description = "An extremely fast Python linter and code formatter, written in Ru optional = false python-versions = ">=3.7" groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff"}, {file = "ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3"}, @@ -1437,19 +1504,20 @@ description = "Easily download, build, install, upgrade, and uninstall Python pa optional = false python-versions = ">=3.9" groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] +core = ["importlib_metadata (>=6)", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "six" @@ -1458,6 +1526,7 @@ description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main", "docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -1470,6 +1539,7 @@ description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" groups = ["main", "build"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -1525,6 +1595,7 @@ description = "Typing stubs for pytz" optional = false python-versions = ">=3.9" groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "types_pytz-2025.2.0.20250809-py3-none-any.whl", hash = "sha256:4f55ed1b43e925cf851a756fe1707e0f5deeb1976e15bf844bcaa025e8fbd0db"}, {file = "types_pytz-2025.2.0.20250809.tar.gz", hash = "sha256:222e32e6a29bb28871f8834e8785e3801f2dc4441c715cd2082b271eecbe21e5"}, @@ -1541,7 +1612,7 @@ files = [ {file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"}, {file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"}, ] -markers = {build = "python_version < \"3.13\"", docs = "python_version < \"3.11\""} +markers = {main = "python_version <= \"3.11\" or python_version >= \"3.12\"", build = "python_version <= \"3.11\" or python_version >= \"3.12\" and python_version < \"3.13\"", dev = "python_version <= \"3.11\" or python_version >= \"3.12\"", docs = "python_version < \"3.11\""} [[package]] name = "typing-inspection" @@ -1550,6 +1621,7 @@ description = "Runtime typing introspection tools" optional = false python-versions = ">=3.9" groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, @@ -1565,6 +1637,7 @@ description = "Provider of IANA time zone data" optional = false python-versions = ">=2" groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, @@ -1577,6 +1650,7 @@ description = "A library to help automate the creation of universal python libra optional = false python-versions = ">=3.9" groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "universalasync-0.4.0.1-py3-none-any.whl", hash = "sha256:55691b23f8265ff9a658d5eaee5a0375d75ac46432862198178c9a360062ba52"}, {file = "universalasync-0.4.0.1.tar.gz", hash = "sha256:aadf1f20170366b76fb0317a343775c403842c8db25062f7d68184a09b3e882c"}, @@ -1589,13 +1663,14 @@ description = "HTTP library with thread-safe connection pooling, file post, and optional = false python-versions = ">=3.9" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, ] [package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -1607,6 +1682,7 @@ description = "Filesystem events monitoring" optional = false python-versions = ">=3.9" groups = ["docs"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, @@ -1650,21 +1726,21 @@ description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.9" groups = ["docs"] -markers = "python_version == \"3.9\"" +markers = "python_version < \"3.10\"" files = [ {file = "zipp-3.22.0-py3-none-any.whl", hash = "sha256:fe208f65f2aca48b81f9e6fd8cf7b8b32c26375266b009b413d45306b6148343"}, {file = "zipp-3.22.0.tar.gz", hash = "sha256:dd2f28c3ce4bc67507bfd3781d21b7bb2be31103b51a4553ad7d90b84e57ace5"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib_resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +test = ["big-O", "importlib_resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9" -content-hash = "e5f5e58ecd17b60e3028d811b8188bd87bb8020976980e9d006081e33206e9cd" +content-hash = "c87f9fabe58c94133995152e57db1001cdf444e9b57504c615cc0a5f8e20f411" diff --git a/pyproject.toml b/pyproject.toml index 123fef2..1050640 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ build-backend = "poetry.core.masonry.api" plugins = ['pydantic.mypy'] [tool.poetry.dependencies] +eval_type_backport = "*" pandas = "*" pyarrow = "*" pydantic = "*"