diff --git a/.github/workflows/workflow-linter.yml b/.github/workflows/workflow-linter.yml index ac95d25..f3c6245 100644 --- a/.github/workflows/workflow-linter.yml +++ b/.github/workflows/workflow-linter.yml @@ -84,6 +84,9 @@ jobs: - name: "Linting: ruff format" run: "uv run ruff format --check --diff ." + - name: "Linting: ty check" + run: "uv run ty check ." + # TODO: Need to cleanup code # - name: "Pylint Tests" # run: "uv run pylint infrahub_sync/**/*.py" diff --git a/AGENTS.md b/AGENTS.md index 4997fcb..f5dde7e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,7 +34,6 @@ uv run infrahub-sync list --directory examples/ # Make a change, then: uv run invoke format uv run invoke lint -uv run mypy infrahub_sync/ --ignore-missing-imports uv run infrahub-sync list --directory examples/ # If docs/CLI changed: @@ -50,13 +49,14 @@ Run these in order before committing. uv sync uv run invoke format uv run invoke lint -uv run mypy infrahub_sync/ --ignore-missing-imports ``` +`invoke lint` runs ruff → pylint → yamllint → ty. + **Policy:** - New or changed code is Ruff-clean and typed where touched (docstrings, specific exceptions). -- Do not increase existing mypy debt. If needed, use targeted `# type: ignore[]` with a short TODO. +- The codebase is clean under ty with no `[[tool.ty.overrides]]` blocks. Don't reintroduce overrides to mask type errors — fix the underlying issue, or use a targeted `# ty: ignore[]` with a short TODO at the call site. - If you add tests, run `uv run pytest -q`. **CLI sanity after changes:** @@ -121,7 +121,7 @@ infrahub-sync/ - Prefer explicit types on new or changed code. - Ruff: formatted and lint-clean. Honor `pyproject.toml`. - Pylint: fix actionable issues in touched code; some warnings are expected. -- Mypy: run with `--ignore-missing-imports`; do not increase the error count. +- ty: included in `uv run invoke lint`; do not increase the error count. For an ad-hoc check, `uv run ty check .` works too. - Public functions and classes require concise docstrings. - Raise specific exceptions; avoid broad `except Exception:`. @@ -187,6 +187,7 @@ uv run invoke --list # linter.lint-ruff Lint Python code with ruff # linter.lint-pylint Lint Python code with pylint # linter.lint-yaml Lint YAML files with yamllint +# linter.lint-ty Type-check Python code with ty # # docs.* # docs.generate Generate CLI documentation @@ -205,7 +206,7 @@ uv run invoke --list - Optional dependencies (for example, `pynetbox`, `pynautobot`) may be missing, producing import warnings. - `generate` and `sync` require running servers (Infrahub, NetBox, Nautobot). -- Existing mypy debt exists; do not increase it and type the code you touch. +- The codebase is clean under ty; there are no `[[tool.ty.overrides]]` blocks in `pyproject.toml`. Prefer real type fixes over reintroducing overrides. - Docs npm audit may flag dev-only vulnerabilities; they do not affect the Python package. ## Development Rules @@ -215,7 +216,7 @@ uv run invoke --list - Do not force-push on shared branches. - Do not amend to hide pre-commit fixes; use a follow-up commit. - Apply PR labels: `bugs`, `breaking`, `enhancements`, `features` (default to `enhancements`). -- Always run the required workflow (format → lint → mypy → CLI sanity) before a PR. +- Always run the required workflow (format → lint → CLI sanity) before a PR. `invoke lint` now includes ty alongside ruff, pylint, and yamllint. ### Commit and PR Messages @@ -236,7 +237,7 @@ uv run invoke --list **Approval checklist:** - [ ] Format and lint clean on changed areas. -- [ ] No increase in mypy errors; new code typed. +- [ ] `uv run ty check .` exits 0; new code typed. - [ ] CLI behaviors validated (`--help`, `list`, targeted `generate`). - [ ] Docs updated if flags or config changed. - [ ] Error handling uses specific exception types and clear messages. diff --git a/infrahub_sync/__init__.py b/infrahub_sync/__init__.py index 214667e..7eae311 100644 --- a/infrahub_sync/__init__.py +++ b/infrahub_sync/__init__.py @@ -3,9 +3,14 @@ import logging import operator import re -from typing import Any, Union +from typing import TYPE_CHECKING, Any, ClassVar, Union import pydantic + +if TYPE_CHECKING: + from collections.abc import Callable + + from diffsync.store import BaseStore from diffsync.enum import DiffSyncFlags from jinja2 import StrictUndefined from jinja2.nativetypes import NativeEnvironment @@ -16,14 +21,13 @@ logger = logging.getLogger(__name__) +# Pydantic v1/v2 compatibility shim — runtime branch picks the right decorator + kwargs. if version.parse(pydantic.__version__) >= version.parse("2.0.0"): - # With Pydantic v2, we use `field_validator` with mode "before" from pydantic import field_validator as validator_decorator - validator_kwargs = {"mode": "before"} + validator_kwargs: dict[str, Any] = {"mode": "before"} else: - # With Pydantic v1, we use validator with `pre=True` and `allow_reuse=True` - from pydantic import validator as validator_decorator + from pydantic import validator as validator_decorator # ty: ignore[deprecated] validator_kwargs = {"pre": True, "allow_reuse": True} @@ -52,7 +56,7 @@ class SchemaMappingModel(pydantic.BaseModel): identifiers: list[str] | None = pydantic.Field(default=None) filters: list[SchemaMappingFilter] | None = pydantic.Field(default=None) transforms: list[SchemaMappingTransform] | None = pydantic.Field(default=None) - fields: list[SchemaMappingField] | None = [] + fields: list[SchemaMappingField] = pydantic.Field(default_factory=list) class SyncAdapter(pydantic.BaseModel): @@ -76,7 +80,7 @@ class SyncConfig(pydantic.BaseModel): schema_mapping: list[SchemaMappingModel] = [] diffsync_flags: list[Union[str, DiffSyncFlags]] | None = [] - @validator_decorator("diffsync_flags", **validator_kwargs) + @validator_decorator("diffsync_flags", **validator_kwargs) # ty: ignore[no-matching-overload] def convert_str_to_enum(cls, v): if not isinstance(v, list): msg = "diffsync_flags must be provided as a list" @@ -111,7 +115,7 @@ def convert_to_int(value: Any) -> int: raise ValueError(msg) from exc -FILTERS_OPERATIONS = { +FILTERS_OPERATIONS: dict[str, Callable[..., Any]] = { "==": operator.eq, "!=": operator.ne, ">": lambda field, value: operator.gt(convert_to_int(field), convert_to_int(value)), @@ -131,6 +135,10 @@ def convert_to_int(value: Any) -> int: class DiffSyncMixin: + top_level: ClassVar[list[str]] = [] + config: SyncConfig + store: BaseStore + def load(self): """Load all the models, one by one based on the order defined in top_level.""" for item in self.top_level: @@ -146,6 +154,9 @@ def model_loader(self, model_name: str, model): class DiffSyncModelMixin: + # Set on generated subclasses (see generator/templates/diffsync_models.j2). + local_id: str | None = None + @classmethod def apply_filter(cls, field_value: Any, operation: str, value: Any) -> bool: """Apply a specified operation to a field value.""" @@ -190,8 +201,9 @@ def apply_transform(cls, item: dict[str, Any], transform_expr: str, field: str) ) # Allow subclasses to add custom filters - if hasattr(cls, "_add_custom_filters"): - cls._add_custom_filters(native_env, item) + add_custom_filters: Callable[..., None] | None = getattr(cls, "_add_custom_filters", None) + if add_custom_filters is not None: + add_custom_filters(native_env, item) # Compile the template with the native env template = native_env.from_string(transform_expr) @@ -250,13 +262,18 @@ def get_resource_name(cls, schema_mapping: list[SchemaMappingModel]) -> str: """Get the resource name from the schema mapping.""" for element in schema_mapping: if element.name == cls.__name__: + if element.mapping is None: + msg = f"Resource mapping is unset for class {cls.__name__}" + raise ValueError(msg) return element.mapping msg = f"Resource name not found for class {cls.__name__}" raise ValueError(msg) @classmethod def is_list(cls, name): - field = cls.__fields__.get(name) + # Pydantic v2 exposes `model_fields`; v1 uses `__fields__`. Try both. + fields = getattr(cls, "model_fields", None) or getattr(cls, "__fields__", None) or {} + field = fields.get(name) if not field: msg = f"Unable to find the field {name} under {cls}" raise ValueError(msg) diff --git a/infrahub_sync/adapters/aci.py b/infrahub_sync/adapters/aci.py index a91e3f2..e167bd2 100644 --- a/infrahub_sync/adapters/aci.py +++ b/infrahub_sync/adapters/aci.py @@ -6,15 +6,15 @@ import logging import os from datetime import datetime, timedelta, timezone -from typing import TYPE_CHECKING, Any, ClassVar +from typing import Any, ClassVar -import requests # type: ignore[import] +import requests import urllib3 from diffsync import Adapter, DiffSyncModel -from requests import Response # type: ignore[import] -from requests.adapters import HTTPAdapter # type: ignore[import] +from requests import Response +from requests.adapters import HTTPAdapter from typing_extensions import Self -from urllib3.util.retry import Retry # type: ignore[import] +from urllib3.util.retry import Retry from infrahub_sync import ( DiffSyncMixin, @@ -26,9 +26,6 @@ from .utils import get_value -if TYPE_CHECKING: - import builtins - logger = logging.getLogger(__name__) @@ -233,7 +230,7 @@ def _create_aci_client(self, adapter: SyncAdapter) -> AciApiClient: verify=verify, ) - def model_loader(self, model_name: str, model: builtins.type[AciModel]) -> None: # type: ignore[valid-type] + def model_loader(self, model_name: str, model: type[AciModel]) -> None: """ Load and process models using schema mapping filters and transformations. @@ -270,10 +267,10 @@ def model_loader(self, model_name: str, model: builtins.type[AciModel]) -> None: is_source_adapter = self.config.source.name.lower() == "aci" if is_source_adapter: # Filter records - filtered_objs = model.filter_records(records=objs, schema_mapping=element) # type: ignore[attr-defined] + filtered_objs = model.filter_records(records=objs, schema_mapping=element) logger.info("%s: Loading %d/%d %s", self.type, len(filtered_objs), total, resource_name) # Transform records - transformed_objs = model.transform_records(records=filtered_objs, schema_mapping=element) # type: ignore[attr-defined] + transformed_objs = model.transform_records(records=filtered_objs, schema_mapping=element) else: logger.info("%s: Loading all %d %s", self.type, total, resource_name) transformed_objs = objs @@ -281,14 +278,14 @@ def model_loader(self, model_name: str, model: builtins.type[AciModel]) -> None: # Create model instances after filtering and transforming for obj in transformed_objs: data = self.obj_to_diffsync(obj=obj, mapping=element, model=model) - item = model(**data) # type: ignore[misc] + item = model(**data) self.add(item) def obj_to_diffsync( self, obj: dict[str, Any], mapping: SchemaMappingModel, - model: builtins.type[AciModel], # type: ignore[valid-type] + model: type[AciModel], ) -> dict[str, Any]: """Convert an object to DiffSync format based on the provided mapping schema.""" obj_id = self._extract_aci_id(obj) @@ -298,7 +295,7 @@ def obj_to_diffsync( if not mapping.fields: return data for field in mapping.fields: - field_is_list = model.is_list(name=field.name) # type: ignore[attr-defined] + field_is_list = model.is_list(name=field.name) if field.static: data[field.name] = field.static diff --git a/infrahub_sync/adapters/genericrestapi.py b/infrahub_sync/adapters/genericrestapi.py index a4a35e5..fa0cc5c 100644 --- a/infrahub_sync/adapters/genericrestapi.py +++ b/infrahub_sync/adapters/genericrestapi.py @@ -1,16 +1,11 @@ from __future__ import annotations -import os -from typing import TYPE_CHECKING, Any - -try: - from typing import Self -except ImportError: - from typing_extensions import Self - import logging +import os +from typing import Any from diffsync import Adapter, DiffSyncModel +from typing_extensions import Self from infrahub_sync import ( DiffSyncMixin, @@ -25,9 +20,6 @@ logger = logging.getLogger(__name__) -if TYPE_CHECKING: - from collections.abc import Mapping - class GenericrestapiAdapter(DiffSyncMixin, Adapter): """ @@ -134,7 +126,7 @@ def _create_rest_client(self, settings: dict) -> RestApiClient: verify=verify_ssl, ) - def model_loader(self, model_name: str, model: GenericrestapiModel) -> None: + def model_loader(self, model_name: str, model: type[GenericrestapiModel]) -> None: """ Load and process models using schema mapping filters and transformations. @@ -165,7 +157,8 @@ def model_loader(self, model_name: str, model: GenericrestapiModel) -> None: raise ValueError(msg) from exc total = len(objs) - if self.config.source.name.title() == self.type.title(): + adapter_type_title = (self.type or "").title() + if self.config.source.name.title() == adapter_type_title: # Filter records filtered_objs = model.filter_records(records=objs, schema_mapping=element) logger.info("%s: Loading %d/%d %s", self.type, len(filtered_objs), total, resource_name) @@ -206,17 +199,16 @@ def _extract_objects_from_response( # Try to get data using the response key objs = response_data.get(response_key, response_data.get(resource_name, {})) - # Handle different response formats + # Filter each branch to dicts so `obj_to_diffsync` (which calls `.get(...)`) never sees non-dict items. if isinstance(objs, dict): - # If it's a dict, convert values to list (like Observium) - objs = list(objs.values()) - elif not isinstance(objs, list): - # If it's neither dict nor list, wrap in list - objs = [objs] if objs else [] - - return objs - - def obj_to_diffsync(self, obj: dict[str, Any], mapping: SchemaMappingModel, model: GenericrestapiModel) -> dict: + return [v for v in objs.values() if isinstance(v, dict)] + if isinstance(objs, list): + return [v for v in objs if isinstance(v, dict)] + return [] + + def obj_to_diffsync( + self, obj: dict[str, Any], mapping: SchemaMappingModel, model: type[GenericrestapiModel] + ) -> dict: """ Convert an object to DiffSync format. @@ -257,7 +249,7 @@ def obj_to_diffsync(self, obj: dict[str, Any], mapping: SchemaMappingModel, mode if isinstance(node, dict): matching_nodes = [] node_id = node.get("id", None) - matching_nodes = [item for item in nodes if item.local_id == str(node_id)] + matching_nodes = [item for item in nodes if item.local_id == str(node_id)] # ty: ignore[unresolved-attribute] if len(matching_nodes) == 0: msg = f"Unable to locate the node {model} {node_id}" raise IndexError(msg) @@ -269,7 +261,7 @@ def obj_to_diffsync(self, obj: dict[str, Any], mapping: SchemaMappingModel, mode else: data[field.name] = [] - for node in get_value(obj, field.mapping): + for node in get_value(obj, field.mapping) or []: if not node: continue node_id = node.get("id", None) @@ -277,7 +269,7 @@ def obj_to_diffsync(self, obj: dict[str, Any], mapping: SchemaMappingModel, mode node_id = node[1] if node[0] == "id" else None if not node_id: continue - matching_nodes = [item for item in nodes if item.local_id == str(node_id)] + matching_nodes = [item for item in nodes if item.local_id == str(node_id)] # ty: ignore[unresolved-attribute] if len(matching_nodes) == 0: msg = f"Unable to locate the node {field.reference} {node_id}" raise IndexError(msg) @@ -296,8 +288,8 @@ class GenericrestapiModel(DiffSyncModelMixin, DiffSyncModel): def create( cls, adapter: Adapter, - ids: Mapping[Any, Any], - attrs: Mapping[Any, Any], + ids: dict[Any, Any], + attrs: dict[Any, Any], ) -> Self | None: # TODO: To implement return super().create(adapter=adapter, ids=ids, attrs=attrs) diff --git a/infrahub_sync/adapters/infrahub.py b/infrahub_sync/adapters/infrahub.py index 188f6f4..6f351f2 100644 --- a/infrahub_sync/adapters/infrahub.py +++ b/infrahub_sync/adapters/infrahub.py @@ -6,14 +6,6 @@ import os from typing import TYPE_CHECKING, Any -from infrahub_sdk.schema.main import GenericSchemaAPI, NodeSchema, RelationshipSchemaAPI - -try: - from typing import Self -except ImportError: - from typing_extensions import Self - - from diffsync import Adapter, DiffSyncModel from infrahub_sdk import ( Config, @@ -21,7 +13,9 @@ ) from infrahub_sdk.exceptions import NodeNotFoundError from infrahub_sdk.node.property import NodeProperty +from infrahub_sdk.schema.main import GenericSchemaAPI, NodeSchemaAPI, RelationshipSchemaAPI from infrahub_sdk.utils import compare_lists +from typing_extensions import Self from infrahub_sync import ( DiffSyncMixin, @@ -84,7 +78,7 @@ def resolve_peer_node( if peer_node and fallback and client and not _node_has_complete_attributes(peer_node): peer_node = client.get(id=key, kind=peer_node.get_kind(), populate_store=True) - if not peer_node and fallback: + if not peer_node and fallback and client is not None: logger.warning("Unable to find %s [%s] in Store - Fallback to Infrahub", rel_schema.peer, key) peer_node = client.get(id=key, kind=rel_schema.peer, populate_store=True) if not peer_node: @@ -122,8 +116,8 @@ def update_node( if attr_name in node._schema.relationship_names: for rel_schema in node._schema.relationships: - peer_schema: MainSchemaTypesAPI = schemas.get(rel_schema.peer) - if attr_name != rel_schema.name: + peer_schema = schemas.get(rel_schema.peer) + if attr_name != rel_schema.name or peer_schema is None: continue if rel_schema.cardinality == "one": @@ -179,7 +173,7 @@ def diffsync_to_infrahub( ids: Mapping[Any, Any], attrs: Mapping[Any, Any], store: NodeStoreSync, - node_schema: NodeSchema, + node_schema: NodeSchemaAPI, schemas: Mapping[str, MainSchemaTypesAPI], ) -> dict[Any, Any]: """ @@ -193,8 +187,8 @@ def diffsync_to_infrahub( for key in list(data.keys()): if key in node_schema.relationship_names: for rel_schema in node_schema.relationships: - peer_schema: MainSchemaTypesAPI = schemas.get(rel_schema.peer) - if key != rel_schema.name: + peer_schema = schemas.get(rel_schema.peer) + if key != rel_schema.name or peer_schema is None: continue if rel_schema.cardinality == "one": @@ -300,7 +294,7 @@ def __init__( # We will keep a copy of the schema self.schema: MutableMapping[str, MainSchemaTypesAPI] = self.client.schema.all(branch=infrahub_branch) - def model_loader(self, model_name: str, model: InfrahubModel) -> None: + def model_loader(self, model_name: str, model: type[InfrahubModel]) -> None: """ Load and process models using schema mapping filters and transformations. @@ -310,7 +304,7 @@ def model_loader(self, model_name: str, model: InfrahubModel) -> None: element = next((el for el in self.config.schema_mapping if el.name == model_name), None) if element: # Retrieve all nodes corresponding to model_name (list of InfrahubNodeSync) - nodes = self.client.all(kind=model_name, include=model._attributes, populate_store=True) + nodes = self.client.all(kind=model_name, include=list(model._attributes), populate_store=True) # Transform the list of InfrahubNodeSync into a list of (node, dict) tuples node_dict_pairs = [(node, self.infrahub_node_to_diffsync(node=node)) for node in nodes] @@ -319,7 +313,7 @@ def model_loader(self, model_name: str, model: InfrahubModel) -> None: # Extract the list of dicts for filtering and transforming list_obj = [pair[1] for pair in node_dict_pairs] - if self.config.source.name.title() == self.type.title(): + if self.config.source.name.title() == self.type.title(): # ty: ignore[unresolved-attribute] # Filter records filtered_objs = model.filter_records(records=list_obj, schema_mapping=element) logger.info("%s: Loading %d/%d %s", self.type, len(filtered_objs), total, model_name) @@ -361,7 +355,9 @@ def infrahub_node_to_diffsync(self, node: InfrahubNodeSync) -> dict[str, Any]: for rel_schema in node._schema.relationships: if not has_field(config=self.config, name=node._schema.kind, field=rel_schema.name): continue - peer_schema: MainSchemaTypesAPI = self.schema.get(rel_schema.peer) + peer_schema = self.schema.get(rel_schema.peer) + if peer_schema is None: + continue if rel_schema.cardinality == "one": rel: RelatedNodeSync = getattr(node, rel_schema.name) @@ -449,11 +445,18 @@ class InfrahubModel(DiffSyncModelMixin, DiffSyncModel): @classmethod def create( cls, - adapter: InfrahubAdapter, - ids: Mapping[Any, Any], - attrs: Mapping[Any, Any], + adapter: Adapter, + ids: dict[Any, Any], + attrs: dict[Any, Any], ) -> Self | None: + if not isinstance(adapter, InfrahubAdapter): + msg = f"{cls.__name__}.create expected an InfrahubAdapter, got {type(adapter).__name__}" + raise TypeError(msg) node_schema = adapter.client.schema.get(kind=cls.__name__) + # client.schema.get() returns the wider MainSchemaTypesAPI; diffsync_to_infrahub needs NodeSchemaAPI. + if not isinstance(node_schema, NodeSchemaAPI): + msg = f"Expected NodeSchemaAPI for {cls.__name__}, got {type(node_schema).__name__}" + raise TypeError(msg) data = diffsync_to_infrahub( ids=ids, attrs=attrs, node_schema=node_schema, store=adapter.client.store, schemas=adapter.schema ) @@ -470,9 +473,13 @@ def create( return super().create(adapter=adapter, ids=ids, attrs=attrs) def update(self, attrs: dict) -> Self | None: - node = self.adapter.client.get(id=self.local_id, kind=self.__class__.__name__) - source_id = self.adapter.source_node.id if self.adapter.source_node else None - owner_id = self.adapter.owner_node.id if self.adapter.owner_node else None + adapter = self.adapter + if not isinstance(adapter, InfrahubAdapter): + msg = f"{self.__class__.__name__}.update expected an InfrahubAdapter, got {type(adapter).__name__}" + raise TypeError(msg) + node = adapter.client.get(id=self.local_id, kind=self.__class__.__name__) + source_id = adapter.source_node.id if adapter.source_node else None + owner_id = adapter.owner_node.id if adapter.owner_node else None node = update_node(node=node, attrs=attrs, source=source_id, owner=owner_id) node.save(allow_upsert=True) diff --git a/infrahub_sync/adapters/ipfabricsync.py b/infrahub_sync/adapters/ipfabricsync.py index 899c86a..0d23f22 100644 --- a/infrahub_sync/adapters/ipfabricsync.py +++ b/infrahub_sync/adapters/ipfabricsync.py @@ -3,14 +3,10 @@ import json import logging import os -from typing import TYPE_CHECKING, Any - -try: - from typing import Self -except ImportError: - from typing_extensions import Self +from typing import Any, ClassVar from diffsync import Adapter, DiffSyncModel +from typing_extensions import Self from infrahub_sync import ( DiffSyncMixin, @@ -24,14 +20,11 @@ logger = logging.getLogger(__name__) try: - from ipfabric import IPFClient + from ipfabric import IPFClient # ty: ignore[unresolved-import] # optional dep, see pyproject extras except ImportError: logger.exception("Failed to import ipfabric") raise -if TYPE_CHECKING: - from collections.abc import Mapping - ipf_filters = { "tables/inventory/summary/platforms": {"and": [{"platform": ["empty", False]}]}, "tables/inventory/summary/models": {"and": [{"model": ["empty", False]}]}, @@ -40,7 +33,7 @@ class IpfabricsyncAdapter(DiffSyncMixin, Adapter): - type = "IPFabricsync" + type: ClassVar[str] = "IPFabricsync" def __init__(self, target: str, adapter: SyncAdapter, config: SyncConfig, *args, **kwargs) -> None: super().__init__(*args, **kwargs) @@ -68,7 +61,7 @@ def _create_ipfabric_client(self, adapter: SyncAdapter) -> IPFClient: return IPFClient(**settings) - def model_loader(self, model_name: str, model: IpfabricsyncModel) -> None: + def model_loader(self, model_name: str, model: type[IpfabricsyncModel]) -> None: """ Load and process models using schema mapping filters and transformations. @@ -84,7 +77,7 @@ def model_loader(self, model_name: str, model: IpfabricsyncModel) -> None: table = self.client.fetch_all(element.mapping, filters=ipf_filters.get(element.mapping)) total = len(table) - if self.config.source.name.title() == self.type.title(): + if self.config.source.name.title() == self.type.title(): # ty: ignore[unresolved-attribute] # Filter records filtered_objs = model.filter_records(records=table, schema_mapping=element) logger.info("%s: Loading %d/%d %s", self.type, len(filtered_objs), total, element.mapping) @@ -99,7 +92,7 @@ def model_loader(self, model_name: str, model: IpfabricsyncModel) -> None: item = model(**data) self.update_or_add_model_instance(item) - def ipfabric_dict_to_diffsync(self, obj: dict, mapping: SchemaMappingModel, model: IpfabricsyncModel) -> dict: # pylint: disable=too-many-branches + def ipfabric_dict_to_diffsync(self, obj: dict, mapping: SchemaMappingModel, model: type[IpfabricsyncModel]) -> dict: # pylint: disable=too-many-branches data: dict[str, Any] = {"local_id": str(obj["id"])} for field in mapping.fields: # pylint: disable=too-many-nested-blocks @@ -163,8 +156,8 @@ class IpfabricsyncModel(DiffSyncModelMixin, DiffSyncModel): def create( cls, adapter: Adapter, - ids: Mapping[Any, Any], - attrs: Mapping[Any, Any], + ids: dict[Any, Any], + attrs: dict[Any, Any], ) -> Self | None: # TODO: To Implement return super().create(adapter=adapter, ids=ids, attrs=attrs) diff --git a/infrahub_sync/adapters/nautobot.py b/infrahub_sync/adapters/nautobot.py index 10d2b5d..0c26f7c 100644 --- a/infrahub_sync/adapters/nautobot.py +++ b/infrahub_sync/adapters/nautobot.py @@ -3,15 +3,11 @@ # pylint: disable=R0801 import logging import os -from typing import TYPE_CHECKING, Any +from typing import Any -try: - from typing import Self -except ImportError: - from typing_extensions import Self - -import pynautobot +import pynautobot # ty: ignore[unresolved-import] # optional dep, see pyproject extras from diffsync import Adapter, DiffSyncModel +from typing_extensions import Self from infrahub_sync import ( DiffSyncMixin, @@ -25,9 +21,6 @@ logger = logging.getLogger(__name__) -if TYPE_CHECKING: - from collections.abc import Mapping - class NautobotAdapter(DiffSyncMixin, Adapter): type = "Nautobot" @@ -52,7 +45,7 @@ def _create_nautobot_client(self, adapter: SyncAdapter) -> pynautobot.api: client = pynautobot.api(url=url, token=token, threading=True, max_workers=5, retries=3, verify=verify_ssl) return client - def model_loader(self, model_name: str, model: NautobotModel) -> None: + def model_loader(self, model_name: str, model: type[NautobotModel]) -> None: """ Load and process models using schema mapping filters and transformations. @@ -82,7 +75,7 @@ def model_loader(self, model_name: str, model: NautobotModel) -> None: list_obj.append(dict(node)) total = len(list_obj) - if self.config.source.name.title() == self.type.title(): + if self.config.source.name.title() == self.type.title(): # ty: ignore[unresolved-attribute] # Filter records filtered_objs = model.filter_records(records=list_obj, schema_mapping=element) logger.info("%s: Loading %d/%d %s", self.type, len(filtered_objs), total, resource_name) @@ -98,7 +91,9 @@ def model_loader(self, model_name: str, model: NautobotModel) -> None: item = model(**data) self.add(item) - def nautobot_obj_to_diffsync(self, obj: dict[str, Any], mapping: SchemaMappingModel, model: NautobotModel) -> dict: + def nautobot_obj_to_diffsync( + self, obj: dict[str, Any], mapping: SchemaMappingModel, model: type[NautobotModel] + ) -> dict: obj_id = obj.get("id") data: dict[str, Any] = {"local_id": str(obj_id)} @@ -129,7 +124,7 @@ def nautobot_obj_to_diffsync(self, obj: dict[str, Any], mapping: SchemaMappingMo matching_nodes = [] node_id = node.get("id", None) if node_id: - matching_nodes = [item for item in nodes if item.local_id == str(node_id)] + matching_nodes = [item for item in nodes if item.local_id == str(node_id)] # ty: ignore[unresolved-attribute] if len(matching_nodes) == 0: # TODO: If the peer is a Node we are filtering, we could end up not finding it logger.warning("Unable to locate the node %s %s", field.name, node_id) @@ -139,7 +134,7 @@ def nautobot_obj_to_diffsync(self, obj: dict[str, Any], mapping: SchemaMappingMo else: data[field.name] = [] - for node in get_value(obj, field.mapping): + for node in get_value(obj, field.mapping) or []: if not node: continue node_id = node.get("id", None) @@ -147,7 +142,7 @@ def nautobot_obj_to_diffsync(self, obj: dict[str, Any], mapping: SchemaMappingMo node_id = node[1] if node[0] == "id" else None if not node_id: continue - matching_nodes = [item for item in nodes if item.local_id == str(node_id)] + matching_nodes = [item for item in nodes if item.local_id == str(node_id)] # ty: ignore[unresolved-attribute] if len(matching_nodes) == 0: # TODO: If the peer is a Node we are filtering, we could end up not finding it logger.warning("Unable to locate the node %s %s", field.name, node_id) @@ -163,8 +158,8 @@ class NautobotModel(DiffSyncModelMixin, DiffSyncModel): def create( cls, adapter: Adapter, - ids: Mapping[Any, Any], - attrs: Mapping[Any, Any], + ids: dict[Any, Any], + attrs: dict[Any, Any], ) -> Self | None: # TODO: To implement return super().create(adapter=adapter, ids=ids, attrs=attrs) diff --git a/infrahub_sync/adapters/netbox.py b/infrahub_sync/adapters/netbox.py index 5c078cc..db2f5e6 100644 --- a/infrahub_sync/adapters/netbox.py +++ b/infrahub_sync/adapters/netbox.py @@ -3,16 +3,12 @@ # pylint: disable=R0801 import logging import os -from typing import TYPE_CHECKING, Any +from typing import Any -from requests import Session - -try: - from typing import Self -except ImportError: - from typing_extensions import Self -import pynetbox +import pynetbox # ty: ignore[unresolved-import] # optional dep, see pyproject extras from diffsync import Adapter, DiffSyncModel +from requests import Session +from typing_extensions import Self from infrahub_sync import ( DiffSyncMixin, @@ -26,9 +22,6 @@ logger = logging.getLogger(__name__) -if TYPE_CHECKING: - from collections.abc import Mapping - class NetboxAdapter(DiffSyncMixin, Adapter): type = "Netbox" @@ -57,7 +50,7 @@ def _create_netbox_client(self, adapter: SyncAdapter) -> pynetbox.api: client.http_session = session return client - def model_loader(self, model_name: str, model: NetboxModel) -> None: + def model_loader(self, model_name: str, model: type[NetboxModel]) -> None: """ Load and process models using schema mapping filters and transformations. @@ -86,7 +79,7 @@ def model_loader(self, model_name: str, model: NetboxModel) -> None: list_obj.append(dict(node)) total = len(list_obj) - if self.config.source.name.title() == self.type.title(): + if self.config.source.name.title() == self.type.title(): # ty: ignore[unresolved-attribute] # Filter records filtered_objs = model.filter_records(records=list_obj, schema_mapping=element) logger.info("%s: Loading %d/%d %s", self.type, len(filtered_objs), total, resource_name) @@ -102,7 +95,9 @@ def model_loader(self, model_name: str, model: NetboxModel) -> None: item = model(**data) self.add(item) - def netbox_obj_to_diffsync(self, obj: dict[str, Any], mapping: SchemaMappingModel, model: NetboxModel) -> dict: + def netbox_obj_to_diffsync( + self, obj: dict[str, Any], mapping: SchemaMappingModel, model: type[NetboxModel] + ) -> dict: obj_id = obj.get("id") data: dict[str, Any] = {"local_id": str(obj_id)} @@ -134,7 +129,7 @@ def netbox_obj_to_diffsync(self, obj: dict[str, Any], mapping: SchemaMappingMode if isinstance(node, dict): matching_nodes = [] node_id = node.get("id", None) - matching_nodes = [item for item in nodes if item.local_id == str(node_id)] + matching_nodes = [item for item in nodes if item.local_id == str(node_id)] # ty: ignore[unresolved-attribute] if len(matching_nodes) == 0: msg = f"Unable to locate the node {field.name} {node_id}" raise IndexError(msg) @@ -144,7 +139,7 @@ def netbox_obj_to_diffsync(self, obj: dict[str, Any], mapping: SchemaMappingMode data[field.name] = node else: data[field.name] = [] - for node in get_value(obj, field.mapping): + for node in get_value(obj, field.mapping) or []: if not node: continue node_id = node.get("id", None) @@ -152,7 +147,7 @@ def netbox_obj_to_diffsync(self, obj: dict[str, Any], mapping: SchemaMappingMode node_id = node[1] if node[0] == "id" else None if not node_id: continue - matching_nodes = [item for item in nodes if item.local_id == str(node_id)] + matching_nodes = [item for item in nodes if item.local_id == str(node_id)] # ty: ignore[unresolved-attribute] if len(matching_nodes) == 0: msg = f"Unable to locate the node {field.reference} {node_id}" raise IndexError(msg) @@ -167,8 +162,8 @@ class NetboxModel(DiffSyncModelMixin, DiffSyncModel): def create( cls, adapter: Adapter, - ids: Mapping[Any, Any], - attrs: Mapping[Any, Any], + ids: dict[Any, Any], + attrs: dict[Any, Any], ) -> Self | None: # TODO: To implement return super().create(adapter=adapter, ids=ids, attrs=attrs) diff --git a/infrahub_sync/adapters/peeringmanager.py b/infrahub_sync/adapters/peeringmanager.py index 189faf9..4314030 100644 --- a/infrahub_sync/adapters/peeringmanager.py +++ b/infrahub_sync/adapters/peeringmanager.py @@ -2,18 +2,12 @@ from typing import TYPE_CHECKING, Any -try: - from typing import Self -except ImportError: - from typing_extensions import Self - import requests +from typing_extensions import Self from infrahub_sync.adapters.genericrestapi import GenericrestapiAdapter, GenericrestapiModel if TYPE_CHECKING: - from collections.abc import Mapping - from diffsync import Adapter from infrahub_sync import ( @@ -54,8 +48,8 @@ class PeeringmanagerModel(GenericrestapiModel): def create( cls, adapter: Adapter, - ids: Mapping[Any, Any], - attrs: Mapping[Any, Any], + ids: dict[Any, Any], + attrs: dict[Any, Any], ) -> Self | None: # TODO: To implement return super().create(adapter=adapter, ids=ids, attrs=attrs) @@ -68,8 +62,10 @@ def update(self, attrs: dict) -> Self | None: based on the schema mapping configuration, and sends an update request to the API endpoint of the object. """ - # Determine the resource name using the schema mapping - resource_name = self.__class__.get_resource_name(schema_mapping=self.adapter.config.schema_mapping) + adapter = self.adapter + assert adapter is not None + # `adapter` is typed as the base diffsync.Adapter; `config` and `client` come from the concrete subclass. + resource_name = self.__class__.get_resource_name(schema_mapping=adapter.config.schema_mapping) # ty: ignore[unresolved-attribute] # Determine the unique identifier for the API request unique_identifier = self.local_id if hasattr(self, "local_id") else self.get_unique_id() @@ -77,7 +73,7 @@ def update(self, attrs: dict) -> Self | None: # Map incoming attributes to the target attributes based on schema mapping mapped_attrs: dict[str, Any] = {} - for field in self.adapter.config.schema_mapping: + for field in adapter.config.schema_mapping: # ty: ignore[unresolved-attribute] if field.name == self.__class__.get_type(): for field_mapping in field.fields: # Map source field name to target field name @@ -87,14 +83,14 @@ def update(self, attrs: dict) -> Self | None: # Check if the field is a relationship if field_mapping.reference: - all_nodes_for_reference = self.adapter.store.get_all(model=field_mapping.reference) + all_nodes_for_reference = adapter.store.get_all(model=field_mapping.reference) if isinstance(value, list): # For lists, filter nodes to match the unique IDs in the attribute value filtered_nodes = [ node for node in all_nodes_for_reference if node.get_unique_id() in value ] - mapped_attrs[target_field_name] = [node.local_id for node in filtered_nodes] + mapped_attrs[target_field_name] = [node.local_id for node in filtered_nodes] # ty: ignore[unresolved-attribute] else: # For single references, find the matching node filtered_node = next( @@ -102,13 +98,13 @@ def update(self, attrs: dict) -> Self | None: None, ) if filtered_node: - mapped_attrs[target_field_name] = filtered_node.local_id + mapped_attrs[target_field_name] = filtered_node.local_id # ty: ignore[unresolved-attribute] else: mapped_attrs[target_field_name] = value # Attempt to send the update request to the API try: - self.adapter.client.patch(endpoint, data=mapped_attrs) + adapter.client.patch(endpoint, data=mapped_attrs) # ty: ignore[unresolved-attribute] return super().update(attrs) except (requests.exceptions.HTTPError, ConnectionError) as exc: msg = f"Error during update: {exc!s}" diff --git a/infrahub_sync/adapters/prometheus.py b/infrahub_sync/adapters/prometheus.py index fdcae8c..3da9e11 100644 --- a/infrahub_sync/adapters/prometheus.py +++ b/infrahub_sync/adapters/prometheus.py @@ -3,17 +3,14 @@ import logging import os import re -from typing import TYPE_CHECKING, Any - -from prometheus_client.parser import text_string_to_metric_families - -try: - from typing import Self -except ImportError: - from typing_extensions import Self +from typing import Any import requests from diffsync import Adapter, DiffSyncModel +from prometheus_client.parser import ( # ty: ignore[unresolved-import] # optional dep, see pyproject extras + text_string_to_metric_families, +) +from typing_extensions import Self from infrahub_sync import ( DiffSyncMixin, @@ -47,8 +44,6 @@ def _derive_identifier_key(obj: dict) -> str: return f"{name}|{key}" if key else name -if TYPE_CHECKING: - from collections.abc import Mapping # ----------------------------------------------------------------------------------------------------------- @@ -434,10 +429,11 @@ def __init__(self, target: str, adapter: SyncAdapter, config: SyncConfig, *args, self._lookup: LookupResolver | None = None def _ensure_samples(self) -> dict[str, list[dict[str, Any]]]: - if self._samples_by_metric is not None: + cached = self._samples_by_metric + if cached is not None: if self._lookup is None: - self._lookup = LookupResolver(samples_by_metric=self._samples_by_metric) - return self._samples_by_metric + self._lookup = LookupResolver(samples_by_metric=cached) + return cached if isinstance(self.client, PrometheusScrapeClient): self._samples_by_metric = self.client.get_metrics(params=self.params) @@ -446,7 +442,7 @@ def _ensure_samples(self) -> dict[str, list[dict[str, Any]]]: store: dict[str, list[dict[str, Any]]] = {} for resource_name, query in self.promql_resources.items(): try: - results = self.client.instant_query(query) # type: ignore[attr-defined] + results = self.client.instant_query(query) except Exception as exc: msg = f"Prometheus API query failed for '{resource_name}': {exc!s}" raise ValueError(msg) from exc @@ -461,8 +457,10 @@ def _ensure_samples(self) -> dict[str, list[dict[str, Any]]]: self._samples_by_metric = store # init lookup resolver - self._lookup = LookupResolver(samples_by_metric=self._samples_by_metric) - return self._samples_by_metric + assert self._samples_by_metric is not None + samples = self._samples_by_metric + self._lookup = LookupResolver(samples_by_metric=samples) + return samples # ---- DiffSync hooks ---- @@ -483,17 +481,18 @@ def model_loader(self, model_name: str, model) -> None: # Inject a callable 'lookup' into each record so Jinja transforms can use it. # NOTE: must be done BEFORE transforms. - if self._lookup: + lookup = self._lookup + if lookup is not None: for obj in objs: # bind current obj into the callable - def _mk_lookup(current: dict[str, Any]): - return lambda metric, key_or_path, value_path, default=None: self._lookup.resolve_fn( + def _mk_lookup(current: dict[str, Any], _lookup: LookupResolver = lookup): + return lambda metric, key_or_path, value_path, default=None: _lookup.resolve_fn( current, metric, key_or_path, value_path, default ) obj["lookup"] = _mk_lookup(obj) - if self.config.source.name.title() == self.type.title(): + if self.config.source.name.title() == self.type.title(): # ty: ignore[unresolved-attribute] filtered_objs = model.filter_records(records=objs, schema_mapping=element) transformed_objs = model.transform_records(records=filtered_objs, schema_mapping=element) logger.info( @@ -556,8 +555,8 @@ class PrometheusModel(DiffSyncModelMixin, DiffSyncModel): def create( cls, adapter: Adapter, - ids: Mapping[Any, Any], - attrs: Mapping[Any, Any], + ids: dict[Any, Any], + attrs: dict[Any, Any], ) -> Self | None: # TODO: To implement return super().create(adapter=adapter, ids=ids, attrs=attrs) diff --git a/infrahub_sync/adapters/slurpitsync.py b/infrahub_sync/adapters/slurpitsync.py index 063ef09..e797194 100644 --- a/infrahub_sync/adapters/slurpitsync.py +++ b/infrahub_sync/adapters/slurpitsync.py @@ -3,15 +3,11 @@ import asyncio import ipaddress import logging -from typing import TYPE_CHECKING, Any +from typing import Any -try: - from typing import Self -except ImportError: - from typing_extensions import Self - -import slurpit +import slurpit # ty: ignore[unresolved-import] # optional dep, see pyproject extras from diffsync import Adapter, DiffSyncModel +from typing_extensions import Self from infrahub_sync import ( DiffSyncMixin, @@ -24,9 +20,6 @@ logger = logging.getLogger(__name__) -if TYPE_CHECKING: - from collections.abc import Mapping - # Create a new event loop for running async functions synchronously loop = asyncio.new_event_loop() @@ -171,7 +164,8 @@ def planning_results(self, planning_name): results = self.run_async(self.client.planning.search_plannings(search_data, limit=30000)) return results or [] - def model_loader(self, model_name: str, model: SlurpitsyncModel) -> None: + def model_loader(self, model_name: str, model: type[SlurpitsyncModel]) -> None: + """Fetch `model_name` from Slurpit, apply schema-mapping filters/transforms, and add each row to the store.""" for element in self.config.schema_mapping: if element.name != model_name: continue @@ -203,7 +197,7 @@ def model_loader(self, model_name: str, model: SlurpitsyncModel) -> None: list_obj.append(node) total = len(list_obj) - if self.config.source.name.title() == self.type.title(): + if self.config.source.name.title() == self.type.title(): # ty: ignore[unresolved-attribute] # Filter records filtered_objs = model.filter_records(records=list_obj, schema_mapping=element) logger.info("%s: Loading %d/%d %s", self.type, len(filtered_objs), total, element.mapping) @@ -225,8 +219,9 @@ def model_loader(self, model_name: str, model: SlurpitsyncModel) -> None: logger.info("%s: skipped syncing %d models", self.type, len(self.skipped)) def slurpit_obj_to_diffsync( - self, obj: dict[str, Any], mapping: SchemaMappingModel, model: SlurpitsyncModel - ) -> dict: + self, obj: dict[str, Any], mapping: SchemaMappingModel, model: type[SlurpitsyncModel] + ) -> dict | None: + """Convert a Slurpit dict to a DiffSync-ready payload, or None when a required reference is missing.""" obj_id = obj.get("id") data: dict[str, Any] = {"local_id": str(obj_id)} @@ -287,12 +282,14 @@ class SlurpitsyncModel(DiffSyncModelMixin, DiffSyncModel): def create( cls, adapter: Adapter, - ids: Mapping[Any, Any], - attrs: Mapping[Any, Any], + ids: dict[Any, Any], + attrs: dict[Any, Any], ) -> Self | None: + """Create a Slurpit-side record from DiffSync ids/attrs (not yet implemented; delegates to the base).""" # TODO: To implement return super().create(adapter=adapter, ids=ids, attrs=attrs) def update(self, attrs: dict) -> Self | None: + """Update a Slurpit-side record from DiffSync attrs (not yet implemented; delegates to the base).""" # TODO: To implement return super().update(attrs) diff --git a/infrahub_sync/adapters/utils.py b/infrahub_sync/adapters/utils.py index 6aefdbd..61af6fc 100644 --- a/infrahub_sync/adapters/utils.py +++ b/infrahub_sync/adapters/utils.py @@ -3,15 +3,14 @@ from typing import TYPE_CHECKING, Any if TYPE_CHECKING: - from diffsync import Adapter + from infrahub_sync import DiffSyncMixin -def build_mapping(adapter: Adapter, reference: str, obj, field) -> str: +def build_mapping(adapter: DiffSyncMixin, reference: str, obj, field) -> str: """This is used when references are encountered to attempt to resolve them for mapping.""" # Get object class and model name from the store object_class, modelname = adapter.store._get_object_class_and_model(model=reference) - # Find the schema element matching the model name schema_element = next( (element for element in adapter.config.schema_mapping if element.name == modelname), None, @@ -29,8 +28,8 @@ def build_mapping(adapter: Adapter, reference: str, obj, field) -> str: # Convert schema_element.fields to a dictionary for fast lookup field_dict = {field.name: field.mapping for field in schema_element.fields} - # Loop through object_class._identifiers to find corresponding field mappings - for identifier in object_class._identifiers: + # `_identifiers` is a private ClassVar on DiffSyncModel subclasses, not exposed on the base. + for identifier in object_class._identifiers: # ty: ignore[unresolved-attribute] if identifier in field_dict: new_identifiers.append(field_dict[identifier]) diff --git a/infrahub_sync/cli.py b/infrahub_sync/cli.py index 706a17f..5d89897 100644 --- a/infrahub_sync/cli.py +++ b/infrahub_sync/cli.py @@ -1,6 +1,9 @@ +from __future__ import annotations + import logging from enum import Enum from timeit import default_timer as timer +from typing import TYPE_CHECKING, NoReturn, cast import typer from infrahub_sdk import InfrahubClientSync @@ -15,6 +18,11 @@ render_adapter, ) +if TYPE_CHECKING: + from collections.abc import MutableMapping + + from infrahub_sdk.schema import GenericSchema, NodeSchema + VERBOSITY_MAP = {"quiet": logging.WARNING, "default": logging.INFO, "verbose": logging.DEBUG} app = typer.Typer() @@ -58,7 +66,7 @@ def main( typer.echo(ctx.get_help()) -def print_error_and_abort(message: str) -> typer.Abort: +def print_error_and_abort(message: str) -> NoReturn: logger.error("%s", message) raise typer.Abort @@ -223,10 +231,12 @@ def generate( except ServerNotResponsiveError as exc: print_error_and_abort(str(exc)) - missing_schema_models = find_missing_schema_model(sync_instance=sync_instance, schema=schema) + # SDK returns *SchemaAPI variants; structurally compatible with the (NodeSchema | GenericSchema) shape utils expects. + typed_schema = cast("MutableMapping[str, NodeSchema | GenericSchema]", schema) + missing_schema_models = find_missing_schema_model(sync_instance=sync_instance, schema=typed_schema) if missing_schema_models: print_error_and_abort(f"One or more model model are not present in the Schema - {missing_schema_models}") - rendered_files = render_adapter(sync_instance=sync_instance, schema=schema) + rendered_files = render_adapter(sync_instance=sync_instance, schema=typed_schema) for template, output_path in rendered_files: logger.info("Rendered template %s to %s", template, output_path) diff --git a/infrahub_sync/generator/__init__.py b/infrahub_sync/generator/__init__.py index 1e40036..be82a79 100644 --- a/infrahub_sync/generator/__init__.py +++ b/infrahub_sync/generator/__init__.py @@ -53,7 +53,7 @@ def has_node(config: SyncConfig, name: str) -> bool: def has_field(config: SyncConfig, name: str, field: str) -> bool: for item in config.schema_mapping: if item.name == name: - for subitem in item.fields: + for subitem in item.fields or []: if subitem.name == field: return True return False @@ -169,6 +169,6 @@ def render_template(template_file: Path, output_dir: Path, output_file: Path, co template = template_env.get_template(str(template_file)) - rendered_tpl = template.render(**context) # type: ignore[arg-type] + rendered_tpl = template.render(**context) output_filename = output_dir / output_file output_filename.write_text(rendered_tpl, encoding="utf-8") diff --git a/infrahub_sync/potenda/__init__.py b/infrahub_sync/potenda/__init__.py index 81ef046..906fee6 100644 --- a/infrahub_sync/potenda/__init__.py +++ b/infrahub_sync/potenda/__init__.py @@ -34,8 +34,9 @@ def __init__( self.source = source self.destination = destination - self.source.top_level = top_level - self.destination.top_level = top_level + # diffsync's `Adapter.top_level` is a ClassVar but the library supports per-instance overrides. + self.source.top_level = top_level # ty: ignore[invalid-attribute-access] + self.destination.top_level = top_level # ty: ignore[invalid-attribute-access] self.partition = partition self.progress_bar = None @@ -45,9 +46,9 @@ def __init__( logging.getLogger("diffsync").setLevel(verbosity) # Combine DiffSyncFlags from the configuration - self.flags = DiffSyncFlags.NONE - for flag in self.config.diffsync_flags: - self.flags |= flag + self.flags: DiffSyncFlags = DiffSyncFlags.NONE + for flag in self.config.diffsync_flags or []: + self.flags |= flag if isinstance(flag, DiffSyncFlags) else DiffSyncFlags[flag] # Fallback to `SKIP_UNMATCHED_DST` if nothing is define if self.flags == DiffSyncFlags.NONE: diff --git a/infrahub_sync/utils.py b/infrahub_sync/utils.py index 3901a81..e3da000 100644 --- a/infrahub_sync/utils.py +++ b/infrahub_sync/utils.py @@ -58,9 +58,9 @@ def render_adapter( for item in files_to_render: render_template( - template_file=item[0], + template_file=Path(item[0]), output_dir=output_dir_path, - output_file=item[1], + output_file=Path(item[1]), context={"schema": schema, "adapter": adapter, "config": sync_instance}, ) output_file_path = output_dir_path / item[1] @@ -147,16 +147,15 @@ def get_instance( return item return None - config_file_path = None - try: - if Path(config_file).is_absolute() or directory is None: - config_file_path = Path(config_file) - elif directory: - config_file_path = Path(directory, config_file) - except TypeError: + if config_file is None: # TODO: Log or raise an Error/Warning return None + # Check `directory is None` (not truthiness) so an empty string still collapses to Path(config_file). + config_file_path: Path = ( + Path(config_file) if Path(config_file).is_absolute() or directory is None else Path(directory, config_file) + ) + if config_file_path: directory_path = config_file_path.parent if config_file_path.is_file(): diff --git a/pyproject.toml b/pyproject.toml index c2f2d95..7c5a72a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,14 +40,13 @@ dev = [ "pre-commit>=4.0,<5.0", "types-toml", "types-ujson", - "types-pyyaml", "typer-cli", "invoke>=2.2.1,<3", - "mypy>=1.15.0,<2", "types-PyYAML>=6.0.12", "ruff==0.15.5", "pytest>=9.0.2,<10", "pytest-xdist>=3.8", + "ty==0.0.32", "types-python-slugify>=8.0.2.20240310", ] @@ -132,12 +131,6 @@ filterwarnings = [ ] addopts = "-vs --cov-report term-missing --cov-report xml --dist loadscope" -[tool.mypy] -pretty = true -ignore_missing_imports = true -disallow_untyped_defs = true -disable_error_code = ["type-abstract"] - [tool.ruff] line-length = 120 @@ -300,3 +293,12 @@ max-complexity = 33 "tasks/**.py" = [ "T201", # print() is standard for invoke task status messages ] + +[tool.ty] + +[tool.ty.src] +exclude = ["examples/**"] + +[tool.ty.environment] +python-version = "3.10" + diff --git a/tasks/__init__.py b/tasks/__init__.py index 95987be..f4b905e 100644 --- a/tasks/__init__.py +++ b/tasks/__init__.py @@ -14,9 +14,9 @@ } } ) -ns.add_collection(linter) -ns.add_collection(docs) -ns.add_collection(tests) +ns.add_collection(Collection.from_module(linter)) +ns.add_collection(Collection.from_module(docs)) +ns.add_collection(Collection.from_module(tests)) @task(name="lint") @@ -49,7 +49,7 @@ def tests_integration(context: Context) -> None: @task(name="generate-doc") def generate_doc(context: Context) -> None: - docs.generate_doc(context) + docs.generate(context) @task(name="docusaurus") diff --git a/tasks/docs.py b/tasks/docs.py index 76b2ed0..481f62b 100644 --- a/tasks/docs.py +++ b/tasks/docs.py @@ -69,5 +69,5 @@ def docusaurus(context: Context) -> None: with context.cd(DOCUMENTATION_DIRECTORY): output = context.run(exec_cmd) - if output.exited != 0: + if output is None or output.exited != 0: sys.exit(-1) diff --git a/tasks/linter.py b/tasks/linter.py index 9673a14..c6b804d 100644 --- a/tasks/linter.py +++ b/tasks/linter.py @@ -16,6 +16,7 @@ def lint_all(context: Context) -> None: lint_ruff(context) lint_pylint(context) lint_yaml(context) + lint_ty(context) print(f" - [{NAMESPACE}] All linter have been executed!") @@ -67,6 +68,17 @@ def lint_yaml(context: Context) -> None: context.run(exec_cmd, pty=True) +@task +def lint_ty(context: Context) -> None: + """Run ty type checker against project files.""" + + print(f" - [{NAMESPACE}] Check code with ty") + exec_cmd = "uv run ty check ." + + with context.cd(ESCAPED_REPO_PATH): + context.run(exec_cmd) + + # ---------------------------------------------------------------------------- # Formatting tasks - Python # ---------------------------------------------------------------------------- diff --git a/tests/__init__.py b/tests/__init__.py index 9c48bcf..38c6c3e 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -2,4 +2,4 @@ from rich import print as rprint -builtins.rprint = rprint # type: ignore +builtins.rprint = rprint # ty: ignore[unresolved-attribute] # injecting rich.print as a test-global helper diff --git a/tests/test_logging.py b/tests/test_logging.py index f441e5c..389e760 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -29,14 +29,17 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None: self.generic_visit(node) self._current_class = old - def visit_FunctionDef(self, node: ast.FunctionDef) -> None: + def _visit_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None: old = self._current_func self._current_func = node.name self.generic_visit(node) self._current_func = old + def visit_FunctionDef(self, node: ast.FunctionDef) -> None: + self._visit_function(node) + def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: - self.visit_FunctionDef(node) # type: ignore[arg-type] + self._visit_function(node) def visit_Call(self, node: ast.Call) -> None: if isinstance(node.func, ast.Name) and node.func.id == "print" and self._current_func != "_print_callback": diff --git a/uv.lock b/uv.lock index a7b751c..220322b 100644 --- a/uv.lock +++ b/uv.lock @@ -498,7 +498,6 @@ dev = [ { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "ipython", version = "9.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "mypy" }, { name = "pre-commit" }, { name = "pylint" }, { name = "pytest" }, @@ -506,6 +505,7 @@ dev = [ { name = "pytest-xdist" }, { name = "requests" }, { name = "ruff" }, + { name = "ty" }, { name = "typer-cli" }, { name = "types-python-slugify" }, { name = "types-pyyaml" }, @@ -520,7 +520,6 @@ requires-dist = [ { name = "infrahub-sdk", extras = ["all"], specifier = ">=1.17,<2" }, { name = "invoke", marker = "extra == 'dev'", specifier = ">=2.2.1,<3" }, { name = "ipython", marker = "extra == 'dev'" }, - { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.15.0,<2" }, { name = "netutils", specifier = ">=1.9,<2.0" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.0,<5.0" }, { name = "pylint", marker = "extra == 'dev'" }, @@ -531,9 +530,9 @@ requires-dist = [ { name = "ruff", marker = "extra == 'dev'", specifier = "==0.15.5" }, { name = "structlog", specifier = ">=25.1,<26.0" }, { name = "tqdm", specifier = ">=4.67" }, + { name = "ty", marker = "extra == 'dev'", specifier = "==0.0.32" }, { name = "typer-cli", marker = "extra == 'dev'" }, { name = "types-python-slugify", marker = "extra == 'dev'", specifier = ">=8.0.2.20240310" }, - { name = "types-pyyaml", marker = "extra == 'dev'" }, { name = "types-pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.12" }, { name = "types-toml", marker = "extra == 'dev'" }, { name = "types-ujson", marker = "extra == 'dev'" }, @@ -678,65 +677,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] -[[package]] -name = "librt" -version = "0.8.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/5f/63f5fa395c7a8a93558c0904ba8f1c8d1b997ca6a3de61bc7659970d66bf/librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc", size = 65697, upload-time = "2026-02-17T16:11:06.903Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e0/0472cf37267b5920eff2f292ccfaede1886288ce35b7f3203d8de00abfe6/librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7", size = 68376, upload-time = "2026-02-17T16:11:08.395Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8bd1359fdcd27ab897cd5963294fa4a7c83b20a8564678e4fd12157e56a5/librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6", size = 197084, upload-time = "2026-02-17T16:11:09.774Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fe/163e33fdd091d0c2b102f8a60cc0a61fd730ad44e32617cd161e7cd67a01/librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0", size = 207337, upload-time = "2026-02-17T16:11:11.311Z" }, - { url = "https://files.pythonhosted.org/packages/01/99/f85130582f05dcf0c8902f3d629270231d2f4afdfc567f8305a952ac7f14/librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b", size = 219980, upload-time = "2026-02-17T16:11:12.499Z" }, - { url = "https://files.pythonhosted.org/packages/6f/54/cb5e4d03659e043a26c74e08206412ac9a3742f0477d96f9761a55313b5f/librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6", size = 212921, upload-time = "2026-02-17T16:11:14.484Z" }, - { url = "https://files.pythonhosted.org/packages/b1/81/a3a01e4240579c30f3487f6fed01eb4bc8ef0616da5b4ebac27ca19775f3/librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71", size = 221381, upload-time = "2026-02-17T16:11:17.459Z" }, - { url = "https://files.pythonhosted.org/packages/08/b0/fc2d54b4b1c6fb81e77288ff31ff25a2c1e62eaef4424a984f228839717b/librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7", size = 216714, upload-time = "2026-02-17T16:11:19.197Z" }, - { url = "https://files.pythonhosted.org/packages/96/96/85daa73ffbd87e1fb287d7af6553ada66bf25a2a6b0de4764344a05469f6/librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05", size = 214777, upload-time = "2026-02-17T16:11:20.443Z" }, - { url = "https://files.pythonhosted.org/packages/12/9c/c3aa7a2360383f4bf4f04d98195f2739a579128720c603f4807f006a4225/librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891", size = 237398, upload-time = "2026-02-17T16:11:22.083Z" }, - { url = "https://files.pythonhosted.org/packages/61/19/d350ea89e5274665185dabc4bbb9c3536c3411f862881d316c8b8e00eb66/librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7", size = 54285, upload-time = "2026-02-17T16:11:23.27Z" }, - { url = "https://files.pythonhosted.org/packages/4f/d6/45d587d3d41c112e9543a0093d883eb57a24a03e41561c127818aa2a6bcc/librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2", size = 61352, upload-time = "2026-02-17T16:11:24.207Z" }, - { url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" }, - { url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" }, - { url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" }, - { url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622, upload-time = "2026-02-17T16:11:28.242Z" }, - { url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304, upload-time = "2026-02-17T16:11:29.344Z" }, - { url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493, upload-time = "2026-02-17T16:11:30.445Z" }, - { url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129, upload-time = "2026-02-17T16:11:32.021Z" }, - { url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113, upload-time = "2026-02-17T16:11:33.192Z" }, - { url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269, upload-time = "2026-02-17T16:11:34.373Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673, upload-time = "2026-02-17T16:11:36.063Z" }, - { url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597, upload-time = "2026-02-17T16:11:37.544Z" }, - { url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733, upload-time = "2026-02-17T16:11:38.691Z" }, - { url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273, upload-time = "2026-02-17T16:11:40.308Z" }, - { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, - { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, - { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, - { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, - { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, - { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, - { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, - { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, - { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, - { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, - { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, - { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, - { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, - { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, - { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, - { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, - { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, - { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, - { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, - { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, -] - [[package]] name = "markdown-it-py" version = "4.0.0" @@ -842,46 +782,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] -[[package]] -name = "mypy" -version = "1.19.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, - { name = "mypy-extensions" }, - { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, - { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, - { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, - { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, - { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, - { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, - { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, - { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, - { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, - { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, - { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, - { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, - { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, - { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, - { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, - { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, - { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, - { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, -] - [[package]] name = "mypy-extensions" version = "1.1.0" @@ -1656,6 +1556,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] +[[package]] +name = "ty" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/7e/2aa791c9ae7b8cd5024cd4122e92267f664ca954cea3def3211919fa3c1f/ty-0.0.32.tar.gz", hash = "sha256:8743174c5f920f6700a4a0c9de140109189192ba16226884cd50095b43b8a45c", size = 5522294, upload-time = "2026-04-20T19:29:01.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/eb/1075dc6a49d7acbe2584ae4d5b410c41b1f177a5adcc567e09eca4c69000/ty-0.0.32-py3-none-linux_armv6l.whl", hash = "sha256:dacbc2f6cd698d488ae7436838ff929570455bf94bfa4d9fe57a630c552aff83", size = 10902959, upload-time = "2026-04-20T19:28:31.907Z" }, + { url = "https://files.pythonhosted.org/packages/33/d2/c35fc8bc66e98d1ee9b0f8ed319bf743e450e1f1e997574b178fab75670f/ty-0.0.32-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914bbc4f605ce2a9e2a78982e28fae1d3359a169d141f9dc3b4c7749cd5eca81", size = 10726172, upload-time = "2026-04-20T19:28:44.765Z" }, + { url = "https://files.pythonhosted.org/packages/96/32/c827da3ca480456fb02d8cea68a2609273b6c220fea0be9a4c8d8470b86e/ty-0.0.32-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4787ac9fe1f86b1f3133f5c6732adbe2df5668b50c679ac6e2d98cd284da812f", size = 10163701, upload-time = "2026-04-20T19:28:27.005Z" }, + { url = "https://files.pythonhosted.org/packages/ba/9e/2734478fbdb90c160cb2813a3916a16a2af5c1e231f87d635f6131d781fb/ty-0.0.32-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ea0a728af99fe40dd744cba6441a2404f80b7f4bde17aa6da393810af5ea57", size = 10656220, upload-time = "2026-04-20T19:29:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/44/9f/0007da2d35e424debe7e9f86ffbc1ab7f60983cfbc5f0411324ab2de5292/ty-0.0.32-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2850561f9b018ae33d7e5bbfa0ac414d3c518513edcffe43877dc9801446b9c5", size = 10696086, upload-time = "2026-04-20T19:28:46.829Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5e/ce5fd4ec803222ae3e69a76d2a2db2eed55e19f5b131702b9789ef45f93d/ty-0.0.32-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5fa2fb3c614349ee211d36476b49d88c5ef79a687cdb91b2872ad023b94d2f8", size = 11184800, upload-time = "2026-04-20T19:28:42.57Z" }, + { url = "https://files.pythonhosted.org/packages/6c/46/ebcf67a5999421331214aac51a7464db42de2be15bbe929c612a3ed0b039/ty-0.0.32-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b89969307ab2417d41c9be8059dd79feea577234e1e10d35132f5495e0d42c6", size = 11718718, upload-time = "2026-04-20T19:28:36.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/2c/2141c86ed0ce0962b45cefb658a95e734f59759d47f20afdcd9c732910a1/ty-0.0.32-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b59868ede9b1d69a088f0d695df52a0061f95fa7baa1d5e0dc6fc9cf06e1334", size = 11346369, upload-time = "2026-04-20T19:28:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/7a/da/ed6f772339cf29bd9a46def9d6db5084689eb574ee4d150ff704224c1ed8/ty-0.0.32-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8300caf35345498e9b9b03e550bba03cee8f5f5f8ab4c83c3b1ff1b7403b7d3a", size = 11280714, upload-time = "2026-04-20T19:28:51.516Z" }, + { url = "https://files.pythonhosted.org/packages/da/9b/c6813987edf4816a40e0c8e408b555f97d3f267c7b3a1688c8bbdf65609c/ty-0.0.32-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:583c7094f4574b02f724db924f98b804d1387a0bd9405ecb5e078cc0f47fbcfb", size = 10638806, upload-time = "2026-04-20T19:28:29.651Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d4/0cefcbd2ad0f3d51762ccf58e652ec7da146eb6ae34f87228f6254bbb8be/ty-0.0.32-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e44ebe1bb4143a5628bc4db67ac0dfebe14594af671e4ee66f6f2e983da56501", size = 10726106, upload-time = "2026-04-20T19:29:06.3Z" }, + { url = "https://files.pythonhosted.org/packages/32/ad/2c8a97f91f06311f4367400f7d13534bbda2522c73c99a3e4c0757dff9b8/ty-0.0.32-py3-none-musllinux_1_2_i686.whl", hash = "sha256:06f17ada3e069cba6148342ef88e9929156beca8473e8d4f101b68f66c75643e", size = 10872951, upload-time = "2026-04-20T19:28:34.077Z" }, + { url = "https://files.pythonhosted.org/packages/ba/68/42293f9248106dd51875120971a5cc6ea315c2c4dcfb8e59aa063aa0af26/ty-0.0.32-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e96e60fa556cec04f15d7ea62d2ceee5982bd389233e961ab9fd42304e278175", size = 11363334, upload-time = "2026-04-20T19:28:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/df/92/be9abf4d3e589ad5023e2ea965b93e204ec856420d46adf73c5c36c04678/ty-0.0.32-py3-none-win32.whl", hash = "sha256:2ff2ebb4986b24aebcf1444db7db5ca41b36086040e95eea9f8fb851c11e805c", size = 10260689, upload-time = "2026-04-20T19:28:56.541Z" }, + { url = "https://files.pythonhosted.org/packages/14/61/dc86acea899349d2579cb8419aecedd83dc504d7d6a10df65eef546c8300/ty-0.0.32-py3-none-win_amd64.whl", hash = "sha256:ba7284a4a954b598c1b31500352b3ec1f89bff533825592b5958848226fdc7ee", size = 11255371, upload-time = "2026-04-20T19:28:39.917Z" }, + { url = "https://files.pythonhosted.org/packages/43/01/beffec56d71ca25b343ede63adb076456b5b3e211f1c066452a44cd120b3/ty-0.0.32-py3-none-win_arm64.whl", hash = "sha256:7e10aadbdbda989a7d567ee6a37f8b98d4d542e31e3b190a2879fd581f75d658", size = 10658087, upload-time = "2026-04-20T19:28:59.286Z" }, +] + [[package]] name = "typer" version = "0.23.1"