From b474c60ad8383674333355261fad55045ffe6ac4 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Thu, 16 Apr 2026 13:51:53 +0200 Subject: [PATCH 1/7] Adapt to apify-client v3 Co-Authored-By: Claude Sonnet 4.6 --- docs/02_concepts/code/07_webhook.py | 2 +- .../02_concepts/code/07_webhook_preventing.py | 2 +- pyproject.toml | 6 +- src/apify/__init__.py | 2 +- src/apify/_actor.py | 83 +++-- src/apify/_charging.py | 14 +- src/apify/_models.py | 79 ++++- .../_apify/_alias_resolving.py | 11 +- .../_apify/_api_client_creation.py | 11 +- .../storage_clients/_apify/_dataset_client.py | 15 +- .../_apify/_key_value_store_client.py | 22 +- src/apify/storage_clients/_apify/_models.py | 21 +- .../_apify/_request_queue_client.py | 38 ++- .../_apify/_request_queue_shared_client.py | 46 +-- .../_apify/_request_queue_single_client.py | 47 ++- src/apify/storage_clients/_apify/_utils.py | 21 ++ tests/e2e/conftest.py | 68 ++-- tests/e2e/test_actor_api_helpers.py | 102 +++--- tests/e2e/test_actor_call_timeouts.py | 4 +- tests/e2e/test_actor_charge.py | 32 +- .../test_actor_create_proxy_configuration.py | 4 +- tests/e2e/test_actor_events.py | 37 +- tests/e2e/test_actor_lifecycle.py | 68 +++- tests/e2e/test_actor_log.py | 2 +- tests/e2e/test_actor_request_queue.py | 8 +- tests/e2e/test_actor_scrapy.py | 4 +- tests/e2e/test_apify_storages.py | 2 +- tests/e2e/test_crawlee/conftest.py | 2 +- tests/e2e/test_fixtures.py | 6 +- tests/e2e/test_scrapy/conftest.py | 2 +- tests/integration/conftest.py | 3 +- tests/integration/test_dataset.py | 2 +- tests/integration/test_key_value_store.py | 2 +- tests/integration/test_request_queue.py | 96 +++--- tests/unit/actor/test_actor_helpers.py | 96 +++--- tests/unit/actor/test_actor_lifecycle.py | 98 +++++- tests/unit/conftest.py | 3 +- .../storage_clients/test_apify_kvs_client.py | 71 ++-- uv.lock | 318 ++---------------- 39 files changed, 808 insertions(+), 642 deletions(-) diff --git a/docs/02_concepts/code/07_webhook.py b/docs/02_concepts/code/07_webhook.py index 3dd48b13..a817b399 100644 --- a/docs/02_concepts/code/07_webhook.py +++ b/docs/02_concepts/code/07_webhook.py @@ -7,7 +7,7 @@ async def main() -> None: async with Actor: # Create a webhook that will be triggered when the Actor run fails. webhook = Webhook( - event_types=[WebhookEventType.ACTOR_RUN_FAILED], + event_types=['ACTOR.RUN.FAILED'], request_url='https://example.com/run-failed', ) diff --git a/docs/02_concepts/code/07_webhook_preventing.py b/docs/02_concepts/code/07_webhook_preventing.py index ec2334e3..8d2b12a2 100644 --- a/docs/02_concepts/code/07_webhook_preventing.py +++ b/docs/02_concepts/code/07_webhook_preventing.py @@ -7,7 +7,7 @@ async def main() -> None: async with Actor: # Create a webhook that will be triggered when the Actor run fails. webhook = Webhook( - event_types=[WebhookEventType.ACTOR_RUN_FAILED], + event_types=['ACTOR.RUN.FAILED'], request_url='https://example.com/run-failed', ) diff --git a/pyproject.toml b/pyproject.toml index b9e8a34b..c482d003 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "Apify SDK for Python" authors = [{ name = "Apify Technologies s.r.o.", email = "support@apify.com" }] license = { file = "LICENSE" } readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", @@ -35,7 +35,7 @@ keywords = [ "scraping", ] dependencies = [ - "apify-client>=2.3.0,<3.0.0", + "apify-client @ git+https://github.com/apify/apify-client-python.git@master", "apify-shared>=2.0.0,<3.0.0", "crawlee>=1.0.4,<2.0.0", "cachetools>=5.5.0", @@ -43,7 +43,7 @@ dependencies = [ "impit>=0.8.0", "lazy-object-proxy>=1.11.0", "more_itertools>=10.2.0", - "pydantic>=2.11.0", + "pydantic[email]>=2.11.0", "typing-extensions>=4.1.0", "websockets>=14.0", "yarl>=1.18.0", diff --git a/src/apify/__init__.py b/src/apify/__init__.py index f6495d55..49c085e5 100644 --- a/src/apify/__init__.py +++ b/src/apify/__init__.py @@ -1,6 +1,6 @@ from importlib import metadata -from apify_shared.consts import WebhookEventType +from apify_client._models import WebhookEventType from crawlee import Request from crawlee.events import ( Event, diff --git a/src/apify/_actor.py b/src/apify/_actor.py index 6d04e9ae..f61bb52c 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -506,17 +506,21 @@ def new_client( (increases exponentially from this value). timeout: The socket timeout of the HTTP requests sent to the Apify API. """ - token = token or self.configuration.token - api_url = api_url or self.configuration.api_base_url - return ApifyClientAsync( - token=token, - api_url=api_url, - max_retries=max_retries, - min_delay_between_retries_millis=int(min_delay_between_retries.total_seconds() * 1000) - if min_delay_between_retries is not None - else None, - timeout_secs=int(timeout.total_seconds()) if timeout else None, - ) + kwargs = { + 'token': token or self.configuration.token, + 'api_url': api_url or self.configuration.api_base_url, + } + + if max_retries is not None: + kwargs['max_retries'] = max_retries + + if min_delay_between_retries is not None: + kwargs['min_delay_between_retries_millis'] = int(min_delay_between_retries.total_seconds() * 1000) + + if timeout is not None: + kwargs['timeout_secs'] = int(timeout.total_seconds()) + + return ApifyClientAsync(**kwargs) # ty: ignore[invalid-argument-type] @_ensure_context async def open_dataset( @@ -919,17 +923,21 @@ async def start( f'Invalid timeout {timeout!r}: expected `None`, `"inherit"`, `"RemainingTime"`, or a `timedelta`.' ) - api_result = await client.actor(actor_id).start( + actor_client = client.actor(actor_id) + run = await actor_client.start( run_input=run_input, content_type=content_type, build=build, memory_mbytes=memory_mbytes, - timeout_secs=int(actor_start_timeout.total_seconds()) if actor_start_timeout is not None else None, + timeout=actor_start_timeout, wait_for_finish=wait_for_finish, webhooks=serialized_webhooks, ) - return ActorRun.model_validate(api_result) + if run is None: + raise RuntimeError(f'Failed to start Actor with ID "{actor_id}".') + + return ActorRun.from_client_actor_run(run) @_ensure_context async def abort( @@ -956,13 +964,17 @@ async def abort( Info about the aborted Actor run. """ client = self.new_client(token=token) if token else self.apify_client + run_client = client.run(run_id) if status_message: - await client.run(run_id).update(status_message=status_message) + await run_client.update(status_message=status_message) - api_result = await client.run(run_id).abort(gracefully=gracefully) + run = await run_client.abort(gracefully=gracefully) - return ActorRun.model_validate(api_result) + if run is None: + raise RuntimeError(f'Failed to abort Actor run with ID "{run_id}".') + + return ActorRun.from_client_actor_run(run) @_ensure_context async def call( @@ -1034,18 +1046,22 @@ async def call( f'Invalid timeout {timeout!r}: expected `None`, `"inherit"`, `"RemainingTime"`, or a `timedelta`.' ) - api_result = await client.actor(actor_id).call( + actor_client = client.actor(actor_id) + run = await actor_client.call( run_input=run_input, content_type=content_type, build=build, memory_mbytes=memory_mbytes, - timeout_secs=int(actor_call_timeout.total_seconds()) if actor_call_timeout is not None else None, + timeout=actor_call_timeout, webhooks=serialized_webhooks, - wait_secs=int(wait.total_seconds()) if wait is not None else None, + wait_duration=wait, logger=logger, ) - return ActorRun.model_validate(api_result) + if run is None: + raise RuntimeError(f'Failed to call Actor with ID "{actor_id}".') + + return ActorRun.from_client_actor_run(run) @_ensure_context async def call_task( @@ -1106,16 +1122,20 @@ async def call_task( else: raise ValueError(f'Invalid timeout {timeout!r}: expected `None`, `"inherit"`, or a `timedelta`.') - api_result = await client.task(task_id).call( + task_client = client.task(task_id) + run = await task_client.call( task_input=task_input, build=build, memory_mbytes=memory_mbytes, - timeout_secs=int(task_call_timeout.total_seconds()) if task_call_timeout is not None else None, + timeout=task_call_timeout, webhooks=serialized_webhooks, - wait_secs=int(wait.total_seconds()) if wait is not None else None, + wait_duration=wait, ) - return ActorRun.model_validate(api_result) + if run is None: + raise RuntimeError(f'Failed to call Task with ID "{task_id}".') + + return ActorRun.from_client_actor_run(run) @_ensure_context async def metamorph( @@ -1293,11 +1313,18 @@ async def set_status_message( if not self.configuration.actor_run_id: raise RuntimeError('actor_run_id cannot be None when running on the Apify platform.') - api_result = await self.apify_client.run(self.configuration.actor_run_id).update( - status_message=status_message, is_status_message_terminal=is_terminal + run_client = self.apify_client.run(self.configuration.actor_run_id) + run = await run_client.update( + status_message=status_message, + is_status_message_terminal=is_terminal, ) - return ActorRun.model_validate(api_result) + if run is None: + raise RuntimeError( + f'Failed to set status message for Actor run with ID "{self.configuration.actor_run_id}".' + ) + + return ActorRun.from_client_actor_run(run) @_ensure_context async def create_proxy_configuration( diff --git a/src/apify/_charging.py b/src/apify/_charging.py index 9efa700d..36d38e71 100644 --- a/src/apify/_charging.py +++ b/src/apify/_charging.py @@ -421,14 +421,20 @@ async def _fetch_pricing_info(self) -> _FetchedPricingInfoDict: if self._actor_run_id is None: raise RuntimeError('Actor run ID not found even though the Actor is running on Apify') - run = run_validator.validate_python(await self._client.run(self._actor_run_id).get()) + run = await self._client.run(self._actor_run_id).get() + if run is None: raise RuntimeError('Actor run not found') - max_charge = run.options.max_total_charge_usd + actor_run = ActorRun.from_client_actor_run(run) + + if actor_run is None: + raise RuntimeError('Actor run not found') + + max_charge = actor_run.options.max_total_charge_usd return _FetchedPricingInfoDict( - pricing_info=run.pricing_info, - charged_event_counts=run.charged_event_counts or {}, + pricing_info=actor_run.pricing_info, + charged_event_counts=actor_run.charged_event_counts or {}, max_total_charge_usd=Decimal(str(max_charge)) if max_charge is not None else Decimal('inf'), ) diff --git a/src/apify/_models.py b/src/apify/_models.py index 357417ec..38db1d6d 100644 --- a/src/apify/_models.py +++ b/src/apify/_models.py @@ -5,7 +5,7 @@ from pydantic import BaseModel, BeforeValidator, ConfigDict, Field -from apify_shared.consts import ActorJobStatus, MetaOrigin, WebhookEventType +from apify_client._models import ActorJobStatus, Run from crawlee._utils.urls import validate_http_url from apify._utils import docs_group @@ -53,7 +53,7 @@ class Webhook(BaseModel): model_config = ConfigDict(populate_by_name=True, extra='allow') event_types: Annotated[ - list[WebhookEventType], + list[str], Field(alias='eventTypes', description='Event types that should trigger the webhook'), ] request_url: Annotated[ @@ -84,7 +84,7 @@ class Webhook(BaseModel): class ActorRunMeta(BaseModel): model_config = ConfigDict(populate_by_name=True, extra='allow') - origin: Annotated[MetaOrigin, Field()] + origin: Annotated[str, Field()] client_ip: Annotated[str | None, Field(alias='clientIp')] = None user_agent: Annotated[str | None, Field(alias='userAgent')] = None schedule_id: Annotated[str | None, Field(alias='scheduleId')] = None @@ -224,33 +224,85 @@ class PayPerEventActorPricingInfo(CommonActorPricingInfo): @docs_group('Actor') class ActorRun(BaseModel): + """Represents an Actor run and its associated data.""" + model_config = ConfigDict(populate_by_name=True, extra='allow') id: Annotated[str, Field(alias='id')] + """Unique identifier of the Actor run.""" + act_id: Annotated[str, Field(alias='actId')] + """ID of the Actor that was run.""" + user_id: Annotated[str, Field(alias='userId')] + """ID of the user who started the run.""" + actor_task_id: Annotated[str | None, Field(alias='actorTaskId')] = None + """ID of the Actor task, if the run was started from a task.""" + started_at: Annotated[datetime, Field(alias='startedAt')] + """Time when the Actor run started.""" + finished_at: Annotated[datetime | None, Field(alias='finishedAt')] = None + """Time when the Actor run finished.""" + status: Annotated[ActorJobStatus, Field(alias='status')] + """Current status of the Actor run.""" + status_message: Annotated[str | None, Field(alias='statusMessage')] = None + """Detailed message about the run status.""" + is_status_message_terminal: Annotated[bool | None, Field(alias='isStatusMessageTerminal')] = None + """Whether the status message is terminal (final).""" + meta: Annotated[ActorRunMeta, Field(alias='meta')] + """Metadata about the Actor run.""" + stats: Annotated[ActorRunStats, Field(alias='stats')] + """Statistics of the Actor run.""" + options: Annotated[ActorRunOptions, Field(alias='options')] + """Configuration options for the Actor run.""" + build_id: Annotated[str, Field(alias='buildId')] + """ID of the Actor build used for this run.""" + exit_code: Annotated[int | None, Field(alias='exitCode')] = None + """Exit code of the Actor run process.""" + general_access: Annotated[str | None, Field(alias='generalAccess')] = None + """General access level for the Actor run.""" + default_key_value_store_id: Annotated[str, Field(alias='defaultKeyValueStoreId')] + """ID of the default key-value store for this run.""" + default_dataset_id: Annotated[str, Field(alias='defaultDatasetId')] + """ID of the default dataset for this run.""" + default_request_queue_id: Annotated[str, Field(alias='defaultRequestQueueId')] + """ID of the default request queue for this run.""" + build_number: Annotated[str | None, Field(alias='buildNumber')] = None + """Build number of the Actor build used for this run.""" + container_url: Annotated[str | None, Field(alias='containerUrl')] = None + """URL of the container running the Actor.""" + is_container_server_ready: Annotated[bool | None, Field(alias='isContainerServerReady')] = None + """Whether the container's HTTP server is ready to accept requests.""" + git_branch_name: Annotated[str | None, Field(alias='gitBranchName')] = None + """Name of the git branch used for the Actor build.""" + usage: Annotated[ActorRunUsage | None, Field(alias='usage')] = None + """Resource usage statistics for the run.""" + usage_total_usd: Annotated[float | None, Field(alias='usageTotalUsd')] = None + """Total cost of the run in USD.""" + usage_usd: Annotated[ActorRunUsageUsd | None, Field(alias='usageUsd')] = None + """Resource usage costs in USD.""" + pricing_info: Annotated[ FreeActorPricingInfo | FlatPricePerMonthActorPricingInfo @@ -259,8 +311,29 @@ class ActorRun(BaseModel): | None, Field(alias='pricingInfo', discriminator='pricing_model'), ] = None + """Pricing information for the Actor.""" + charged_event_counts: Annotated[ dict[str, int] | None, Field(alias='chargedEventCounts'), ] = None + """Count of charged events for pay-per-event pricing model.""" + metamorphs: Annotated[list[Metamorph] | None, Field(alias='metamorphs')] = None + """List of metamorph events that occurred during the run.""" + + @classmethod + def from_client_actor_run(cls, client_actor_run: Run) -> ActorRun: + """Create an `ActorRun` from an Apify API client's `Run` model. + + Args: + client_actor_run: `Run` instance from Apify API client. + + Returns: + `ActorRun` instance with properly converted types. + """ + # Dump to dict first with mode='json' to serialize special types + client_actor_run_dict = client_actor_run.model_dump(by_alias=True, mode='json') + + # Validate and construct ActorRun from the serialized dict + return cls.model_validate(client_actor_run_dict) diff --git a/src/apify/storage_clients/_apify/_alias_resolving.py b/src/apify/storage_clients/_apify/_alias_resolving.py index c07edca3..8ebb3e54 100644 --- a/src/apify/storage_clients/_apify/_alias_resolving.py +++ b/src/apify/storage_clients/_apify/_alias_resolving.py @@ -2,6 +2,7 @@ import logging from asyncio import Lock +from datetime import timedelta from functools import cached_property from logging import getLogger from typing import TYPE_CHECKING, ClassVar, Literal, overload @@ -15,7 +16,7 @@ from collections.abc import Callable from types import TracebackType - from apify_client.clients import ( + from apify_client._resource_clients import ( DatasetClientAsync, DatasetCollectionClientAsync, KeyValueStoreClientAsync, @@ -106,8 +107,8 @@ async def open_by_alias( # Create new unnamed storage and store alias mapping raw_metadata = await collection_client.get_or_create() - await alias_resolver.store_mapping(storage_id=raw_metadata['id']) - return get_resource_client_by_id(raw_metadata['id']) + await alias_resolver.store_mapping(storage_id=raw_metadata.id) + return get_resource_client_by_id(raw_metadata.id) class AliasResolver: @@ -252,8 +253,8 @@ async def _get_default_kvs_client(configuration: Configuration) -> KeyValueStore token=configuration.token, api_url=configuration.api_base_url, max_retries=8, - min_delay_between_retries_millis=500, - timeout_secs=360, + min_delay_between_retries=timedelta(milliseconds=500), + timeout=timedelta(seconds=360), ) if not configuration.default_key_value_store_id: diff --git a/src/apify/storage_clients/_apify/_api_client_creation.py b/src/apify/storage_clients/_apify/_api_client_creation.py index 542a203f..ed5cd6cf 100644 --- a/src/apify/storage_clients/_apify/_api_client_creation.py +++ b/src/apify/storage_clients/_apify/_api_client_creation.py @@ -1,5 +1,6 @@ from __future__ import annotations +from datetime import timedelta from typing import TYPE_CHECKING, Literal, overload from apify_client import ApifyClientAsync @@ -8,7 +9,7 @@ from apify.storage_clients._apify._alias_resolving import AliasResolver, open_by_alias if TYPE_CHECKING: - from apify_client.clients import DatasetClientAsync, KeyValueStoreClientAsync, RequestQueueClientAsync + from apify_client._resource_clients import DatasetClientAsync, KeyValueStoreClientAsync, RequestQueueClientAsync from apify._configuration import Configuration @@ -137,13 +138,13 @@ def get_resource_client(storage_id: str) -> DatasetClientAsync: # Default storage does not exist. Create a new one. if not raw_metadata: raw_metadata = await collection_client.get_or_create() - resource_client = get_resource_client(raw_metadata['id']) + resource_client = get_resource_client(raw_metadata.id) return resource_client # Open by name. case (None, str(), None, _): raw_metadata = await collection_client.get_or_create(name=name) - return get_resource_client(raw_metadata['id']) + return get_resource_client(raw_metadata.id) # Open by ID. case (None, None, str(), _): @@ -177,6 +178,6 @@ def _create_api_client(configuration: Configuration) -> ApifyClientAsync: api_url=configuration.api_base_url, api_public_url=configuration.api_public_base_url, max_retries=8, - min_delay_between_retries_millis=500, - timeout_secs=360, + min_delay_between_retries=timedelta(milliseconds=500), + timeout=timedelta(seconds=360), ) diff --git a/src/apify/storage_clients/_apify/_dataset_client.py b/src/apify/storage_clients/_apify/_dataset_client.py index 2287ec66..c3445915 100644 --- a/src/apify/storage_clients/_apify/_dataset_client.py +++ b/src/apify/storage_clients/_apify/_dataset_client.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: from collections.abc import AsyncIterator - from apify_client.clients import DatasetClientAsync + from apify_client._resource_clients import DatasetClientAsync from crawlee._types import JsonSerializable from apify import Configuration @@ -69,7 +69,18 @@ def __init__( @override async def get_metadata(self) -> DatasetMetadata: metadata = await self._api_client.get() - return DatasetMetadata.model_validate(metadata) + + if metadata is None: + raise ValueError('Failed to retrieve dataset metadata.') + + return DatasetMetadata( + id=metadata.id, + name=metadata.name, + created_at=metadata.created_at, + modified_at=metadata.modified_at, + accessed_at=metadata.accessed_at, + item_count=int(metadata.item_count), + ) @classmethod async def open( diff --git a/src/apify/storage_clients/_apify/_key_value_store_client.py b/src/apify/storage_clients/_apify/_key_value_store_client.py index b422b464..712a5e78 100644 --- a/src/apify/storage_clients/_apify/_key_value_store_client.py +++ b/src/apify/storage_clients/_apify/_key_value_store_client.py @@ -11,12 +11,12 @@ from crawlee.storage_clients.models import KeyValueStoreRecord, KeyValueStoreRecordMetadata from ._api_client_creation import create_storage_api_client -from ._models import ApifyKeyValueStoreMetadata, KeyValueStoreListKeysPage +from ._models import ApifyKeyValueStoreMetadata if TYPE_CHECKING: from collections.abc import AsyncIterator - from apify_client.clients import KeyValueStoreClientAsync + from apify_client._resource_clients import KeyValueStoreClientAsync from apify import Configuration @@ -54,7 +54,18 @@ def __init__( @override async def get_metadata(self) -> ApifyKeyValueStoreMetadata: metadata = await self._api_client.get() - return ApifyKeyValueStoreMetadata.model_validate(metadata) + + if metadata is None: + raise ValueError('Failed to retrieve dataset metadata.') + + return ApifyKeyValueStoreMetadata( + id=metadata.id, + name=metadata.name, + created_at=metadata.created_at, + modified_at=metadata.modified_at, + accessed_at=metadata.accessed_at, + url_signing_secret_key=metadata.url_signing_secret_key, + ) @classmethod async def open( @@ -143,14 +154,13 @@ async def iterate_keys( count = 0 while True: - response = await self._api_client.list_keys(exclusive_start_key=exclusive_start_key) - list_key_page = KeyValueStoreListKeysPage.model_validate(response) + list_key_page = await self._api_client.list_keys(exclusive_start_key=exclusive_start_key) for item in list_key_page.items: # Convert KeyValueStoreKeyInfo to KeyValueStoreRecordMetadata record_metadata = KeyValueStoreRecordMetadata( key=item.key, - size=item.size, + size=int(item.size), content_type='application/octet-stream', # Content type not available from list_keys ) yield record_metadata diff --git a/src/apify/storage_clients/_apify/_models.py b/src/apify/storage_clients/_apify/_models.py index d05f3394..6673e805 100644 --- a/src/apify/storage_clients/_apify/_models.py +++ b/src/apify/storage_clients/_apify/_models.py @@ -1,7 +1,7 @@ from __future__ import annotations from datetime import datetime, timedelta -from typing import Annotated +from typing import TYPE_CHECKING, Annotated from pydantic import BaseModel, ConfigDict, Field @@ -10,6 +10,9 @@ from apify import Request from apify._utils import docs_group +if TYPE_CHECKING: + from apify_client._models import LockedRequestQueueHead + @docs_group('Storage data') class ApifyKeyValueStoreMetadata(KeyValueStoreMetadata): @@ -59,6 +62,22 @@ class RequestQueueHead(BaseModel): items: Annotated[list[Request], Field(alias='items', default_factory=list[Request])] """The list of request objects retrieved from the beginning of the queue.""" + @classmethod + def from_client_locked_head(cls, client_locked_head: LockedRequestQueueHead) -> RequestQueueHead: + """Create a `RequestQueueHead` from an Apify API client's `LockedRequestQueueHead` model. + + Args: + client_locked_head: `LockedRequestQueueHead` instance from Apify API client. + + Returns: + `RequestQueueHead` instance with properly converted types. + """ + # Dump to dict with mode='json' to serialize special types like AnyUrl + head_dict = client_locked_head.model_dump(by_alias=True, mode='json') + + # Validate and construct RequestQueueHead from the serialized dict + return cls.model_validate(head_dict) + class KeyValueStoreKeyInfo(BaseModel): """Model for a key-value store key info. diff --git a/src/apify/storage_clients/_apify/_request_queue_client.py b/src/apify/storage_clients/_apify/_request_queue_client.py index 9a589ec1..cc44c3c5 100644 --- a/src/apify/storage_clients/_apify/_request_queue_client.py +++ b/src/apify/storage_clients/_apify/_request_queue_client.py @@ -15,11 +15,10 @@ if TYPE_CHECKING: from collections.abc import Sequence - from apify_client.clients import RequestQueueClientAsync - from crawlee import Request + from apify_client._resource_clients import RequestQueueClientAsync from crawlee.storage_clients.models import AddRequestsResponse, ProcessedRequest, RequestQueueMetadata - from apify import Configuration + from apify import Configuration, Request logger = getLogger(__name__) @@ -77,26 +76,31 @@ async def get_metadata(self) -> ApifyRequestQueueMetadata: Returns: Request queue metadata with accurate counts and timestamps, combining API data with local estimates. """ - response = await self._api_client.get() + metadata = await self._api_client.get() - if response is None: + if metadata is None: raise ValueError('Failed to fetch request queue metadata from the API.') + total_request_count = int(metadata.total_request_count) + handled_request_count = int(metadata.handled_request_count) + pending_request_count = int(metadata.pending_request_count) + # Enhance API response with local estimations to account for propagation delays (API data can be delayed # by a few seconds, while local estimates are immediately accurate). return ApifyRequestQueueMetadata( - id=response['id'], - name=response['name'], - total_request_count=max(response['totalRequestCount'], self._implementation.metadata.total_request_count), - handled_request_count=max( - response['handledRequestCount'], self._implementation.metadata.handled_request_count + id=metadata.id, + name=metadata.name, + total_request_count=max(total_request_count, self._implementation.metadata.total_request_count), + handled_request_count=max(handled_request_count, self._implementation.metadata.handled_request_count), + pending_request_count=pending_request_count, + created_at=min(metadata.created_at, self._implementation.metadata.created_at), + modified_at=max(metadata.modified_at, self._implementation.metadata.modified_at), + accessed_at=max(metadata.accessed_at, self._implementation.metadata.accessed_at), + had_multiple_clients=metadata.had_multiple_clients or self._implementation.metadata.had_multiple_clients, + stats=RequestQueueStats.model_validate( + metadata.stats.model_dump(by_alias=True) if metadata.stats else {}, + by_alias=True, ), - pending_request_count=response['pendingRequestCount'], - created_at=min(response['createdAt'], self._implementation.metadata.created_at), - modified_at=max(response['modifiedAt'], self._implementation.metadata.modified_at), - accessed_at=max(response['accessedAt'], self._implementation.metadata.accessed_at), - had_multiple_clients=response['hadMultipleClients'] or self._implementation.metadata.had_multiple_clients, - stats=RequestQueueStats.model_validate(response['stats'], by_alias=True), ) @classmethod @@ -145,7 +149,7 @@ async def open( raw_metadata = await api_client.get() if raw_metadata is None: raise ValueError('Failed to retrieve request queue metadata from the API.') - metadata = ApifyRequestQueueMetadata.model_validate(raw_metadata) + metadata = ApifyRequestQueueMetadata.model_validate(raw_metadata.model_dump(by_alias=True)) return cls( api_client=api_client, diff --git a/src/apify/storage_clients/_apify/_request_queue_shared_client.py b/src/apify/storage_clients/_apify/_request_queue_shared_client.py index eb2b4338..d30921c4 100644 --- a/src/apify/storage_clients/_apify/_request_queue_shared_client.py +++ b/src/apify/storage_clients/_apify/_request_queue_shared_client.py @@ -11,13 +11,14 @@ from crawlee.storage_clients.models import AddRequestsResponse, ProcessedRequest, RequestQueueMetadata from ._models import ApifyRequestQueueMetadata, CachedRequest, RequestQueueHead -from ._utils import unique_key_to_request_id -from apify import Request +from ._utils import to_crawlee_request, unique_key_to_request_id if TYPE_CHECKING: from collections.abc import Callable, Coroutine, Sequence - from apify_client.clients import RequestQueueClientAsync + from apify_client._resource_clients import RequestQueueClientAsync + + from apify import Request logger = getLogger(__name__) @@ -65,7 +66,7 @@ def __init__( """The Apify API client for communication with Apify platform.""" self._queue_head = deque[str]() - """Local cache of request IDs from the queue head for efficient fetching.""" + """Local cache of request IDs from the request queue head for efficient fetching.""" self._requests_cache: LRUCache[str, CachedRequest] = LRUCache(maxsize=cache_size) """LRU cache storing request objects, keyed by request ID.""" @@ -121,18 +122,17 @@ async def add_batch_of_requests( if new_requests: # Prepare requests for API by converting to dictionaries. - requests_dict = [ - request.model_dump( - by_alias=True, - ) - for request in new_requests - ] + requests_dict = [request.model_dump(by_alias=True) for request in new_requests] # Send requests to API. - api_response = AddRequestsResponse.model_validate( - await self._api_client.batch_add_requests(requests=requests_dict, forefront=forefront) + batch_response = await self._api_client.batch_add_requests( + requests=requests_dict, + forefront=forefront, ) + batch_response_dict = batch_response.model_dump(by_alias=True) + api_response = AddRequestsResponse.model_validate(batch_response_dict) + # Add the locally known already present processed requests based on the local cache. api_response.processed_requests.extend(already_present_requests) @@ -177,7 +177,7 @@ async def fetch_next_request(self) -> Request | None: if not self._queue_head: return None - # Get the next request ID from the queue head + # Get the next request ID from the request queue head next_request_id = self._queue_head.popleft() request = await self._get_or_hydrate_request(next_request_id) @@ -312,7 +312,7 @@ async def _get_request_by_id(self, request_id: str) -> Request | None: if response is None: return None - return Request.model_validate(response) + return to_crawlee_request(response) async def _ensure_head_is_non_empty(self) -> None: """Ensure that the queue head has requests if they are available in the queue.""" @@ -388,7 +388,7 @@ async def _update_request( ) return ProcessedRequest.model_validate( - {'uniqueKey': request.unique_key} | response, + {'uniqueKey': request.unique_key} | response.model_dump(by_alias=True), ) async def _list_head( @@ -431,19 +431,19 @@ async def _list_head( self._should_check_for_forefront_requests = False # Otherwise fetch from API - response = await self._api_client.list_and_lock_head( - lock_secs=int(self._DEFAULT_LOCK_TIME.total_seconds()), + locked_queue_head = await self._api_client.list_and_lock_head( + lock_duration=self._DEFAULT_LOCK_TIME, limit=limit, ) # Update the queue head cache - self._queue_has_locked_requests = response.get('queueHasLockedRequests', False) + self._queue_has_locked_requests = locked_queue_head.queue_has_locked_requests # Check if there is another client working with the RequestQueue - self.metadata.had_multiple_clients = response.get('hadMultipleClients', False) + self.metadata.had_multiple_clients = locked_queue_head.had_multiple_clients - for request_data in response.get('items', []): - request = Request.model_validate(request_data) - request_id = request_data.get('id') + for request_data in locked_queue_head.items: + request = to_crawlee_request(request_data) + request_id = request_data.id # Skip requests without ID or unique key if not request.unique_key or not request_id: @@ -473,7 +473,7 @@ async def _list_head( # After adding new requests to the forefront, any existing leftover locked request is kept in the end. self._queue_head.append(leftover_id) - return RequestQueueHead.model_validate(response) + return RequestQueueHead.from_client_locked_head(locked_queue_head) def _cache_request( self, diff --git a/src/apify/storage_clients/_apify/_request_queue_single_client.py b/src/apify/storage_clients/_apify/_request_queue_single_client.py index 6ab1b392..9c7004e1 100644 --- a/src/apify/storage_clients/_apify/_request_queue_single_client.py +++ b/src/apify/storage_clients/_apify/_request_queue_single_client.py @@ -9,13 +9,14 @@ from crawlee.storage_clients.models import AddRequestsResponse, ProcessedRequest, RequestQueueMetadata -from ._utils import unique_key_to_request_id -from apify import Request +from ._utils import to_crawlee_request, unique_key_to_request_id if TYPE_CHECKING: from collections.abc import Sequence - from apify_client.clients import RequestQueueClientAsync + from apify_client._resource_clients import RequestQueueClientAsync + + from apify import Request logger = getLogger(__name__) @@ -147,22 +148,20 @@ async def add_batch_of_requests( if new_requests: # Prepare requests for API by converting to dictionaries. - requests_dict = [ - request.model_dump( - by_alias=True, - ) - for request in new_requests - ] + requests_dict = [request.model_dump(by_alias=True) for request in new_requests] # Send requests to API. - api_response = AddRequestsResponse.model_validate( - await self._api_client.batch_add_requests(requests=requests_dict, forefront=forefront) - ) + batch_response = await self._api_client.batch_add_requests(requests=requests_dict, forefront=forefront) + batch_response_dict = batch_response.model_dump(by_alias=True) + api_response = AddRequestsResponse.model_validate(batch_response_dict) + # Add the locally known already present processed requests based on the local cache. api_response.processed_requests.extend(already_present_requests) + # Remove unprocessed requests from the cache for unprocessed_request in api_response.unprocessed_requests: - self._requests_cache.pop(unique_key_to_request_id(unprocessed_request.unique_key), None) + request_id = unique_key_to_request_id(unprocessed_request.unique_key) + self._requests_cache.pop(request_id, None) else: api_response = AddRequestsResponse( @@ -288,16 +287,16 @@ async def _list_head(self) -> None: # Update metadata # Check if there is another client working with the RequestQueue - self.metadata.had_multiple_clients = response.get('hadMultipleClients', False) + self.metadata.had_multiple_clients = response.had_multiple_clients # Should warn once? This might be outside expected context if the other consumers consumes at the same time - if modified_at := response.get('queueModifiedAt'): - self.metadata.modified_at = max(self.metadata.modified_at, modified_at) + if response.queue_modified_at: + self.metadata.modified_at = max(self.metadata.modified_at, response.queue_modified_at) # Update the cached data - for request_data in response.get('items', []): - request = Request.model_validate(request_data) - request_id = request_data['id'] + for request_data in response.items: + request = to_crawlee_request(request_data) + request_id = request_data.id if request_id in self._requests_in_progress: # Ignore requests that are already in progress, we will not process them again. @@ -333,7 +332,7 @@ async def _get_request_by_id(self, id: str) -> Request | None: if response is None: return None - request = Request.model_validate(response) + request = to_crawlee_request(response) # Updated local caches if id in self._requests_in_progress: @@ -370,7 +369,7 @@ async def _update_request( ) return ProcessedRequest.model_validate( - {'uniqueKey': request.unique_key} | response, + {'uniqueKey': request.unique_key} | response.model_dump(by_alias=True), ) async def _init_caches(self) -> None: @@ -383,9 +382,9 @@ async def _init_caches(self) -> None: Local deduplication is cheaper, it takes 1 API call for whole cache and 1 read operation per request. """ response = await self._api_client.list_requests(limit=10_000) - for request_data in response.get('items', []): - request = Request.model_validate(request_data) - request_id = request_data['id'] + for request_data in response.items: + request = to_crawlee_request(request_data) + request_id = request_data.id if request.was_already_handled: # Cache just id for deduplication diff --git a/src/apify/storage_clients/_apify/_utils.py b/src/apify/storage_clients/_apify/_utils.py index 5d8174e8..c9bcd54d 100644 --- a/src/apify/storage_clients/_apify/_utils.py +++ b/src/apify/storage_clients/_apify/_utils.py @@ -7,7 +7,12 @@ from crawlee._utils.crypto import compute_short_hash +from apify import Request + if TYPE_CHECKING: + from apify_client._models import HeadRequest, LockedHeadRequest + from apify_client._models import Request as ClientRequest + from apify import Configuration @@ -39,3 +44,19 @@ def hash_api_base_url_and_token(configuration: Configuration) -> str: if configuration.api_public_base_url is None or configuration.token is None: raise ValueError("'Configuration.api_public_base_url' and 'Configuration.token' must be set.") return compute_short_hash(f'{configuration.api_public_base_url}{configuration.token}'.encode()) + + +def to_crawlee_request(client_request: ClientRequest | HeadRequest | LockedHeadRequest) -> Request: + """Convert an Apify API client's `Request` model to a Crawlee's `Request` model. + + Args: + client_request: Request instances from Apify API client. + + Returns: + `Request` instance from Crawlee with properly converted types. + """ + # Dump to dict with mode='json' to serialize special types like AnyUrl + request_dict = client_request.model_dump(by_alias=True, mode='json') + + # Validate and construct Crawlee Request from the serialized dict + return Request.model_validate(request_dict) diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index bdcc9883..12bd2bbb 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -6,6 +6,7 @@ import subprocess import sys import textwrap +from datetime import timedelta from pathlib import Path from typing import TYPE_CHECKING, Any, Protocol @@ -13,7 +14,8 @@ from filelock import FileLock from apify_client import ApifyClient, ApifyClientAsync -from apify_shared.consts import ActorJobStatus, ActorPermissionLevel, ActorSourceType, ApifyEnvVars +from apify_client._models import ActorPermissionLevel, VersionSourceType +from apify_shared.consts import ApifyEnvVars from crawlee import service_locator import apify._actor @@ -25,7 +27,7 @@ from collections.abc import Awaitable, Callable, Coroutine, Iterator, Mapping from decimal import Decimal - from apify_client.clients.resource_clients import ActorClientAsync + from apify_client._resource_clients import ActorClientAsync _TOKEN_ENV_VAR = 'APIFY_TEST_USER_API_TOKEN' _API_URL_ENV_VAR = 'APIFY_INTEGRATION_TESTS_API_URL' @@ -47,7 +49,9 @@ def apify_client_async(apify_token: str) -> ApifyClientAsync: """Create an instance of the ApifyClientAsync.""" api_url = os.getenv(_API_URL_ENV_VAR) - return ApifyClientAsync(apify_token, api_url=api_url) + if api_url is not None: + return ApifyClientAsync(apify_token, api_url=api_url) + return ApifyClientAsync(apify_token) @pytest.fixture @@ -236,7 +240,13 @@ async def _make_actor( if (main_func and main_py) or (main_func and source_files) or (main_py and source_files): raise TypeError('Cannot specify more than one of `main_func`, `main_py` and `source_files` arguments') - client = ApifyClientAsync(token=apify_token, api_url=os.getenv(_API_URL_ENV_VAR)) + api_url = os.getenv(_API_URL_ENV_VAR) + client = ( + ApifyClientAsync(token=apify_token) + if api_url is None + else ApifyClientAsync(token=apify_token, api_url=api_url) + ) + actor_name = generate_unique_resource_name(label) # Get the source of main_func and convert it into a reasonable main_py file. @@ -299,30 +309,30 @@ async def _make_actor( name=actor_name, default_run_build='latest', default_run_memory_mbytes=memory_mbytes, - default_run_timeout_secs=600, + default_run_timeout=timedelta(seconds=600), versions=[ { 'versionNumber': '0.0', 'buildTag': 'latest', - 'sourceType': ActorSourceType.SOURCE_FILES, + 'sourceType': VersionSourceType.SOURCE_FILES.value, 'sourceFiles': source_files_for_api, } ], ) - actor_client = client.actor(created_actor['id']) + actor_client = client.actor(created_actor.id) print(f'Building Actor {actor_name}...') build_result = await actor_client.build(version_number='0.0') - build_client = client.build(build_result['id']) - build_client_result = await build_client.wait_for_finish(wait_secs=600) + build_client = client.build(build_result.id) + build_client_result = await build_client.wait_for_finish(wait_duration=timedelta(seconds=600)) assert build_client_result is not None - assert build_client_result['status'] == ActorJobStatus.SUCCEEDED + assert build_client_result.status.value == 'SUCCEEDED' # We only mark the client for cleanup if the build succeeded, so that if something goes wrong here, # you have a chance to check the error. - actors_for_cleanup.append(created_actor['id']) + actors_for_cleanup.append(created_actor.id) return actor_client @@ -330,17 +340,20 @@ async def _make_actor( # Delete all the generated Actors. for actor_id in actors_for_cleanup: - actor_client = ApifyClient(token=apify_token, api_url=os.getenv(_API_URL_ENV_VAR)).actor(actor_id) - - if (actor := actor_client.get()) is not None: - actor_client.update( - pricing_infos=[ - *actor.get('pricingInfos', []), - { - 'pricingModel': 'FREE', - }, - ] - ) + api_url = os.getenv(_API_URL_ENV_VAR) + + apify_client = ( + ApifyClient(token=apify_token) if api_url is None else ApifyClient(token=apify_token, api_url=api_url) + ) + + actor_client = apify_client.actor(actor_id) + actor = actor_client.get() + + if actor is not None and actor.pricing_infos is not None: + # Convert Pydantic models to dicts before mixing with plain dict + existing_pricing_infos = [pi.model_dump(by_alias=True, exclude_none=True) for pi in actor.pricing_infos] + new_pricing_infos = [*existing_pricing_infos, {'pricingModel': 'FREE'}] + actor_client.update(pricing_infos=new_pricing_infos) actor_client.delete() @@ -389,12 +402,13 @@ async def _run_actor( force_permission_level=force_permission_level, ) - assert isinstance(call_result, dict), 'The result of ActorClientAsync.call() is not a dictionary.' - assert 'id' in call_result, 'The result of ActorClientAsync.call() does not contain an ID.' + assert call_result is not None, 'Failed to start Actor run: missing run ID in the response.' + + run_client = apify_client_async.run(call_result.id) + client_actor_run = await run_client.wait_for_finish(wait_duration=timedelta(seconds=600)) - run_client = apify_client_async.run(call_result['id']) - run_result = await run_client.wait_for_finish(wait_secs=600) + assert client_actor_run is not None, 'Actor run did not finish successfully within the expected time.' - return ActorRun.model_validate(run_result) + return ActorRun.from_client_actor_run(client_actor_run) return _run_actor diff --git a/tests/e2e/test_actor_api_helpers.py b/tests/e2e/test_actor_api_helpers.py index 3747dd3b..e5fd7474 100644 --- a/tests/e2e/test_actor_api_helpers.py +++ b/tests/e2e/test_actor_api_helpers.py @@ -2,9 +2,10 @@ import asyncio import json +from datetime import timedelta from typing import TYPE_CHECKING -from apify_shared.consts import ActorPermissionLevel +from apify_client._models import ActorPermissionLevel from crawlee._utils.crypto import crypto_random_object_id from ._utils import generate_unique_resource_name @@ -28,7 +29,7 @@ async def main() -> None: actor = await make_actor(label='is-at-home', main_func=main) run_result = await run_actor(actor) - assert run_result.status == 'SUCCEEDED' + assert run_result.status.value == 'SUCCEEDED' async def test_actor_retrieves_env_vars( @@ -52,7 +53,7 @@ async def main() -> None: actor = await make_actor(label='get-env', main_func=main) run_result = await run_actor(actor) - assert run_result.status == 'SUCCEEDED' + assert run_result.status.value == 'SUCCEEDED' async def test_actor_creates_new_client_instance( @@ -76,7 +77,7 @@ async def main() -> None: actor = await make_actor(label='new-client', main_func=main) run_result = await run_actor(actor) - assert run_result.status == 'SUCCEEDED' + assert run_result.status.value == 'SUCCEEDED' output_record = await actor.last_run().key_value_store().get_record('OUTPUT') assert output_record is not None @@ -95,13 +96,13 @@ async def main() -> None: actor = await make_actor(label='set-status-message', main_func=main) run_result_1 = await run_actor(actor) - assert run_result_1.status == 'SUCCEEDED' + assert run_result_1.status.value == 'SUCCEEDED' assert run_result_1.status_message == 'testing-status-message' assert run_result_1.is_status_message_terminal is None run_result_2 = await run_actor(actor, run_input={'is_terminal': True}) - assert run_result_2.status == 'SUCCEEDED' + assert run_result_2.status.value == 'SUCCEEDED' assert run_result_2.status_message == 'testing-status-message' assert run_result_2.is_status_message_terminal is True @@ -129,12 +130,15 @@ async def main_outer() -> None: inner_run_status = await Actor.apify_client.actor(inner_actor_id).last_run().get() assert inner_run_status is not None - assert inner_run_status.get('status') in ['READY', 'RUNNING'] + assert inner_run_status.status.value in {'READY', 'RUNNING'} inner_actor = await make_actor(label='start-inner', main_func=main_inner) outer_actor = await make_actor(label='start-outer', main_func=main_outer) - inner_actor_id = (await inner_actor.get() or {})['id'] + inner_actor_get_result = await inner_actor.get() + assert inner_actor_get_result is not None, 'Failed to get inner actor ID' + + inner_actor_id = inner_actor_get_result.id test_value = crypto_random_object_id() run_result_outer = await run_actor( @@ -142,9 +146,9 @@ async def main_outer() -> None: run_input={'test_value': test_value, 'inner_actor_id': inner_actor_id}, ) - assert run_result_outer.status == 'SUCCEEDED' + assert run_result_outer.status.value == 'SUCCEEDED' - await inner_actor.last_run().wait_for_finish(wait_secs=600) + await inner_actor.last_run().wait_for_finish(wait_duration=timedelta(seconds=600)) inner_output_record = await inner_actor.last_run().key_value_store().get_record('OUTPUT') assert inner_output_record is not None @@ -172,14 +176,18 @@ async def main_outer() -> None: await Actor.call(inner_actor_id, run_input={'test_value': test_value}) - inner_run_status = await Actor.apify_client.actor(inner_actor_id).last_run().get() - assert inner_run_status is not None - assert inner_run_status.get('status') == 'SUCCEEDED' + run_result_inner = await Actor.apify_client.actor(inner_actor_id).last_run().get() + + assert run_result_inner is not None + assert run_result_inner.status.value == 'SUCCEEDED' inner_actor = await make_actor(label='call-inner', main_func=main_inner) outer_actor = await make_actor(label='call-outer', main_func=main_outer) - inner_actor_id = (await inner_actor.get() or {})['id'] + inner_actor_get_result = await inner_actor.get() + assert inner_actor_get_result is not None, 'Failed to get inner actor ID' + + inner_actor_id = inner_actor_get_result.id test_value = crypto_random_object_id() run_result_outer = await run_actor( @@ -187,9 +195,9 @@ async def main_outer() -> None: run_input={'test_value': test_value, 'inner_actor_id': inner_actor_id}, ) - assert run_result_outer.status == 'SUCCEEDED' + assert run_result_outer.status.value == 'SUCCEEDED' - await inner_actor.last_run().wait_for_finish(wait_secs=600) + await inner_actor.last_run().wait_for_finish(wait_duration=timedelta(seconds=600)) inner_output_record = await inner_actor.last_run().key_value_store().get_record('OUTPUT') assert inner_output_record is not None @@ -217,14 +225,18 @@ async def main_outer() -> None: await Actor.call_task(inner_task_id) - inner_run_status = await Actor.apify_client.task(inner_task_id).last_run().get() - assert inner_run_status is not None - assert inner_run_status.get('status') == 'SUCCEEDED' + run_result_inner = await Actor.apify_client.task(inner_task_id).last_run().get() + + assert run_result_inner is not None + assert run_result_inner.status.value == 'SUCCEEDED' inner_actor = await make_actor(label='call-task-inner', main_func=main_inner) outer_actor = await make_actor(label='call-task-outer', main_func=main_outer) - inner_actor_id = (await inner_actor.get() or {})['id'] + inner_actor_get_result = await inner_actor.get() + assert inner_actor_get_result is not None, 'Failed to get inner actor ID' + + inner_actor_id = inner_actor_get_result.id test_value = crypto_random_object_id() task = await apify_client_async.tasks().create( @@ -235,19 +247,19 @@ async def main_outer() -> None: run_result_outer = await run_actor( outer_actor, - run_input={'test_value': test_value, 'inner_task_id': task['id']}, + run_input={'test_value': test_value, 'inner_task_id': task.id}, force_permission_level=ActorPermissionLevel.FULL_PERMISSIONS, ) - assert run_result_outer.status == 'SUCCEEDED' + assert run_result_outer.status.value == 'SUCCEEDED' - await inner_actor.last_run().wait_for_finish(wait_secs=600) + await inner_actor.last_run().wait_for_finish(wait_duration=timedelta(seconds=600)) inner_output_record = await inner_actor.last_run().key_value_store().get_record('OUTPUT') assert inner_output_record is not None assert inner_output_record['value'] == f'{test_value}_XXX_{test_value}' - await apify_client_async.task(task['id']).delete() + await apify_client_async.task(task.id).delete() async def test_actor_aborts_another_actor_run( @@ -273,7 +285,7 @@ async def main_outer() -> None: outer_actor = await make_actor(label='abort-outer', main_func=main_outer) run_result_inner = await inner_actor.start(force_permission_level=ActorPermissionLevel.FULL_PERMISSIONS) - inner_run_id = run_result_inner['id'] + inner_run_id = run_result_inner.id run_result_outer = await run_actor( outer_actor, @@ -281,13 +293,17 @@ async def main_outer() -> None: force_permission_level=ActorPermissionLevel.FULL_PERMISSIONS, ) - assert run_result_outer.status == 'SUCCEEDED' + assert run_result_outer.status.value == 'SUCCEEDED' + + inner_actor_run_client = inner_actor.last_run() + inner_actor_run = await inner_actor_run_client.wait_for_finish(wait_duration=timedelta(seconds=600)) + + if inner_actor_run is None: + raise AssertionError('Failed to get inner actor run after aborting it.') - await inner_actor.last_run().wait_for_finish(wait_secs=600) - inner_actor_last_run_dict = await inner_actor.last_run().get() - inner_actor_last_run = ActorRun.model_validate(inner_actor_last_run_dict) + inner_actor_last_run = ActorRun.from_client_actor_run(inner_actor_run) - assert inner_actor_last_run.status == 'ABORTED' + assert inner_actor_last_run.status.value == 'ABORTED' inner_output_record = await inner_actor.last_run().key_value_store().get_record('OUTPUT') assert inner_output_record is None @@ -331,7 +347,10 @@ async def main_outer() -> None: inner_actor = await make_actor(label='metamorph-inner', main_func=main_inner) outer_actor = await make_actor(label='metamorph-outer', main_func=main_outer) - inner_actor_id = (await inner_actor.get() or {})['id'] + inner_actor_get_result = await inner_actor.get() + assert inner_actor_get_result is not None, 'Failed to get inner actor ID' + + inner_actor_id = inner_actor_get_result.id test_value = crypto_random_object_id() run_result_outer = await run_actor( @@ -339,7 +358,7 @@ async def main_outer() -> None: run_input={'test_value': test_value, 'inner_actor_id': inner_actor_id}, ) - assert run_result_outer.status == 'SUCCEEDED' + assert run_result_outer.status.value == 'SUCCEEDED' outer_run_key_value_store = outer_actor.last_run().key_value_store() @@ -377,7 +396,7 @@ async def main() -> None: run_input={'counter_key': 'reboot_counter'}, ) - assert run_result.status == 'SUCCEEDED' + assert run_result.status.value == 'SUCCEEDED' not_written_value = await actor.last_run().key_value_store().get_record('THIS_KEY_SHOULD_NOT_BE_WRITTEN') assert not_written_value is None @@ -433,7 +452,7 @@ async def main_client() -> None: await Actor.add_webhook( Webhook( - event_types=[WebhookEventType.ACTOR_RUN_SUCCEEDED], + event_types=[WebhookEventType.ACTOR_RUN_SUCCEEDED.value], request_url=server_actor_container_url, ) ) @@ -444,7 +463,7 @@ async def main_client() -> None: ) server_actor_run = await server_actor.start() - server_actor_container_url = server_actor_run['containerUrl'] + server_actor_container_url = server_actor_run.container_url server_actor_initialized = await server_actor.last_run().key_value_store().get_record('INITIALIZED') while not server_actor_initialized: @@ -456,12 +475,17 @@ async def main_client() -> None: run_input={'server_actor_container_url': server_actor_container_url}, ) - assert ac_run_result.status == 'SUCCEEDED' + assert ac_run_result.status.value == 'SUCCEEDED' + + sa_run_client = server_actor.last_run() + sa_run_client_run = await sa_run_client.wait_for_finish(wait_duration=timedelta(seconds=600)) + + if sa_run_client_run is None: + raise AssertionError('Failed to get server actor run after waiting for finish.') - sa_run_result_dict = await server_actor.last_run().wait_for_finish(wait_secs=600) - sa_run_result = ActorRun.model_validate(sa_run_result_dict) + sa_run_result = ActorRun.from_client_actor_run(sa_run_client_run) - assert sa_run_result.status == 'SUCCEEDED' + assert sa_run_result.status.value == 'SUCCEEDED' webhook_body_record = await server_actor.last_run().key_value_store().get_record('WEBHOOK_BODY') assert webhook_body_record is not None diff --git a/tests/e2e/test_actor_call_timeouts.py b/tests/e2e/test_actor_call_timeouts.py index abd754ce..796d6a9b 100644 --- a/tests/e2e/test_actor_call_timeouts.py +++ b/tests/e2e/test_actor_call_timeouts.py @@ -56,7 +56,7 @@ async def main() -> None: actor = await make_actor(label='inherit-timeout', main_func=main) run_result = await run_actor(actor) - assert run_result.status == 'SUCCEEDED' + assert run_result.status.value == 'SUCCEEDED' async def test_actor_call_inherit_timeout( @@ -108,4 +108,4 @@ async def main() -> None: actor = await make_actor(label='remaining-timeout', main_func=main) run_result = await run_actor(actor) - assert run_result.status == 'SUCCEEDED' + assert run_result.status.value == 'SUCCEEDED' diff --git a/tests/e2e/test_actor_charge.py b/tests/e2e/test_actor_charge.py index 0e2e98a0..51d93b0a 100644 --- a/tests/e2e/test_actor_charge.py +++ b/tests/e2e/test_actor_charge.py @@ -6,8 +6,6 @@ import pytest_asyncio -from apify_shared.consts import ActorJobStatus - from apify import Actor from apify._models import ActorRun @@ -15,7 +13,7 @@ from collections.abc import Iterable from apify_client import ApifyClientAsync - from apify_client.clients import ActorClientAsync + from apify_client._resource_clients import ActorClientAsync from .conftest import MakeActorFunction, RunActorFunction @@ -95,13 +93,13 @@ async def main() -> None: }, }, }, - ] + ], ) actor = await actor_client.get() assert actor is not None - return str(actor['id']) + return str(actor.id) @pytest_asyncio.fixture(scope='function', loop_scope='module') @@ -129,11 +127,15 @@ async def test_actor_charge_basic( # Refetch until the platform gets its act together for is_last_attempt, _ in retry_counter(30): await asyncio.sleep(1) - updated_run = await apify_client_async.run(run.id).get() - run = ActorRun.model_validate(updated_run) + + run_client = apify_client_async.run(run.id) + updated_run = await run_client.get() + assert updated_run is not None, 'Updated run should not be None' + + run = ActorRun.from_client_actor_run(updated_run) try: - assert run.status == ActorJobStatus.SUCCEEDED + assert run.status.value == 'SUCCEEDED' assert run.charged_event_counts == {'foobar': 4} break except AssertionError: @@ -146,17 +148,21 @@ async def test_actor_charge_limit( run_actor: RunActorFunction, apify_client_async: ApifyClientAsync, ) -> None: - run = await run_actor(ppe_actor, max_total_charge_usd=Decimal('0.2')) + run_result = await run_actor(ppe_actor, max_total_charge_usd=Decimal('0.2')) # Refetch until the platform gets its act together for is_last_attempt, _ in retry_counter(30): await asyncio.sleep(1) - updated_run = await apify_client_async.run(run.id).get() - run = ActorRun.model_validate(updated_run) + + run_client = apify_client_async.run(run_result.id) + updated_run = await run_client.get() + assert updated_run is not None, 'Updated run should not be None' + + run_result = ActorRun.from_client_actor_run(updated_run) try: - assert run.status == ActorJobStatus.SUCCEEDED - assert run.charged_event_counts == {'foobar': 2} + assert run_result.status.value == 'SUCCEEDED' + assert run_result.charged_event_counts == {'foobar': 2} break except AssertionError: if is_last_attempt: diff --git a/tests/e2e/test_actor_create_proxy_configuration.py b/tests/e2e/test_actor_create_proxy_configuration.py index 9ed60704..7d011dcf 100644 --- a/tests/e2e/test_actor_create_proxy_configuration.py +++ b/tests/e2e/test_actor_create_proxy_configuration.py @@ -30,7 +30,7 @@ async def main() -> None: actor = await make_actor(label='proxy-configuration', main_func=main) run_result = await run_actor(actor) - assert run_result.status == 'SUCCEEDED' + assert run_result.status.value == 'SUCCEEDED' async def test_create_proxy_configuration_with_groups_and_country( @@ -70,4 +70,4 @@ async def main() -> None: actor = await make_actor(label='proxy-configuration', main_func=main) run_result = await run_actor(actor) - assert run_result.status == 'SUCCEEDED' + assert run_result.status.value == 'SUCCEEDED' diff --git a/tests/e2e/test_actor_events.py b/tests/e2e/test_actor_events.py index ce2bf399..0617f0e0 100644 --- a/tests/e2e/test_actor_events.py +++ b/tests/e2e/test_actor_events.py @@ -60,7 +60,7 @@ async def log_event(data: Any) -> None: actor = await make_actor(label='actor-interval-events', main_func=main) run_result = await run_actor(actor) - assert run_result.status == 'SUCCEEDED' + assert run_result.status.value == 'SUCCEEDED' dataset_items_page = await actor.last_run().dataset().list_items() persist_state_events = [ @@ -71,3 +71,38 @@ async def log_event(data: Any) -> None: ] assert len(persist_state_events) > 2 assert len(system_info_events) > 0 + + +async def test_event_listener_can_be_removed_successfully( + make_actor: MakeActorFunction, + run_actor: RunActorFunction, +) -> None: + async def main() -> None: + import os + from typing import Any + + from apify_shared.consts import ApifyEnvVars + from crawlee.events._types import Event + + os.environ[ApifyEnvVars.PERSIST_STATE_INTERVAL_MILLIS] = '100' + + counter = 0 + + def count_event(data: Any) -> None: + nonlocal counter + print(data) + counter += 1 + + async with Actor: + Actor.on(Event.PERSIST_STATE, count_event) + await asyncio.sleep(0.5) + assert counter > 1 + last_count = counter + Actor.off(Event.PERSIST_STATE, count_event) + await asyncio.sleep(0.5) + assert counter == last_count + + actor = await make_actor(label='actor-off-event', main_func=main) + run_result = await run_actor(actor) + + assert run_result.status.value == 'SUCCEEDED' diff --git a/tests/e2e/test_actor_lifecycle.py b/tests/e2e/test_actor_lifecycle.py index 983b8ca3..b4fbffea 100644 --- a/tests/e2e/test_actor_lifecycle.py +++ b/tests/e2e/test_actor_lifecycle.py @@ -8,6 +8,56 @@ from .conftest import MakeActorFunction, RunActorFunction +async def test_actor_init_and_double_init_prevention( + make_actor: MakeActorFunction, + run_actor: RunActorFunction, +) -> None: + async def main() -> None: + my_actor = Actor + await my_actor.init() + assert my_actor._is_initialized is True + double_init = False + try: + await my_actor.init() + double_init = True + except RuntimeError as err: + assert str(err) == 'The Actor was already initialized!' # noqa: PT017 + except Exception: + raise + try: + await Actor.init() + double_init = True + except RuntimeError as err: + assert str(err) == 'The Actor was already initialized!' # noqa: PT017 + except Exception: + raise + await my_actor.exit() + assert double_init is False + assert my_actor._is_initialized is False + + actor = await make_actor(label='actor-init', main_func=main) + run_result = await run_actor(actor) + + assert run_result.status.value == 'SUCCEEDED' + + +async def test_actor_init_correctly_in_async_with_block( + make_actor: MakeActorFunction, + run_actor: RunActorFunction, +) -> None: + async def main() -> None: + import apify._actor + + async with Actor: + assert apify._actor.Actor._is_initialized + assert apify._actor.Actor._is_initialized is False + + actor = await make_actor(label='with-actor-init', main_func=main) + run_result = await run_actor(actor) + + assert run_result.status.value == 'SUCCEEDED' + + async def test_actor_exit_with_different_exit_codes( make_actor: MakeActorFunction, run_actor: RunActorFunction, @@ -23,7 +73,7 @@ async def main() -> None: run_result = await run_actor(actor, run_input={'exit_code': exit_code}) assert run_result.exit_code == exit_code - assert run_result.status == 'FAILED' if exit_code > 0 else 'SUCCEEDED' + assert run_result.status.value == 'FAILED' if exit_code > 0 else 'SUCCEEDED' async def test_actor_fail_with_custom_exit_codes_and_status_messages( @@ -39,18 +89,18 @@ async def main() -> None: run_result = await run_actor(actor) assert run_result.exit_code == 1 - assert run_result.status == 'FAILED' + assert run_result.status.value == 'FAILED' for exit_code in [1, 10, 100]: run_result = await run_actor(actor, run_input={'exit_code': exit_code}) assert run_result.exit_code == exit_code - assert run_result.status == 'FAILED' + assert run_result.status.value == 'FAILED' # Fail with a status message. run_result = await run_actor(actor, run_input={'status_message': 'This is a test message'}) - assert run_result.status == 'FAILED' + assert run_result.status.value == 'FAILED' assert run_result.status_message == 'This is a test message' @@ -66,7 +116,7 @@ async def main() -> None: run_result = await run_actor(actor) assert run_result.exit_code == 91 - assert run_result.status == 'FAILED' + assert run_result.status.value == 'FAILED' async def test_actor_with_crawler_reboot(make_actor: MakeActorFunction, run_actor: RunActorFunction) -> None: @@ -86,8 +136,8 @@ async def main() -> None: requests = ['https://example.com/1', 'https://example.com/2'] run = await Actor.apify_client.run(Actor.configuration.actor_run_id or '').get() - assert run - first_run = run.get('stats', {}).get('rebootCount', 0) == 0 + assert run is not None + first_run = run.stats.reboot_count == 0 @crawler.router.default_handler async def default_handler(context: BasicCrawlingContext) -> None: @@ -109,7 +159,7 @@ async def default_handler(context: BasicCrawlingContext) -> None: actor = await make_actor(label='migration', main_func=main) run_result = await run_actor(actor) - assert run_result.status == 'SUCCEEDED' + assert run_result.status.value == 'SUCCEEDED' async def test_actor_sequential_contexts(make_actor: MakeActorFunction, run_actor: RunActorFunction) -> None: @@ -140,4 +190,4 @@ async def main() -> None: actor = await make_actor(label='actor-sequential-contexts', main_func=main) run_result = await run_actor(actor) - assert run_result.status == 'SUCCEEDED' + assert run_result.status.value == 'SUCCEEDED' diff --git a/tests/e2e/test_actor_log.py b/tests/e2e/test_actor_log.py index 9d80bc90..767539e0 100644 --- a/tests/e2e/test_actor_log.py +++ b/tests/e2e/test_actor_log.py @@ -43,7 +43,7 @@ async def main() -> None: actor = await make_actor(label='actor-log', main_func=main) run_result = await run_actor(actor) - assert run_result.status == 'FAILED' + assert run_result.status.value == 'FAILED' run_log = await actor.last_run().log().get() assert run_log is not None diff --git a/tests/e2e/test_actor_request_queue.py b/tests/e2e/test_actor_request_queue.py index 81f75919..45efcae4 100644 --- a/tests/e2e/test_actor_request_queue.py +++ b/tests/e2e/test_actor_request_queue.py @@ -1,5 +1,6 @@ from __future__ import annotations +from datetime import timedelta from typing import TYPE_CHECKING from apify import Actor @@ -32,14 +33,17 @@ async def main() -> None: actor = await make_actor(label='rq-clients-resurrection', main_func=main) run_result = await run_actor(actor) assert run_result.status == 'SUCCEEDED' - # Resurrect the run, the RequestQueue should still use same client key and thus not have multiple clients. run_client = apify_client_async.run(run_id=run_result.id) # Redirect logs even from the resurrected run streamed_log = await run_client.get_streamed_log(from_start=False) await run_client.resurrect() + async with streamed_log: - run_result = ActorRun.model_validate(await run_client.wait_for_finish(wait_secs=600)) + raw_run_result = await run_client.wait_for_finish(wait_duration=timedelta(seconds=600)) + assert raw_run_result is not None + + run_result = ActorRun.from_client_actor_run(raw_run_result) assert run_result.status == 'SUCCEEDED' diff --git a/tests/e2e/test_actor_scrapy.py b/tests/e2e/test_actor_scrapy.py index c7327b58..08bd60e8 100644 --- a/tests/e2e/test_actor_scrapy.py +++ b/tests/e2e/test_actor_scrapy.py @@ -3,7 +3,7 @@ from pathlib import Path from typing import TYPE_CHECKING -from apify_shared.consts import ActorPermissionLevel +from apify_client._models import ActorPermissionLevel if TYPE_CHECKING: from .conftest import MakeActorFunction, RunActorFunction @@ -40,7 +40,7 @@ async def test_actor_scrapy_title_spider( force_permission_level=ActorPermissionLevel.FULL_PERMISSIONS, ) - assert run_result.status == 'SUCCEEDED' + assert run_result.status.value == 'SUCCEEDED' items = await actor.last_run().dataset().list_items() diff --git a/tests/e2e/test_apify_storages.py b/tests/e2e/test_apify_storages.py index f3f3696a..4082727c 100644 --- a/tests/e2e/test_apify_storages.py +++ b/tests/e2e/test_apify_storages.py @@ -25,4 +25,4 @@ async def main() -> None: actor = await make_actor(label='explicit_storage_init', main_func=main) run_result = await run_actor(actor) - assert run_result.status == 'SUCCEEDED' + assert run_result.status.value == 'SUCCEEDED' diff --git a/tests/e2e/test_crawlee/conftest.py b/tests/e2e/test_crawlee/conftest.py index 9965e5cc..f074e3ae 100644 --- a/tests/e2e/test_crawlee/conftest.py +++ b/tests/e2e/test_crawlee/conftest.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from apify_client.clients.resource_clients import ActorClientAsync + from apify_client._resource_clients import ActorClientAsync from apify._models import ActorRun diff --git a/tests/e2e/test_fixtures.py b/tests/e2e/test_fixtures.py index 865effa9..d6735b3e 100644 --- a/tests/e2e/test_fixtures.py +++ b/tests/e2e/test_fixtures.py @@ -28,7 +28,7 @@ async def main() -> None: actor = await make_actor(label='make-actor-main-func', main_func=main) run_result = await run_actor(actor) - assert run_result.status == 'SUCCEEDED' + assert run_result.status.value == 'SUCCEEDED' output_record = await actor.last_run().key_value_store().get_record('OUTPUT') @@ -52,7 +52,7 @@ async def main(): actor = await make_actor(label='make-actor-main-py', main_py=main_py_source) run_result = await run_actor(actor) - assert run_result.status == 'SUCCEEDED' + assert run_result.status.value == 'SUCCEEDED' output_record = await actor.last_run().key_value_store().get_record('OUTPUT') @@ -86,7 +86,7 @@ async def main(): actor = await make_actor(label='make-actor-source-files', source_files=actor_source_files) run_result = await run_actor(actor) - assert run_result.status == 'SUCCEEDED' + assert run_result.status.value == 'SUCCEEDED' output_record = await actor.last_run().key_value_store().get_record('OUTPUT') assert output_record is not None diff --git a/tests/e2e/test_scrapy/conftest.py b/tests/e2e/test_scrapy/conftest.py index e19f6c36..e58b8d55 100644 --- a/tests/e2e/test_scrapy/conftest.py +++ b/tests/e2e/test_scrapy/conftest.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from apify_client.clients.resource_clients import ActorClientAsync + from apify_client._resource_clients import ActorClientAsync from apify._models import ActorRun diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 30aa077d..ee67aa90 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -37,8 +37,7 @@ def apify_token() -> str: def apify_client_async(apify_token: str) -> ApifyClientAsync: """Create an instance of the ApifyClientAsync.""" api_url = os.getenv(_API_URL_ENV_VAR) - - return ApifyClientAsync(apify_token, api_url=api_url) + return ApifyClientAsync(apify_token) if api_url is None else ApifyClientAsync(apify_token, api_url=api_url) @pytest.fixture diff --git a/tests/integration/test_dataset.py b/tests/integration/test_dataset.py index 5a5d1b92..63598023 100644 --- a/tests/integration/test_dataset.py +++ b/tests/integration/test_dataset.py @@ -117,7 +117,7 @@ async def test_force_cloud( try: dataset_details = await dataset_client.get() assert dataset_details is not None - assert dataset_details.get('name') == dataset_name + assert dataset_details.name == dataset_name dataset_items = await dataset_client.list_items() assert dataset_items.items == [dataset_item] diff --git a/tests/integration/test_key_value_store.py b/tests/integration/test_key_value_store.py index 912ecfb0..8b94acd4 100644 --- a/tests/integration/test_key_value_store.py +++ b/tests/integration/test_key_value_store.py @@ -156,7 +156,7 @@ async def test_force_cloud( try: key_value_store_details = await key_value_store_client.get() assert key_value_store_details is not None - assert key_value_store_details.get('name') == key_value_store_name + assert key_value_store_details.name == key_value_store_name key_value_store_record = await key_value_store_client.get_record('foo') assert key_value_store_record is not None diff --git a/tests/integration/test_request_queue.py b/tests/integration/test_request_queue.py index 25f4caf6..5f088513 100644 --- a/tests/integration/test_request_queue.py +++ b/tests/integration/test_request_queue.py @@ -8,6 +8,7 @@ import pytest +from apify_client._models import BatchAddResult, RequestDraft from apify_shared.consts import ApifyEnvVars from crawlee import service_locator from crawlee.crawlers import BasicCrawler @@ -881,7 +882,7 @@ async def test_request_queue_had_multiple_clients( # Check that it is correctly in the API api_response = await api_client.get() assert api_response - assert api_response['hadMultipleClients'] is True + assert api_response.had_multiple_clients is True async def test_request_queue_not_had_multiple_clients( @@ -900,7 +901,7 @@ async def test_request_queue_not_had_multiple_clients( api_client = apify_client_async.request_queue(request_queue_id=rq.id) api_response = await api_client.get() assert api_response - assert api_response['hadMultipleClients'] is False + assert api_response.had_multiple_clients is False async def test_request_queue_simple_and_full_at_the_same_time( @@ -1095,11 +1096,11 @@ async def test_force_cloud( request_queue_details = await request_queue_client.get() assert request_queue_details is not None - assert request_queue_details.get('name') == request_queue_apify.name + assert request_queue_details.name == request_queue_apify.name request_queue_request = await request_queue_client.get_request(request_info.id) assert request_queue_request is not None - assert request_queue_request['url'] == 'http://example.com' + assert str(request_queue_request.url) == 'http://example.com/' async def test_request_queue_is_finished( @@ -1137,22 +1138,31 @@ async def test_request_queue_deduplication_unprocessed_requests( # Get raw client, because stats are not exposed in `RequestQueue` class, but are available in raw client rq_client = Actor.apify_client.request_queue(request_queue_id=request_queue_apify.id) _rq = await rq_client.get() - assert _rq - stats_before = _rq.get('stats', {}) + assert _rq is not None + stats_before = _rq.stats Actor.log.info(stats_before) - def return_unprocessed_requests(requests: list[dict], *_: Any, **__: Any) -> dict[str, list[dict]]: + assert stats_before is not None + assert stats_before.write_count is not None + + def return_unprocessed_requests(requests: list[dict], *_: Any, **__: Any) -> BatchAddResult: """Simulate API returning unprocessed requests.""" - return { - 'processedRequests': [], - 'unprocessedRequests': [ - {'url': request['url'], 'uniqueKey': request['uniqueKey'], 'method': request['method']} - for request in requests - ], - } + unprocessed_requests = [ + RequestDraft.model_construct( + url=request['url'], + unique_key=request['uniqueKey'], + method=request['method'], + ) + for request in requests + ] + + return BatchAddResult.model_construct( + processed_requests=[], + unprocessed_requests=unprocessed_requests, + ) with mock.patch( - 'apify_client.clients.resource_clients.request_queue.RequestQueueClientAsync.batch_add_requests', + 'apify_client._resource_clients.request_queue.RequestQueueClientAsync.batch_add_requests', side_effect=return_unprocessed_requests, ): # Simulate failed API call for adding requests. Request was not processed and should not be cached. @@ -1164,15 +1174,16 @@ def return_unprocessed_requests(requests: list[dict], *_: Any, **__: Any) -> dic # Poll until stats reflect the successful write. async def _get_rq_stats() -> dict: result = await rq_client.get() - return (result or {}).get('stats', {}) + return result.stats.model_dump(by_alias=True) if result and result.stats else {} - stats_after = await poll_until_condition( + _stats_before = stats_before.model_dump(by_alias=True) if stats_before else {} + stats_after_dict = await poll_until_condition( _get_rq_stats, - lambda s: s.get('writeCount', 0) - stats_before.get('writeCount', 0) >= 1, + lambda s: s.get('writeCount', 0) - _stats_before.get('writeCount', 0) >= 1, ) - Actor.log.info(stats_after) + Actor.log.info(stats_after_dict) - assert (stats_after['writeCount'] - stats_before['writeCount']) == 1 + assert (stats_after_dict['writeCount'] - _stats_before['writeCount']) == 1 async def test_request_queue_api_fail_when_marking_as_handled( @@ -1261,7 +1272,7 @@ async def test_request_queue_deduplication( rq_client = apify_client_async.request_queue(request_queue_id=rq.id) _rq = await rq_client.get() assert _rq - stats_before = _rq.get('stats', {}) + stats_before = _rq.stats # Add same request twice (same unique_key because same URL with default unique key) request1 = Request.from_url('http://example.com', method='POST') @@ -1270,16 +1281,17 @@ async def test_request_queue_deduplication( await rq.add_request(request2) # Poll until stats reflect the write. - async def _get_rq_stats() -> dict: + async def _get_rq_stats_dedup() -> dict: result = await rq_client.get() - return (result or {}).get('stats', {}) + return result.stats.model_dump(by_alias=True) if result and result.stats else {} - stats_after = await poll_until_condition( - _get_rq_stats, - lambda s: s.get('writeCount', 0) - stats_before.get('writeCount', 0) >= 1, + _stats_before_dedup = stats_before.model_dump(by_alias=True) if stats_before else {} + stats_after_dict = await poll_until_condition( + _get_rq_stats_dedup, + lambda s: s.get('writeCount', 0) - _stats_before_dedup.get('writeCount', 0) >= 1, ) - assert (stats_after['writeCount'] - stats_before['writeCount']) == 1 + assert (stats_after_dict['writeCount'] - _stats_before_dedup['writeCount']) == 1 async def test_request_queue_deduplication_use_extended_unique_key( @@ -1294,7 +1306,7 @@ async def test_request_queue_deduplication_use_extended_unique_key( rq_client = apify_client_async.request_queue(request_queue_id=rq.id) _rq = await rq_client.get() assert _rq - stats_before = _rq.get('stats', {}) + stats_before = _rq.stats request1 = Request.from_url('http://example.com', method='POST', use_extended_unique_key=True) request2 = Request.from_url('http://example.com', method='GET', use_extended_unique_key=True) @@ -1302,16 +1314,17 @@ async def test_request_queue_deduplication_use_extended_unique_key( await rq.add_request(request2) # Poll until stats reflect both writes. - async def _get_rq_stats() -> dict: + async def _get_rq_stats_ext() -> dict: result = await rq_client.get() - return (result or {}).get('stats', {}) + return result.stats.model_dump(by_alias=True) if result and result.stats else {} - stats_after = await poll_until_condition( - _get_rq_stats, - lambda s: s.get('writeCount', 0) - stats_before.get('writeCount', 0) >= 2, + _stats_before_ext = stats_before.model_dump(by_alias=True) if stats_before else {} + stats_after_dict = await poll_until_condition( + _get_rq_stats_ext, + lambda s: s.get('writeCount', 0) - _stats_before_ext.get('writeCount', 0) >= 2, ) - assert (stats_after['writeCount'] - stats_before['writeCount']) == 2 + assert (stats_after_dict['writeCount'] - _stats_before_ext['writeCount']) == 2 async def test_request_queue_parallel_deduplication( @@ -1328,7 +1341,7 @@ async def test_request_queue_parallel_deduplication( rq_client = apify_client_async.request_queue(request_queue_id=rq.id) _rq = await rq_client.get() assert _rq - stats_before = _rq.get('stats', {}) + stats_before = _rq.stats requests = [Request.from_url(f'http://example.com/{i}') for i in range(max_requests)] batch_size = iter(range(10, max_requests + 1, int(max_requests / worker_count))) @@ -1340,16 +1353,17 @@ async def add_requests_worker() -> None: await asyncio.gather(*add_requests_workers) # Poll until stats reflect all written requests. - async def _get_rq_stats() -> dict: + async def _get_rq_stats_concurrent() -> dict: result = await rq_client.get() - return (result or {}).get('stats', {}) + return result.stats.model_dump(by_alias=True) if result and result.stats else {} - stats_after = await poll_until_condition( - _get_rq_stats, - lambda s: s.get('writeCount', 0) - stats_before.get('writeCount', 0) >= len(requests), + _stats_before_concurrent = stats_before.model_dump(by_alias=True) if stats_before else {} + stats_after_dict = await poll_until_condition( + _get_rq_stats_concurrent, + lambda s: s.get('writeCount', 0) - _stats_before_concurrent.get('writeCount', 0) >= len(requests), ) - assert (stats_after['writeCount'] - stats_before['writeCount']) == len(requests) + assert (stats_after_dict['writeCount'] - _stats_before_concurrent['writeCount']) == len(requests) async def test_concurrent_processing_simulation(apify_token: str, monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/tests/unit/actor/test_actor_helpers.py b/tests/unit/actor/test_actor_helpers.py index 2cf84c51..10227ad2 100644 --- a/tests/unit/actor/test_actor_helpers.py +++ b/tests/unit/actor/test_actor_helpers.py @@ -9,7 +9,8 @@ import pytest from apify_client import ApifyClientAsync -from apify_shared.consts import ApifyEnvVars, WebhookEventType +from apify_client._models import Run, WebhookEventType +from apify_shared.consts import ApifyEnvVars from crawlee.events._types import Event from apify import Actor, Webhook @@ -20,45 +21,48 @@ @pytest.fixture -def fake_actor_run() -> dict: - return { - 'id': 'asdfasdf', - 'buildId': '3ads35', - 'buildNumber': '3.4.5', - 'actId': 'actor_id', - 'actorId': 'actor_id', - 'userId': 'user_id', - 'startedAt': '2024-08-08 12:12:44', - 'status': 'RUNNING', - 'meta': {'origin': 'API'}, - 'containerUrl': 'http://0.0.0.0:3333', - 'defaultDatasetId': 'dhasdrfughaerguoi', - 'defaultKeyValueStoreId': 'asjkldhguiofg', - 'defaultRequestQueueId': 'lkjgklserjghios', - 'stats': { - 'inputBodyLen': 0, - 'restartCount': 0, - 'resurrectCount': 0, - 'memAvgBytes': 0, - 'memMaxBytes': 0, - 'memCurrentBytes': 0, - 'cpuAvgUsage': 0, - 'cpuMaxUsage': 0, - 'cpuCurrentUsage': 0, - 'netRxBytes': 0, - 'netTxBytes': 0, - 'durationMillis': 3333, - 'runTimeSecs': 33, - 'metamorph': 0, - 'computeUnits': 4.33, - }, - 'options': { - 'build': '', - 'timeoutSecs': 44, - 'memoryMbytes': 4096, - 'diskMbytes': 16384, - }, - } +def fake_actor_run() -> Run: + return Run.model_validate( + { + 'id': 'asdfasdf', + 'buildId': '3ads35', + 'buildNumber': '3.4.5', + 'actId': 'actor_id', + 'actorId': 'actor_id', + 'userId': 'user_id', + 'startedAt': '2024-08-08T12:12:44Z', + 'status': 'RUNNING', + 'meta': {'origin': 'API'}, + 'containerUrl': 'http://0.0.0.0:3333', + 'defaultDatasetId': 'dhasdrfughaerguoi', + 'defaultKeyValueStoreId': 'asjkldhguiofg', + 'defaultRequestQueueId': 'lkjgklserjghios', + 'generalAccess': 'RESTRICTED', + 'stats': { + 'inputBodyLen': 0, + 'restartCount': 0, + 'resurrectCount': 0, + 'memAvgBytes': 0, + 'memMaxBytes': 0, + 'memCurrentBytes': 0, + 'cpuAvgUsage': 0, + 'cpuMaxUsage': 0, + 'cpuCurrentUsage': 0, + 'netRxBytes': 0, + 'netTxBytes': 0, + 'durationMillis': 3333, + 'runTimeSecs': 33, + 'metamorph': 0, + 'computeUnits': 4.33, + }, + 'options': { + 'build': '', + 'timeoutSecs': 44, + 'memoryMbytes': 4096, + 'diskMbytes': 16384, + }, + } + ) async def test_new_client_config_creation(monkeypatch: pytest.MonkeyPatch) -> None: @@ -79,7 +83,7 @@ async def test_new_client_config_creation(monkeypatch: pytest.MonkeyPatch) -> No await my_actor.exit() -async def test_call_actor(apify_client_async_patcher: ApifyClientAsyncPatcher, fake_actor_run: dict) -> None: +async def test_call_actor(apify_client_async_patcher: ApifyClientAsyncPatcher, fake_actor_run: Run) -> None: apify_client_async_patcher.patch('actor', 'call', return_value=fake_actor_run) actor_id = 'some-actor-id' @@ -91,7 +95,7 @@ async def test_call_actor(apify_client_async_patcher: ApifyClientAsyncPatcher, f assert apify_client_async_patcher.calls['actor']['call'][0][0][0].resource_id == actor_id -async def test_call_actor_task(apify_client_async_patcher: ApifyClientAsyncPatcher, fake_actor_run: dict) -> None: +async def test_call_actor_task(apify_client_async_patcher: ApifyClientAsyncPatcher, fake_actor_run: Run) -> None: apify_client_async_patcher.patch('task', 'call', return_value=fake_actor_run) task_id = 'some-task-id' @@ -102,7 +106,7 @@ async def test_call_actor_task(apify_client_async_patcher: ApifyClientAsyncPatch assert apify_client_async_patcher.calls['task']['call'][0][0][0].resource_id == task_id -async def test_start_actor(apify_client_async_patcher: ApifyClientAsyncPatcher, fake_actor_run: dict) -> None: +async def test_start_actor(apify_client_async_patcher: ApifyClientAsyncPatcher, fake_actor_run: Run) -> None: apify_client_async_patcher.patch('actor', 'start', return_value=fake_actor_run) actor_id = 'some-id' @@ -113,7 +117,7 @@ async def test_start_actor(apify_client_async_patcher: ApifyClientAsyncPatcher, assert apify_client_async_patcher.calls['actor']['start'][0][0][0].resource_id == actor_id -async def test_abort_actor_run(apify_client_async_patcher: ApifyClientAsyncPatcher, fake_actor_run: dict) -> None: +async def test_abort_actor_run(apify_client_async_patcher: ApifyClientAsyncPatcher, fake_actor_run: Run) -> None: apify_client_async_patcher.patch('run', 'abort', return_value=fake_actor_run) run_id = 'some-run-id' @@ -153,7 +157,7 @@ async def test_add_webhook_fails_locally(caplog: pytest.LogCaptureFixture) -> No caplog.set_level('WARNING') async with Actor: await Actor.add_webhook( - Webhook(event_types=[WebhookEventType.ACTOR_BUILD_ABORTED], request_url='https://example.com') + Webhook(event_types=[WebhookEventType.ACTOR_BUILD_ABORTED.value], request_url='https://example.com') ) matching = [r for r in caplog.records if 'Actor.add_webhook()' in r.message] @@ -264,7 +268,7 @@ async def test_remote_method_with_timedelta_timeout( calls = apify_client_async_patcher.calls[client_resource][client_method] assert len(calls) == 1 _, kwargs = calls[0][0], calls[0][1] - assert kwargs.get('timeout_secs') == 120 + assert kwargs.get('timeout') == timedelta(seconds=120) async def test_call_actor_with_remaining_time_deprecation( diff --git a/tests/unit/actor/test_actor_lifecycle.py b/tests/unit/actor/test_actor_lifecycle.py index 03fdd00e..ed5358ea 100644 --- a/tests/unit/actor/test_actor_lifecycle.py +++ b/tests/unit/actor/test_actor_lifecycle.py @@ -2,14 +2,20 @@ import asyncio import contextlib +import json import logging -from typing import TYPE_CHECKING -from unittest.mock import AsyncMock +from datetime import datetime, timezone +from typing import TYPE_CHECKING, Any +from unittest import mock +from unittest.mock import AsyncMock, Mock import pytest +import websockets +import websockets.asyncio.server -from apify_shared.consts import ActorExitCodes, ApifyEnvVars -from crawlee.events._types import Event +from apify_client._models import Run +from apify_shared.consts import ActorEnvVars, ActorExitCodes, ApifyEnvVars +from crawlee.events._types import Event, EventPersistStateData from apify import Actor @@ -212,6 +218,90 @@ def on_event(event_type: Event) -> Callable: assert on_system_info_count == len(on_system_info) +async def test_actor_handles_migrating_event_correctly(monkeypatch: pytest.MonkeyPatch) -> None: + """Test that Actor handles MIGRATING events correctly by emitting PERSIST_STATE.""" + # This should test whether when you get a MIGRATING event, + # the Actor automatically emits the PERSIST_STATE event with data `{'isMigrating': True}` + monkeypatch.setenv(ApifyEnvVars.IS_AT_HOME, '1') + monkeypatch.setenv(ActorEnvVars.RUN_ID, 'asdf') + + persist_state_events_data = [] + + def log_persist_state(data: Any) -> None: + nonlocal persist_state_events_data + persist_state_events_data.append(data) + + async def handler(websocket: websockets.asyncio.server.ServerConnection) -> None: + await websocket.wait_closed() + + async with websockets.asyncio.server.serve(handler, host='localhost') as ws_server: + port: int = ws_server.sockets[0].getsockname()[1] + monkeypatch.setenv(ActorEnvVars.EVENTS_WEBSOCKET_URL, f'ws://localhost:{port}') + + mock_run_client = Mock() + mock_run_client.run.return_value.get = AsyncMock( + side_effect=lambda: Run.model_validate( + { + 'id': 'asdf', + 'actId': 'asdf', + 'userId': 'adsf', + 'startedAt': datetime.now(timezone.utc).isoformat(), + 'status': 'RUNNING', + 'meta': {'origin': 'DEVELOPMENT'}, + 'buildId': 'hjkl', + 'defaultDatasetId': 'hjkl', + 'defaultKeyValueStoreId': 'hjkl', + 'defaultRequestQueueId': 'hjkl', + 'containerUrl': 'https://hjkl', + 'buildNumber': '0.0.1', + 'generalAccess': 'RESTRICTED', + 'stats': { + 'restartCount': 0, + 'resurrectCount': 0, + 'computeUnits': 1, + }, + 'options': { + 'build': 'asdf', + 'timeoutSecs': 4, + 'memoryMbytes': 1024, + 'diskMbytes': 1024, + }, + } + ) + ) + + with mock.patch.object(Actor, 'new_client', return_value=mock_run_client): + async with Actor: + Actor.on(Event.PERSIST_STATE, log_persist_state) + await asyncio.sleep(2) + + for socket in ws_server.connections: + await socket.send( + json.dumps( + { + 'name': 'migrating', + 'data': { + 'isMigrating': True, + }, + } + ) + ) + + await asyncio.sleep(1) + + # It is enough to check the persist state event we send manually and the crawler final one. + assert len(persist_state_events_data) >= 2 + + # Expect last event to be is_migrating=False (persistence event on exiting EventManager) + assert persist_state_events_data.pop() == EventPersistStateData(is_migrating=False) + # Expect second last event to be is_migrating=True (emitted on MIGRATING event) + assert persist_state_events_data.pop() == EventPersistStateData(is_migrating=True) + + # Check if all the other events are regular persist state events + for event_data in persist_state_events_data: + assert event_data == EventPersistStateData(is_migrating=False) + + async def test_actor_fail_prevents_further_execution(caplog: pytest.LogCaptureFixture) -> None: """Test that calling Actor.fail() prevents further code execution in the Actor context.""" caplog.set_level(logging.INFO) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 8d8297c5..94f1f5f2 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -62,7 +62,8 @@ def prepare_test_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Callabl def _prepare_test_env() -> None: if hasattr(apify._actor.Actor, '__wrapped__'): delattr(apify._actor.Actor, '__wrapped__') - apify._actor.Actor._active = False + + apify._actor.Actor._is_initialized = False # Set the environment variable for the local storage directory to the temporary path. monkeypatch.setenv(ApifyEnvVars.LOCAL_STORAGE_DIR, str(tmp_path)) diff --git a/tests/unit/storage_clients/test_apify_kvs_client.py b/tests/unit/storage_clients/test_apify_kvs_client.py index 4e5b4c6b..5bba9b7d 100644 --- a/tests/unit/storage_clients/test_apify_kvs_client.py +++ b/tests/unit/storage_clients/test_apify_kvs_client.py @@ -6,6 +6,8 @@ import pytest +from apify_client._models import ListOfKeys + from apify.storage_clients._apify._key_value_store_client import ApifyKeyValueStoreClient @@ -53,13 +55,18 @@ async def test_iterate_keys_single_page() -> None: """Test iterating keys with a single page of results.""" api_client = AsyncMock() api_client.list_keys = AsyncMock( - return_value={ - 'items': [{'key': 'key1', 'size': 100}, {'key': 'key2', 'size': 200}], - 'count': 2, - 'limit': 1000, - 'isTruncated': False, - 'nextExclusiveStartKey': None, - } + return_value=ListOfKeys.model_validate( + { + 'items': [ + {'key': 'key1', 'size': 100, 'recordPublicUrl': 'https://example.com/key1'}, + {'key': 'key2', 'size': 200, 'recordPublicUrl': 'https://example.com/key2'}, + ], + 'count': 2, + 'limit': 1000, + 'isTruncated': False, + 'nextExclusiveStartKey': None, + } + ) ) client, _ = _make_kvs_client(api_client=api_client) @@ -73,13 +80,17 @@ async def test_iterate_keys_with_limit() -> None: """Test that iterate_keys respects the limit parameter.""" api_client = AsyncMock() api_client.list_keys = AsyncMock( - return_value={ - 'items': [{'key': f'key{i}', 'size': 100} for i in range(5)], - 'count': 5, - 'limit': 1000, - 'isTruncated': True, - 'nextExclusiveStartKey': 'key4', - } + return_value=ListOfKeys.model_validate( + { + 'items': [ + {'key': f'key{i}', 'size': 100, 'recordPublicUrl': f'https://example.com/key{i}'} for i in range(5) + ], + 'count': 5, + 'limit': 1000, + 'isTruncated': True, + 'nextExclusiveStartKey': 'key4', + } + ) ) client, _ = _make_kvs_client(api_client=api_client) @@ -89,20 +100,24 @@ async def test_iterate_keys_with_limit() -> None: async def test_iterate_keys_pagination() -> None: """Test that iterate_keys handles pagination across multiple pages.""" - page1 = { - 'items': [{'key': 'key1', 'size': 100}], - 'count': 1, - 'limit': 1000, - 'isTruncated': True, - 'nextExclusiveStartKey': 'key1', - } - page2 = { - 'items': [{'key': 'key2', 'size': 200}], - 'count': 1, - 'limit': 1000, - 'isTruncated': False, - 'nextExclusiveStartKey': None, - } + page1 = ListOfKeys.model_validate( + { + 'items': [{'key': 'key1', 'size': 100, 'recordPublicUrl': 'https://example.com/key1'}], + 'count': 1, + 'limit': 1000, + 'isTruncated': True, + 'nextExclusiveStartKey': 'key1', + } + ) + page2 = ListOfKeys.model_validate( + { + 'items': [{'key': 'key2', 'size': 200, 'recordPublicUrl': 'https://example.com/key2'}], + 'count': 1, + 'limit': 1000, + 'isTruncated': False, + 'nextExclusiveStartKey': None, + } + ) api_client = AsyncMock() api_client.list_keys = AsyncMock(side_effect=[page1, page2]) client, _ = _make_kvs_client(api_client=api_client) diff --git a/uv.lock b/uv.lock index 9f152b5f..29217123 100644 --- a/uv.lock +++ b/uv.lock @@ -1,9 +1,9 @@ version = 1 revision = 3 -requires-python = ">=3.10" +requires-python = ">=3.11" [options] -exclude-newer = "2026-04-14T13:23:40.899705395Z" +exclude-newer = "2026-04-15T11:49:40.072177007Z" exclude-newer-span = "PT24H" [[package]] @@ -20,7 +20,6 @@ name = "anyio" version = "4.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] @@ -42,7 +41,7 @@ dependencies = [ { name = "impit" }, { name = "lazy-object-proxy" }, { name = "more-itertools" }, - { name = "pydantic" }, + { name = "pydantic", extra = ["email"] }, { name = "typing-extensions" }, { name = "websockets" }, { name = "yarl" }, @@ -79,7 +78,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "apify-client", specifier = ">=2.3.0,<3.0.0" }, + { name = "apify-client", git = "https://github.com/apify/apify-client-python.git?rev=master" }, { name = "apify-shared", specifier = ">=2.0.0,<3.0.0" }, { name = "cachetools", specifier = ">=5.5.0" }, { name = "crawlee", specifier = ">=1.0.4,<2.0.0" }, @@ -87,7 +86,7 @@ requires-dist = [ { name = "impit", specifier = ">=0.8.0" }, { name = "lazy-object-proxy", specifier = ">=1.11.0" }, { name = "more-itertools", specifier = ">=10.2.0" }, - { name = "pydantic", specifier = ">=2.11.0" }, + { name = "pydantic", extras = ["email"], specifier = ">=2.11.0" }, { name = "scrapy", marker = "extra == 'scrapy'", specifier = ">=2.14.0" }, { name = "typing-extensions", specifier = ">=4.1.0" }, { name = "websockets", specifier = ">=14.0" }, @@ -121,17 +120,13 @@ dev = [ [[package]] name = "apify-client" -version = "2.5.0" -source = { registry = "https://pypi.org/simple" } +version = "2.5.1" +source = { git = "https://github.com/apify/apify-client-python.git?rev=master#6eb9aeee9ce82d0787237aed9810d07f7164bb58" } dependencies = [ - { name = "apify-shared" }, { name = "colorama" }, { name = "impit" }, { name = "more-itertools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/78/6a/b872d6bbc84c6aaf27b455492c6ff1bd057fea302c5d40619c733d48a718/apify_client-2.5.0.tar.gz", hash = "sha256:daa2af6a50e573f78bd46a4728a3f2be76cee93cf5c4ff9d0fd38b6756792689", size = 377916, upload-time = "2026-02-18T13:03:16.083Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/82/4fe19adfa6b962ab8a740782b6246b7c499f13edccac24733f015d895725/apify_client-2.5.0-py3-none-any.whl", hash = "sha256:4aa6172bed92d83f2d2bbe1f95cfaab2e147a834dfa007e309fd0b4709423316", size = 86996, upload-time = "2026-02-18T13:03:14.891Z" }, + { name = "pydantic", extra = ["email"] }, ] [[package]] @@ -170,15 +165,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/ff/1175b0b7371e46244032d43a56862d0af455823b5280a50c63d99cc50f18/automat-25.4.16-py3-none-any.whl", hash = "sha256:04e9bce696a8d5671ee698005af6e5a9fa15354140a87f4870744604dcdd3ba1", size = 42842, upload-time = "2025-04-16T20:12:14.447Z" }, ] -[[package]] -name = "backports-asyncio-runner" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, -] - [[package]] name = "black" version = "26.3.1" @@ -190,16 +176,9 @@ dependencies = [ { name = "pathspec" }, { name = "platformdirs" }, { name = "pytokens" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/a8/11170031095655d36ebc6664fe0897866f6023892396900eec0e8fdc4299/black-26.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2", size = 1866562, upload-time = "2026-03-12T03:39:58.639Z" }, - { url = "https://files.pythonhosted.org/packages/69/ce/9e7548d719c3248c6c2abfd555d11169457cbd584d98d179111338423790/black-26.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b", size = 1703623, upload-time = "2026-03-12T03:40:00.347Z" }, - { url = "https://files.pythonhosted.org/packages/7f/0a/8d17d1a9c06f88d3d030d0b1d4373c1551146e252afe4547ed601c0e697f/black-26.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c54a4a82e291a1fee5137371ab488866b7c86a3305af4026bdd4dc78642e1ac", size = 1768388, upload-time = "2026-03-12T03:40:01.765Z" }, - { url = "https://files.pythonhosted.org/packages/52/79/c1ee726e221c863cde5164f925bacf183dfdf0397d4e3f94889439b947b4/black-26.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:6e131579c243c98f35bce64a7e08e87fb2d610544754675d4a0e73a070a5aa3a", size = 1412969, upload-time = "2026-03-12T03:40:03.252Z" }, - { url = "https://files.pythonhosted.org/packages/73/a5/15c01d613f5756f68ed8f6d4ec0a1e24b82b18889fa71affd3d1f7fad058/black-26.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:5ed0ca58586c8d9a487352a96b15272b7fa55d139fc8496b519e78023a8dab0a", size = 1220345, upload-time = "2026-03-12T03:40:04.892Z" }, { url = "https://files.pythonhosted.org/packages/17/57/5f11c92861f9c92eb9dddf515530bc2d06db843e44bdcf1c83c1427824bc/black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff", size = 1851987, upload-time = "2026-03-12T03:40:06.248Z" }, { url = "https://files.pythonhosted.org/packages/54/aa/340a1463660bf6831f9e39646bf774086dbd8ca7fc3cded9d59bbdf4ad0a/black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c", size = 1689499, upload-time = "2026-03-12T03:40:07.642Z" }, { url = "https://files.pythonhosted.org/packages/f3/01/b726c93d717d72733da031d2de10b92c9fa4c8d0c67e8a8a372076579279/black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5", size = 1754369, upload-time = "2026-03-12T03:40:09.279Z" }, @@ -229,10 +208,8 @@ version = "1.4.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "os_name == 'nt'" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, { name = "packaging" }, { name = "pyproject-hooks" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3f/16/4b272700dea44c1d2e8ca963ebb3c684efe22b3eba8cfa31c5fdb60de707/build-1.4.3.tar.gz", hash = "sha256:5aa4231ae0e807efdf1fd0623e07366eca2ab215921345a2e38acdd5d0fa0a74", size = 89314, upload-time = "2026-04-10T21:25:40.857Z" } wheels = [ @@ -266,18 +243,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, - { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, - { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, - { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, - { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, - { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, - { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, - { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, - { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, - { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, @@ -354,22 +319,6 @@ version = "3.4.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, - { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, - { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, - { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, - { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, - { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, - { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, - { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, - { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, - { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, - { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, - { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, - { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, @@ -489,20 +438,6 @@ version = "7.13.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/33/e8c48488c29a73fd089f9d71f9653c1be7478f2ad6b5bc870db11a55d23d/coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5", size = 219255, upload-time = "2026-03-17T10:29:51.081Z" }, - { url = "https://files.pythonhosted.org/packages/da/bd/b0ebe9f677d7f4b74a3e115eec7ddd4bcf892074963a00d91e8b164a6386/coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf", size = 219772, upload-time = "2026-03-17T10:29:52.867Z" }, - { url = "https://files.pythonhosted.org/packages/48/cc/5cb9502f4e01972f54eedd48218bb203fe81e294be606a2bc93970208013/coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8", size = 246532, upload-time = "2026-03-17T10:29:54.688Z" }, - { url = "https://files.pythonhosted.org/packages/7d/d8/3217636d86c7e7b12e126e4f30ef1581047da73140614523af7495ed5f2d/coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4", size = 248333, upload-time = "2026-03-17T10:29:56.221Z" }, - { url = "https://files.pythonhosted.org/packages/2b/30/2002ac6729ba2d4357438e2ed3c447ad8562866c8c63fc16f6dfc33afe56/coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d", size = 250211, upload-time = "2026-03-17T10:29:57.938Z" }, - { url = "https://files.pythonhosted.org/packages/6c/85/552496626d6b9359eb0e2f86f920037c9cbfba09b24d914c6e1528155f7d/coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930", size = 252125, upload-time = "2026-03-17T10:29:59.388Z" }, - { url = "https://files.pythonhosted.org/packages/44/21/40256eabdcbccdb6acf6b381b3016a154399a75fe39d406f790ae84d1f3c/coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d", size = 247219, upload-time = "2026-03-17T10:30:01.199Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e8/96e2a6c3f21a0ea77d7830b254a1542d0328acc8d7bdf6a284ba7e529f77/coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40", size = 248248, upload-time = "2026-03-17T10:30:03.317Z" }, - { url = "https://files.pythonhosted.org/packages/da/ba/8477f549e554827da390ec659f3c38e4b6d95470f4daafc2d8ff94eaa9c2/coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878", size = 246254, upload-time = "2026-03-17T10:30:04.832Z" }, - { url = "https://files.pythonhosted.org/packages/55/59/bc22aef0e6aa179d5b1b001e8b3654785e9adf27ef24c93dc4228ebd5d68/coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400", size = 250067, upload-time = "2026-03-17T10:30:06.535Z" }, - { url = "https://files.pythonhosted.org/packages/de/1b/c6a023a160806a5137dca53468fd97530d6acad24a22003b1578a9c2e429/coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0", size = 246521, upload-time = "2026-03-17T10:30:08.486Z" }, - { url = "https://files.pythonhosted.org/packages/2d/3f/3532c85a55aa2f899fa17c186f831cfa1aa434d88ff792a709636f64130e/coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0", size = 247126, upload-time = "2026-03-17T10:30:09.966Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2e/b9d56af4a24ef45dfbcda88e06870cb7d57b2b0bfa3a888d79b4c8debd76/coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58", size = 221860, upload-time = "2026-03-17T10:30:11.393Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cc/d938417e7a4d7f0433ad4edee8bb2acdc60dc7ac5af19e2a07a048ecbee3/coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e", size = 222788, upload-time = "2026-03-17T10:30:12.886Z" }, { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, @@ -636,7 +571,6 @@ version = "46.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } wheels = [ @@ -769,6 +703,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + [[package]] name = "docspec" version = "2.2.1" @@ -816,15 +759,16 @@ wheels = [ ] [[package]] -name = "exceptiongroup" -version = "1.3.1" +name = "email-validator" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "dnspython" }, + { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] [[package]] @@ -895,13 +839,6 @@ version = "0.7.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/e5/c07e0bcf4ec8db8164e9f6738c048b2e66aabf30e7506f440c4cc6953f60/httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78", size = 204531, upload-time = "2025-10-10T03:54:20.887Z" }, - { url = "https://files.pythonhosted.org/packages/7e/4f/35e3a63f863a659f92ffd92bef131f3e81cf849af26e6435b49bd9f6f751/httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4", size = 109408, upload-time = "2025-10-10T03:54:22.455Z" }, - { url = "https://files.pythonhosted.org/packages/f5/71/b0a9193641d9e2471ac541d3b1b869538a5fb6419d52fd2669fa9c79e4b8/httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05", size = 440889, upload-time = "2025-10-10T03:54:23.753Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d9/2e34811397b76718750fea44658cb0205b84566e895192115252e008b152/httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed", size = 440460, upload-time = "2025-10-10T03:54:25.313Z" }, - { url = "https://files.pythonhosted.org/packages/01/3f/a04626ebeacc489866bb4d82362c0657b2262bef381d68310134be7f40bb/httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a", size = 425267, upload-time = "2025-10-10T03:54:26.81Z" }, - { url = "https://files.pythonhosted.org/packages/a5/99/adcd4f66614db627b587627c8ad6f4c55f18881549bab10ecf180562e7b9/httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b", size = 424429, upload-time = "2025-10-10T03:54:28.174Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/ec8fc904a8fd30ba022dfa85f3bbc64c3c7cd75b669e24242c0658e22f3c/httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568", size = 86173, upload-time = "2025-10-10T03:54:29.5Z" }, { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, @@ -968,13 +905,6 @@ version = "0.12.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/25/e3/a765812d447714a9606e388325b59602ae61a7da6e59cd981a5dd2eedb11/impit-0.12.0.tar.gz", hash = "sha256:c9a29ba3cee820d2a0f11596a056e8316497b2e7e2ec789db180d72d35d344ac", size = 148594, upload-time = "2026-03-06T13:39:47.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/8a/b31ff1181109b21ae8b1ef0a6a2182c88bb066be72b4f05afc9c49fddc98/impit-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:81d398cbfbbd325bc744c7a22cf5222e8182d709be66f345db2a97b81e878762", size = 3797579, upload-time = "2026-03-06T13:38:13.896Z" }, - { url = "https://files.pythonhosted.org/packages/ea/c3/13d78752d6838e059762cb0fe7b56b49ada42cd507b2c5e8fa6773255dad/impit-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dba43f52e25d8fa46a7adb47f7b11f10897dbf2232f1de80cd2ec310e66f880b", size = 3666177, upload-time = "2026-03-06T13:38:16.322Z" }, - { url = "https://files.pythonhosted.org/packages/65/1b/2a6ff03d43c364918c697cb407a9e9aea84e92d517ffda198dd10bd377df/impit-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40aa46a8aae5144fae75d47caaf9315924832a4636d5f61fb7730beb314c0469", size = 4005171, upload-time = "2026-03-06T13:38:18.7Z" }, - { url = "https://files.pythonhosted.org/packages/d2/eb/7f0aaee4d0559761b4434d85b3f626d267ccf407dea322891dd9846f3dec/impit-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7cdde666a78cb1ba0af27092ce80eb62d8d28a188bea8d605c08e9e80143dcc8", size = 3872956, upload-time = "2026-03-06T13:38:20.365Z" }, - { url = "https://files.pythonhosted.org/packages/bd/3f/2540814c24f2957820719188598a468aca05b032b3272e0d74e76f962e19/impit-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:12418a537a90442c53b751b1e6cb90a5e758424e095c45a811a9fbfaf678b533", size = 4085093, upload-time = "2026-03-06T13:38:22.066Z" }, - { url = "https://files.pythonhosted.org/packages/a3/01/3d5b2317e6f9c1e1a788c3cc2c76239cdc5362cfec75955386bd465fcde0/impit-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fcd783c539ab6ee63e85fd1724a31d315a9e320b45951ab928af699d22bea3ef", size = 4232122, upload-time = "2026-03-06T13:38:24.255Z" }, - { url = "https://files.pythonhosted.org/packages/28/d3/e238d11acade870e179fc5c691c9a6d1038ffa82f9b38b88c4f4d54917e0/impit-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:1c1e23d99755eef2240589e41f078d3d02491914533f02abd8ab567a7adc4541", size = 3678624, upload-time = "2026-03-06T13:38:25.877Z" }, { url = "https://files.pythonhosted.org/packages/6f/31/520d93bfc8c13ae1e188e268c49491269634e55c535506ae933075e9b342/impit-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2c528c156d128beff4a08dd7d277dc7d91d0bd48c41d1e6f03257c87cbea416e", size = 3797921, upload-time = "2026-03-06T13:38:27.928Z" }, { url = "https://files.pythonhosted.org/packages/b5/a8/ed6fec1f3cc5674f0b2d06066a5b2ee03604a1c551bd7095d37c4cd39c1b/impit-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2985c91f4826bf7fff9b32a8dbcbf6ced75b5d9e57ff3448bfb848dac9bec047", size = 3666483, upload-time = "2026-03-06T13:38:29.934Z" }, { url = "https://files.pythonhosted.org/packages/2c/4b/5e19de4d736b3b8baa0ab1c4f63beabc2d961ac366a4b5a5240b6d287124/impit-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d881307ae67f2316a683008a1ea88ed39c8284a26fe82a98318cfc2fc1669e9", size = 4005142, upload-time = "2026-03-06T13:38:31.635Z" }, @@ -1017,25 +947,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6b/7c/7ba4b99307bb084ab0891dccf1689195657a6ac675f7d1a8b0f134973fe2/impit-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:58c26d748480f7a937f6777503b1a88beda8bf548a7275238de8dc34edaa94bc", size = 4232704, upload-time = "2026-03-06T13:39:45.838Z" }, ] -[[package]] -name = "importlib-metadata" -version = "9.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, -] - [[package]] name = "incremental" version = "24.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/3c/82e84109e02c492f382c711c58a3dd91badda6d746def81a1465f74dc9f5/incremental-24.11.0.tar.gz", hash = "sha256:87d3480dbb083c1d736222511a8cf380012a8176c2456d01ef483242abbbcf8c", size = 24000, upload-time = "2025-11-28T02:30:17.861Z" } wheels = [ @@ -1101,12 +1018,6 @@ version = "1.12.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/08/a2/69df9c6ba6d316cfd81fe2381e464db3e6de5db45f8c43c6a23504abf8cb/lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61", size = 43681, upload-time = "2025-08-22T13:50:06.783Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/2b/d5e8915038acbd6c6a9fcb8aaf923dc184222405d3710285a1fec6e262bc/lazy_object_proxy-1.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61d5e3310a4aa5792c2b599a7a78ccf8687292c8eb09cf187cca8f09cf6a7519", size = 26658, upload-time = "2025-08-22T13:42:23.373Z" }, - { url = "https://files.pythonhosted.org/packages/da/8f/91fc00eeea46ee88b9df67f7c5388e60993341d2a406243d620b2fdfde57/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ca33565f698ac1aece152a10f432415d1a2aa9a42dfe23e5ba2bc255ab91f6", size = 68412, upload-time = "2025-08-22T13:42:24.727Z" }, - { url = "https://files.pythonhosted.org/packages/07/d2/b7189a0e095caedfea4d42e6b6949d2685c354263bdf18e19b21ca9b3cd6/lazy_object_proxy-1.12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d01c7819a410f7c255b20799b65d36b414379a30c6f1684c7bd7eb6777338c1b", size = 67559, upload-time = "2025-08-22T13:42:25.875Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/b013840cc43971582ff1ceaf784d35d3a579650eb6cc348e5e6ed7e34d28/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:029d2b355076710505c9545aef5ab3f750d89779310e26ddf2b7b23f6ea03cd8", size = 66651, upload-time = "2025-08-22T13:42:27.427Z" }, - { url = "https://files.pythonhosted.org/packages/7e/6f/b7368d301c15612fcc4cd00412b5d6ba55548bde09bdae71930e1a81f2ab/lazy_object_proxy-1.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc6e3614eca88b1c8a625fc0a47d0d745e7c3255b21dac0e30b3037c5e3deeb8", size = 66901, upload-time = "2025-08-22T13:42:28.585Z" }, - { url = "https://files.pythonhosted.org/packages/61/1b/c6b1865445576b2fc5fa0fbcfce1c05fee77d8979fd1aa653dd0f179aefc/lazy_object_proxy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:be5fe974e39ceb0d6c9db0663c0464669cf866b2851c73971409b9566e880eab", size = 26536, upload-time = "2025-08-22T13:42:29.636Z" }, { url = "https://files.pythonhosted.org/packages/01/b3/4684b1e128a87821e485f5a901b179790e6b5bc02f89b7ee19c23be36ef3/lazy_object_proxy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1cf69cd1a6c7fe2dbcc3edaa017cf010f4192e53796538cc7d5e1fedbfa4bcff", size = 26656, upload-time = "2025-08-22T13:42:30.605Z" }, { url = "https://files.pythonhosted.org/packages/3a/03/1bdc21d9a6df9ff72d70b2ff17d8609321bea4b0d3cffd2cea92fb2ef738/lazy_object_proxy-1.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efff4375a8c52f55a145dc8487a2108c2140f0bec4151ab4e1843e52eb9987ad", size = 68832, upload-time = "2025-08-22T13:42:31.675Z" }, { url = "https://files.pythonhosted.org/packages/3d/4b/5788e5e8bd01d19af71e50077ab020bc5cce67e935066cd65e1215a09ff9/lazy_object_proxy-1.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1192e8c2f1031a6ff453ee40213afa01ba765b3dc861302cd91dbdb2e2660b00", size = 69148, upload-time = "2025-08-22T13:42:32.876Z" }, @@ -1146,22 +1057,6 @@ version = "6.0.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ce/08/1217ca4043f55c3c92993b283a7dbfa456a2058d8b57bbb416cc96b6efff/lxml-6.0.4.tar.gz", hash = "sha256:4137516be2a90775f99d8ef80ec0283f8d78b5d8bd4630ff20163b72e7e9abf2", size = 4237780, upload-time = "2026-04-12T16:28:24.182Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/b9/93d71026bf6c4dfe3afc32064a3fcd533d9032c8b97499744a999f97c230/lxml-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4a2c26422c359e93d97afd29f18670ae2079dbe2dd17469f1e181aa6699e96a7", size = 8540588, upload-time = "2026-04-12T16:22:56.746Z" }, - { url = "https://files.pythonhosted.org/packages/c0/61/33639497c73383e2f53f0b93d485248b77d5498f3589534952bd94380ff3/lxml-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e3b455459e5ed424a4cc277cd085fc1a50a05b940af30703a13a8ec0932d6a69", size = 4601730, upload-time = "2026-04-12T16:22:59.152Z" }, - { url = "https://files.pythonhosted.org/packages/10/ad/cb2de3d32a0d4748be7cd002a3e3eb67e82027af3796f9fe2462aadb1f7c/lxml-6.0.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3109bdeb9674abbc4d8bd3fd273cce4a4087a93f31c17dc321130b71384992e5", size = 5000607, upload-time = "2026-04-12T16:23:01.103Z" }, - { url = "https://files.pythonhosted.org/packages/93/4d/87d8eaba7638c917b2fd971efd1bd93d0662dade95e1d868c18ba7bb84d9/lxml-6.0.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d41f733476eecf7a919a1b909b12e67f247564b21c2b5d13e5f17851340847da", size = 5154439, upload-time = "2026-04-12T16:23:03.818Z" }, - { url = "https://files.pythonhosted.org/packages/1b/6a/dd74a938ff10daadbc441bb4bc9d23fb742341da46f2730d7e335cb034bb/lxml-6.0.4-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:717e702b07b512aca0f09d402896e476cfdc1db12bca0441210b1a36fdddb6dd", size = 5055024, upload-time = "2026-04-12T16:23:06.085Z" }, - { url = "https://files.pythonhosted.org/packages/ef/4a/ac0f195f52fae450338cae90234588a2ead2337440b4e5ff7230775477a3/lxml-6.0.4-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ad61a5fb291e45bb1d680b4de0c99e28547bd249ec57d60e3e59ebe6628a01f", size = 5285427, upload-time = "2026-04-12T16:23:08.081Z" }, - { url = "https://files.pythonhosted.org/packages/34/f1/804925a5723b911507d7671ab164b697f2e3acb12c0bb17a201569ab848e/lxml-6.0.4-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:2c75422b742dd70cc2b5dbffb181ac093a847b338c7ca1495d92918ae35eabae", size = 5410657, upload-time = "2026-04-12T16:23:11.154Z" }, - { url = "https://files.pythonhosted.org/packages/73/bc/1d032759c6fbd45c72c29880df44bd2115cdd4574b01a10c9d448496cb75/lxml-6.0.4-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:28df3bd54561a353ce24e80c556e993b397a41a6671d567b6c9bee757e1bf894", size = 4769048, upload-time = "2026-04-12T16:23:13.306Z" }, - { url = "https://files.pythonhosted.org/packages/b1/d0/a6b5054a2df979d6c348173bc027cb9abaa781fe96590f93a0765f50748c/lxml-6.0.4-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8d7db1fa5f95a8e4fcf0462809f70e536c3248944ddeba692363177ac6b44f2b", size = 5358493, upload-time = "2026-04-12T16:23:15.927Z" }, - { url = "https://files.pythonhosted.org/packages/c7/ce/99e7233391290b6e9a7d8429846b340aa547f16ad026307bf2a02919a3e2/lxml-6.0.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8fdae368cb2deb4b2476f886c107aecaaea084e97c0bc0a268861aa0dd2b7237", size = 5106775, upload-time = "2026-04-12T16:23:18.276Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c8/1d6d65736cec2cd3198bbe512ec121625a3dc4bb7c9dbd19cc0ea967e9b1/lxml-6.0.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:14e4af403766322522440863ca55a9561683b4aedf828df6726b8f83de14a17f", size = 4802389, upload-time = "2026-04-12T16:23:20.948Z" }, - { url = "https://files.pythonhosted.org/packages/e1/99/2b9b704843f5661347ba33150918d4c1d18025449489b05895d352501ae7/lxml-6.0.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c4633c39204e97f36d68deff76471a0251afe8a82562034e4eda63673ee62d36", size = 5348648, upload-time = "2026-04-12T16:23:23.18Z" }, - { url = "https://files.pythonhosted.org/packages/3e/af/2f15de7f947a71ee1b4c850d8f1764adfdfae459e434caf50e6c81983da4/lxml-6.0.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a72e2e31dbc3c35427486402472ca5d8ca2ef2b33648ed0d1b22de2a96347b76", size = 5307603, upload-time = "2026-04-12T16:23:25.169Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9a/028f3c7981411b90afce0743a12f947a047e7b75a0e0efd3774a704eb49a/lxml-6.0.4-cp310-cp310-win32.whl", hash = "sha256:15f135577ffb6514b40f02c00c1ba0ca6305248b1e310101ca17787beaf4e7ad", size = 3597402, upload-time = "2026-04-12T16:23:27.416Z" }, - { url = "https://files.pythonhosted.org/packages/32/84/dac34d557eab04384914a9788caf6ec99132434a52a534bf7b367cf8b366/lxml-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:fd7f6158824b8bc1e96ae87fb14159553be8f7fa82aec73e0bdf98a5af54290c", size = 4019839, upload-time = "2026-04-12T16:23:29.594Z" }, - { url = "https://files.pythonhosted.org/packages/97/cb/c91537a07a23ee6c55cf701df3dc34f76cf0daec214adffda9c8395648ef/lxml-6.0.4-cp310-cp310-win_arm64.whl", hash = "sha256:5ff4d73736c80cb9470c8efa492887e4e752a67b7fd798127794e2be103ebef1", size = 3667037, upload-time = "2026-04-12T16:23:31.768Z" }, { url = "https://files.pythonhosted.org/packages/15/93/5145f2c9210bf99c01f2f54d364be805f556f2cb13af21d3c2d80e0780bb/lxml-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3602d57fdb6f744f4c5d0bd49513fe5abbced08af85bba345fc354336667cd47", size = 8525003, upload-time = "2026-04-12T16:23:34.045Z" }, { url = "https://files.pythonhosted.org/packages/93/19/9d61560a53ac1b26aec1a83ae51fadbe0cc0b6534e2c753ad5af854f231b/lxml-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8c7976c384dcab4bca42f371449fb711e20f1bfce99c135c9b25614aed80e55", size = 4594697, upload-time = "2026-04-12T16:23:36.403Z" }, { url = "https://files.pythonhosted.org/packages/93/1a/0db40884f959c94ede238507ea0967dd47527ab11d130c5a571088637e78/lxml-6.0.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:579e20c120c3d231e53f0376058e4e1926b71ca4f7b77a7a75f82aea7a9b501e", size = 4922365, upload-time = "2026-04-12T16:23:38.709Z" }, @@ -1264,17 +1159,6 @@ version = "3.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, - { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, - { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, - { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, @@ -1356,29 +1240,8 @@ wheels = [ name = "multidict" version = "6.7.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/0b/19348d4c98980c4851d2f943f8ebafdece2ae7ef737adcfa5994ce8e5f10/multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5", size = 77176, upload-time = "2026-01-26T02:42:59.784Z" }, - { url = "https://files.pythonhosted.org/packages/ef/04/9de3f8077852e3d438215c81e9b691244532d2e05b4270e89ce67b7d103c/multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8", size = 44996, upload-time = "2026-01-26T02:43:01.674Z" }, - { url = "https://files.pythonhosted.org/packages/31/5c/08c7f7fe311f32e83f7621cd3f99d805f45519cd06fafb247628b861da7d/multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872", size = 44631, upload-time = "2026-01-26T02:43:03.169Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7f/0e3b1390ae772f27501199996b94b52ceeb64fe6f9120a32c6c3f6b781be/multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991", size = 242561, upload-time = "2026-01-26T02:43:04.733Z" }, - { url = "https://files.pythonhosted.org/packages/dd/f4/8719f4f167586af317b69dd3e90f913416c91ca610cac79a45c53f590312/multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03", size = 242223, upload-time = "2026-01-26T02:43:06.695Z" }, - { url = "https://files.pythonhosted.org/packages/47/ab/7c36164cce64a6ad19c6d9a85377b7178ecf3b89f8fd589c73381a5eedfd/multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981", size = 222322, upload-time = "2026-01-26T02:43:08.472Z" }, - { url = "https://files.pythonhosted.org/packages/f5/79/a25add6fb38035b5337bc5734f296d9afc99163403bbcf56d4170f97eb62/multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6", size = 254005, upload-time = "2026-01-26T02:43:10.127Z" }, - { url = "https://files.pythonhosted.org/packages/4a/7b/64a87cf98e12f756fc8bd444b001232ffff2be37288f018ad0d3f0aae931/multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190", size = 251173, upload-time = "2026-01-26T02:43:11.731Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ac/b605473de2bb404e742f2cc3583d12aedb2352a70e49ae8fce455b50c5aa/multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92", size = 243273, upload-time = "2026-01-26T02:43:13.063Z" }, - { url = "https://files.pythonhosted.org/packages/03/65/11492d6a0e259783720f3bc1d9ea55579a76f1407e31ed44045c99542004/multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee", size = 238956, upload-time = "2026-01-26T02:43:14.843Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a7/7ee591302af64e7c196fb63fe856c788993c1372df765102bd0448e7e165/multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2", size = 233477, upload-time = "2026-01-26T02:43:16.025Z" }, - { url = "https://files.pythonhosted.org/packages/9c/99/c109962d58756c35fd9992fed7f2355303846ea2ff054bb5f5e9d6b888de/multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568", size = 243615, upload-time = "2026-01-26T02:43:17.84Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5f/1973e7c771c86e93dcfe1c9cc55a5481b610f6614acfc28c0d326fe6bfad/multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40", size = 249930, upload-time = "2026-01-26T02:43:19.06Z" }, - { url = "https://files.pythonhosted.org/packages/5d/a5/f170fc2268c3243853580203378cd522446b2df632061e0a5409817854c7/multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962", size = 243807, upload-time = "2026-01-26T02:43:20.286Z" }, - { url = "https://files.pythonhosted.org/packages/de/01/73856fab6d125e5bc652c3986b90e8699a95e84b48d72f39ade6c0e74a8c/multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505", size = 239103, upload-time = "2026-01-26T02:43:21.508Z" }, - { url = "https://files.pythonhosted.org/packages/e7/46/f1220bd9944d8aa40d8ccff100eeeee19b505b857b6f603d6078cb5315b0/multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122", size = 41416, upload-time = "2026-01-26T02:43:22.703Z" }, - { url = "https://files.pythonhosted.org/packages/68/00/9b38e272a770303692fc406c36e1a4c740f401522d5787691eb38a8925a8/multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df", size = 46022, upload-time = "2026-01-26T02:43:23.77Z" }, - { url = "https://files.pythonhosted.org/packages/64/65/d8d42490c02ee07b6bbe00f7190d70bb4738b3cce7629aaf9f213ef730dd/multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db", size = 43238, upload-time = "2026-01-26T02:43:24.882Z" }, { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, @@ -1607,7 +1470,6 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pastel" }, { name = "pyyaml" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/a4/e487662f12a5ecd2ac4d77f7697e4bda481953bb80032b158e5ab55173d4/poethepoet-0.44.0.tar.gz", hash = "sha256:c2667b513621788fb46482e371cdf81c0b04344e0e0bcb7aa8af45f84c2fce7b", size = 96040, upload-time = "2026-04-06T19:40:58.908Z" } wheels = [ @@ -1636,21 +1498,6 @@ version = "0.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, - { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, - { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, - { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, - { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, - { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, - { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, - { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, - { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, - { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, - { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, - { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, @@ -1826,6 +1673,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/d7/c3a52c61f5b7be648e919005820fbac33028c6149994cd64453f49951c17/pydantic-2.13.0-py3-none-any.whl", hash = "sha256:ab0078b90da5f3e2fd2e71e3d9b457ddcb35d0350854fbda93b451e28d56baaf", size = 471872, upload-time = "2026-04-13T10:51:33.343Z" }, ] +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + [[package]] name = "pydantic-core" version = "2.46.0" @@ -1835,20 +1687,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/6f/0a/9414cddf82eda3976b14048cc0fa8f5b5d1aecb0b22e1dcd2dbfe0e139b1/pydantic_core-2.46.0.tar.gz", hash = "sha256:82d2498c96be47b47e903e1378d1d0f770097ec56ea953322f39936a7cf34977", size = 471441, upload-time = "2026-04-13T09:06:33.813Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/17/fd3ba2f035ac7b3a1ae0c55e5c0f6eb5275e87ad80a9b277cb2e70317e2c/pydantic_core-2.46.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d449eae37d6b066d8a8be0e3a7d7041712d6e9152869e7d03c203795aae44ed", size = 2122942, upload-time = "2026-04-13T09:04:32.413Z" }, - { url = "https://files.pythonhosted.org/packages/01/b5/214cb10e4050f430f383a21496087c1e51d583eec3c884b0e5f55c34eb69/pydantic_core-2.46.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4f7bfc1ffee4ddc03c2db472c7607a238dbbf76f7f64104fc6a623d47fb8e310", size = 1949068, upload-time = "2026-04-13T09:05:28.803Z" }, - { url = "https://files.pythonhosted.org/packages/b4/ab/8ab4ec2a879eead4bb51c3e9af65583e16cc504867e808909cd4f991a5ae/pydantic_core-2.46.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a30f5d1d4e1c958b44b5c777a0d1adcd930429f35101e4780281ffbe11103925", size = 1974362, upload-time = "2026-04-13T09:05:26.894Z" }, - { url = "https://files.pythonhosted.org/packages/8f/dd/dc8ef47e18ddcab169af68b3c11648e1ef85c56aa18e2f96312cc5442404/pydantic_core-2.46.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f68e12d2de32ac6313a7d3854f346d71731288184fbbfc9004e368714244d2cd", size = 2043754, upload-time = "2026-04-13T09:04:54.637Z" }, - { url = "https://files.pythonhosted.org/packages/7f/52/69195c8f6549d2b1b9ce0efbb9bf169b47dcb9a60f81ff53a67cb22d8fc7/pydantic_core-2.46.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7d1a058fb5aff8a1a221e7d8a0cf5b0133d069b2f293cb05f174c61bc7cdac34", size = 2230099, upload-time = "2026-04-13T09:04:44.37Z" }, - { url = "https://files.pythonhosted.org/packages/2a/41/48c8e7709604a4230f86f77bc17e1eb575e0894831f2c3beaecb3e8f7583/pydantic_core-2.46.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbd01128431f355e309267283e37e23704f24558e9059d930e213a377b1be919", size = 2293730, upload-time = "2026-04-13T09:04:27.583Z" }, - { url = "https://files.pythonhosted.org/packages/08/ab/f3bc576d37eb3036f7b1b2721ab0f89e4684fab48e1de1d0eca0dfef7469/pydantic_core-2.46.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7747a50d9f75fe264b9e2091a2f462a7dd400add8723a87a75240106b6f4d949", size = 2095380, upload-time = "2026-04-13T09:04:45.929Z" }, - { url = "https://files.pythonhosted.org/packages/fe/69/0f6e5bd9c5594b41deb91029ad0b16ffe5a270dd412033dd1135a40bbfa3/pydantic_core-2.46.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:1d9b841e9c82a9cdf397a720bb8a4f2d6da6780204e1eb07c2d90c4b5b791b0d", size = 2140115, upload-time = "2026-04-13T09:07:00.944Z" }, - { url = "https://files.pythonhosted.org/packages/28/7c/79cfc18d352797b84a7c5b27171d6557121843729bc637a90550d08370fd/pydantic_core-2.46.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:61d0f5951b7b86ec24e24fe0c5a2cce7c360830026dfbe004954e8fac9918b95", size = 2183044, upload-time = "2026-04-13T09:03:58.106Z" }, - { url = "https://files.pythonhosted.org/packages/59/bc/701b17bf7fd375e59e03838cffe8f6893498503b7d412d577ffd92dab56c/pydantic_core-2.46.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:aec0be48d2555ceac04905ffb8f2bb7e55a56644858891196191827b6fc656b7", size = 2185277, upload-time = "2026-04-13T09:05:52.482Z" }, - { url = "https://files.pythonhosted.org/packages/c3/43/ad927b8861ab787b4189ddb2dd70ebcdc20c5a4baf52df94934d6f87d730/pydantic_core-2.46.0-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:2c1ec2ced44a8a479d71a14f5be35461360acd388987873a8e0a02f7f81c8ec2", size = 2329998, upload-time = "2026-04-13T09:05:54.803Z" }, - { url = "https://files.pythonhosted.org/packages/47/33/ad11d56b97ea986f991da998d551a7513d19c06ed05a529e86520430e10e/pydantic_core-2.46.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5e157a25eed281f5e40119078e3dbf698c28b3d88ff0176eea3dd37191447b8d", size = 2369004, upload-time = "2026-04-13T09:05:14.052Z" }, - { url = "https://files.pythonhosted.org/packages/16/d1/a9a28a122f1227dc13fdd361d77a3f2df4aee64e4ac5693d7ce74a8ecfa4/pydantic_core-2.46.0-cp310-cp310-win32.whl", hash = "sha256:311929d9bfdb9fdbaf28beb39d88a1e36ca6dc5424ceca6d3bf81c9e1da2313c", size = 1982879, upload-time = "2026-04-13T09:05:19.277Z" }, - { url = "https://files.pythonhosted.org/packages/94/9a/52988a743cf7a9d84861e380c6a5496589aebbc3592d9ecdecb13c6bd0a2/pydantic_core-2.46.0-cp310-cp310-win_amd64.whl", hash = "sha256:60edfb53b13fbe7be9bb51447016b7bcd8772beb8ca216873be33e9d11b2c8e8", size = 2068907, upload-time = "2026-04-13T09:03:59.541Z" }, { url = "https://files.pythonhosted.org/packages/ce/43/9bc38d43a6a48794209e4eb6d61e9c68395f69b7949f66842854b0cd1344/pydantic_core-2.46.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0027da787ae711f7fbd5a76cb0bb8df526acba6c10c1e44581de1b838db10b7b", size = 2121004, upload-time = "2026-04-13T09:05:17.531Z" }, { url = "https://files.pythonhosted.org/packages/8c/1d/f43342b7107939b305b5e4efeef7d54e267a5ef51515570a5c1d77726efb/pydantic_core-2.46.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:63e288fc18d7eaeef5f16c73e65c4fd0ad95b25e7e21d8a5da144977b35eb997", size = 1947505, upload-time = "2026-04-13T09:04:48.975Z" }, { url = "https://files.pythonhosted.org/packages/4a/cd/ccf48cbbcaf0d99ba65969459ebfbf7037600b2cfdcca3062084dd83a008/pydantic_core-2.46.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:080a3bdc6807089a1fe1fbc076519cea287f1a964725731d80b49d8ecffaa217", size = 1973301, upload-time = "2026-04-13T09:05:42.149Z" }, @@ -2045,12 +1883,10 @@ version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ @@ -2062,7 +1898,6 @@ name = "pytest-asyncio" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, { name = "pytest" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] @@ -2150,11 +1985,6 @@ version = "0.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" }, - { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" }, - { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" }, - { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" }, - { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" }, { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, @@ -2189,15 +2019,6 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, @@ -2539,7 +2360,6 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5e/da/6eee1ff8b6cbeed47eeb5229749168e81eb4b7b999a1a15a7176e51410c9/uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e", size = 86947, upload-time = "2026-04-06T09:23:22.826Z" } wheels = [ @@ -2563,12 +2383,6 @@ version = "0.22.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, - { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, - { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, - { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, @@ -2610,7 +2424,6 @@ dependencies = [ { name = "filelock" }, { name = "platformdirs" }, { name = "python-discovery" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/60/8c/bdd9f89f89e4a787ac61bb2da4d884bc45e0c287ec694dfa3170dddd5cfe/virtualenv-21.2.3.tar.gz", hash = "sha256:9bb6d1414ab55ca624371e30c7719c32f183ef44da544ef8aa44a456de7ac191", size = 5844776, upload-time = "2026-04-14T01:10:36.692Z" } wheels = [ @@ -2632,9 +2445,6 @@ version = "6.0.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, - { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, - { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, @@ -2644,8 +2454,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, - { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, @@ -2667,18 +2475,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, - { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, - { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, - { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, - { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, - { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, - { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, - { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, - { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, - { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, - { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, @@ -2751,10 +2547,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, - { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, - { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, @@ -2767,15 +2559,6 @@ version = "16.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, - { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, - { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, - { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, - { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, - { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, - { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, @@ -2847,17 +2630,6 @@ version = "2.1.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/d2/387594fb592d027366645f3d7cc9b4d7ca7be93845fbaba6d835a912ef3c/wrapt-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a86d99a14f76facb269dc148590c01aaf47584071809a70da30555228158c", size = 60669, upload-time = "2026-03-06T02:52:40.671Z" }, - { url = "https://files.pythonhosted.org/packages/c9/18/3f373935bc5509e7ac444c8026a56762e50c1183e7061797437ca96c12ce/wrapt-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a819e39017f95bf7aede768f75915635aa8f671f2993c036991b8d3bfe8dbb6f", size = 61603, upload-time = "2026-03-06T02:54:21.032Z" }, - { url = "https://files.pythonhosted.org/packages/c2/7a/32758ca2853b07a887a4574b74e28843919103194bb47001a304e24af62f/wrapt-2.1.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5681123e60aed0e64c7d44f72bbf8b4ce45f79d81467e2c4c728629f5baf06eb", size = 113632, upload-time = "2026-03-06T02:53:54.121Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d5/eeaa38f670d462e97d978b3b0d9ce06d5b91e54bebac6fbed867809216e7/wrapt-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8b28e97a44d21836259739ae76284e180b18abbb4dcfdff07a415cf1016c3e", size = 115644, upload-time = "2026-03-06T02:54:53.33Z" }, - { url = "https://files.pythonhosted.org/packages/e3/09/2a41506cb17affb0bdf9d5e2129c8c19e192b388c4c01d05e1b14db23c00/wrapt-2.1.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cef91c95a50596fcdc31397eb6955476f82ae8a3f5a8eabdc13611b60ee380ba", size = 112016, upload-time = "2026-03-06T02:54:43.274Z" }, - { url = "https://files.pythonhosted.org/packages/64/15/0e6c3f5e87caadc43db279724ee36979246d5194fa32fed489c73643ba59/wrapt-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dad63212b168de8569b1c512f4eac4b57f2c6934b30df32d6ee9534a79f1493f", size = 114823, upload-time = "2026-03-06T02:54:29.392Z" }, - { url = "https://files.pythonhosted.org/packages/56/b2/0ad17c8248f4e57bedf44938c26ec3ee194715f812d2dbbd9d7ff4be6c06/wrapt-2.1.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d307aa6888d5efab2c1cde09843d48c843990be13069003184b67d426d145394", size = 111244, upload-time = "2026-03-06T02:54:02.149Z" }, - { url = "https://files.pythonhosted.org/packages/ff/04/bcdba98c26f2c6522c7c09a726d5d9229120163493620205b2f76bd13c01/wrapt-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c87cf3f0c85e27b3ac7d9ad95da166bf8739ca215a8b171e8404a2d739897a45", size = 113307, upload-time = "2026-03-06T02:54:12.428Z" }, - { url = "https://files.pythonhosted.org/packages/0e/1b/5e2883c6bc14143924e465a6fc5a92d09eeabe35310842a481fb0581f832/wrapt-2.1.2-cp310-cp310-win32.whl", hash = "sha256:d1c5fea4f9fe3762e2b905fdd67df51e4be7a73b7674957af2d2ade71a5c075d", size = 57986, upload-time = "2026-03-06T02:54:26.823Z" }, - { url = "https://files.pythonhosted.org/packages/42/5a/4efc997bccadd3af5749c250b49412793bc41e13a83a486b2b54a33e240c/wrapt-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:d8f7740e1af13dff2684e4d56fe604a7e04d6c94e737a60568d8d4238b9a0c71", size = 60336, upload-time = "2026-03-06T02:54:18Z" }, - { url = "https://files.pythonhosted.org/packages/c1/f5/a2bb833e20181b937e87c242645ed5d5aa9c373006b0467bfe1a35c727d0/wrapt-2.1.2-cp310-cp310-win_arm64.whl", hash = "sha256:1c6cc827c00dc839350155f316f1f8b4b0c370f52b6a19e782e2bda89600c7dc", size = 58757, upload-time = "2026-03-06T02:53:51.545Z" }, { url = "https://files.pythonhosted.org/packages/c7/81/60c4471fce95afa5922ca09b88a25f03c93343f759aae0f31fb4412a85c7/wrapt-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96159a0ee2b0277d44201c3b5be479a9979cf154e8c82fa5df49586a8e7679bb", size = 60666, upload-time = "2026-03-06T02:52:58.934Z" }, { url = "https://files.pythonhosted.org/packages/6b/be/80e80e39e7cb90b006a0eaf11c73ac3a62bbfb3068469aec15cc0bc795de/wrapt-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98ba61833a77b747901e9012072f038795de7fc77849f1faa965464f3f87ff2d", size = 61601, upload-time = "2026-03-06T02:53:00.487Z" }, { url = "https://files.pythonhosted.org/packages/b0/be/d7c88cd9293c859fc74b232abdc65a229bb953997995d6912fc85af18323/wrapt-2.1.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:767c0dbbe76cae2a60dd2b235ac0c87c9cccf4898aef8062e57bead46b5f6894", size = 114057, upload-time = "2026-03-06T02:52:44.08Z" }, @@ -2933,7 +2705,6 @@ version = "0.43.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "platformdirs" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/23/97/b6f296d1e9cc1ec25c7604178b48532fa5901f721bcf1b8d8148b13e5588/yapf-0.43.0.tar.gz", hash = "sha256:00d3aa24bfedff9420b2e0d5d9f5ab6d9d4268e72afbf59bb3fa542781d5218e", size = 254907, upload-time = "2024-11-14T00:11:41.584Z" } wheels = [ @@ -2951,24 +2722,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/0d/9cc638702f6fc3c7a3685bcc8cf2a9ed7d6206e932a49f5242658047ef51/yarl-1.23.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107", size = 123764, upload-time = "2026-03-01T22:04:09.7Z" }, - { url = "https://files.pythonhosted.org/packages/7a/35/5a553687c5793df5429cd1db45909d4f3af7eee90014888c208d086a44f0/yarl-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d", size = 86282, upload-time = "2026-03-01T22:04:11.892Z" }, - { url = "https://files.pythonhosted.org/packages/68/2e/c5a2234238f8ce37a8312b52801ee74117f576b1539eec8404a480434acc/yarl-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05", size = 86053, upload-time = "2026-03-01T22:04:13.292Z" }, - { url = "https://files.pythonhosted.org/packages/74/3f/bbd8ff36fb038622797ffbaf7db314918bb4d76f1cc8a4f9ca7a55fe5195/yarl-1.23.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d", size = 99395, upload-time = "2026-03-01T22:04:15.133Z" }, - { url = "https://files.pythonhosted.org/packages/77/04/9516bc4e269d2a3ec9c6779fcdeac51ce5b3a9b0156f06ac7152e5bba864/yarl-1.23.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748", size = 92143, upload-time = "2026-03-01T22:04:16.829Z" }, - { url = "https://files.pythonhosted.org/packages/c7/63/88802d1f6b1cb1fc67d67a58cd0cf8a1790de4ce7946e434240f1d60ab4a/yarl-1.23.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764", size = 107643, upload-time = "2026-03-01T22:04:18.519Z" }, - { url = "https://files.pythonhosted.org/packages/8e/db/4f9b838f4d8bdd6f0f385aed8bbf21c71ed11a0b9983305c302cbd557815/yarl-1.23.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007", size = 108700, upload-time = "2026-03-01T22:04:20.373Z" }, - { url = "https://files.pythonhosted.org/packages/50/12/95a1d33f04a79c402664070d43b8b9f72dc18914e135b345b611b0b1f8cc/yarl-1.23.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4", size = 102769, upload-time = "2026-03-01T22:04:23.055Z" }, - { url = "https://files.pythonhosted.org/packages/86/65/91a0285f51321369fd1a8308aa19207520c5f0587772cfc2e03fc2467e90/yarl-1.23.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26", size = 101114, upload-time = "2026-03-01T22:04:25.031Z" }, - { url = "https://files.pythonhosted.org/packages/58/80/c7c8244fc3e5bc483dc71a09560f43b619fab29301a0f0a8f936e42865c7/yarl-1.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769", size = 98883, upload-time = "2026-03-01T22:04:27.281Z" }, - { url = "https://files.pythonhosted.org/packages/86/e7/71ca9cc9ca79c0b7d491216177d1aed559d632947b8ffb0ee60f7d8b23e3/yarl-1.23.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716", size = 94172, upload-time = "2026-03-01T22:04:28.554Z" }, - { url = "https://files.pythonhosted.org/packages/6a/3f/6c6c8a0fe29c26fb2db2e8d32195bb84ec1bfb8f1d32e7f73b787fcf349b/yarl-1.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993", size = 107010, upload-time = "2026-03-01T22:04:30.385Z" }, - { url = "https://files.pythonhosted.org/packages/56/38/12730c05e5ad40a76374d440ed8b0899729a96c250516d91c620a6e38fc2/yarl-1.23.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0", size = 100285, upload-time = "2026-03-01T22:04:31.752Z" }, - { url = "https://files.pythonhosted.org/packages/34/92/6a7be9239f2347234e027284e7a5f74b1140cc86575e7b469d13fba1ebfe/yarl-1.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750", size = 108230, upload-time = "2026-03-01T22:04:33.844Z" }, - { url = "https://files.pythonhosted.org/packages/5e/81/4aebccfa9376bd98b9d8bfad20621a57d3e8cfc5b8631c1fa5f62cdd03f4/yarl-1.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6", size = 103008, upload-time = "2026-03-01T22:04:35.856Z" }, - { url = "https://files.pythonhosted.org/packages/38/0f/0b4e3edcec794a86b853b0c6396c0a888d72dfce19b2d88c02ac289fb6c1/yarl-1.23.0-cp310-cp310-win32.whl", hash = "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d", size = 83073, upload-time = "2026-03-01T22:04:38.268Z" }, - { url = "https://files.pythonhosted.org/packages/a0/71/ad95c33da18897e4c636528bbc24a1dd23fe16797de8bc4ec667b8db0ba4/yarl-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb", size = 87328, upload-time = "2026-03-01T22:04:39.558Z" }, - { url = "https://files.pythonhosted.org/packages/e2/14/dfa369523c79bccf9c9c746b0a63eb31f65db9418ac01275f7950962e504/yarl-1.23.0-cp310-cp310-win_arm64.whl", hash = "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220", size = 82463, upload-time = "2026-03-01T22:04:41.454Z" }, { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641, upload-time = "2026-03-01T22:04:42.841Z" }, { url = "https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", size = 86248, upload-time = "2026-03-01T22:04:44.757Z" }, { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988, upload-time = "2026-03-01T22:04:46.365Z" }, @@ -3080,27 +2833,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, ] -[[package]] -name = "zipp" -version = "3.23.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, -] - [[package]] name = "zope-interface" version = "8.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/c9/04/0b1d92e7d31507c5fbe203d9cc1ae80fb0645688c7af751ea0ec18c2223e/zope_interface-8.3.tar.gz", hash = "sha256:e1a9de7d0b5b5c249a73b91aebf4598ce05e334303af6aa94865893283e9ff10", size = 256822, upload-time = "2026-04-10T06:12:35.036Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/47/791e8da00c00332d4db7f9add22cb102c523e452ea0449bb63eb7dcc3c17/zope_interface-8.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8a2f9c4ee0f2ad4817e9481684993d33b66d9b815f9157a716a189af483bc34", size = 210367, upload-time = "2026-04-10T06:21:50.304Z" }, - { url = "https://files.pythonhosted.org/packages/8b/d5/92bad86cb429af22f59f6e08227c58c74a3d8395a64a5ca61b9301fc6171/zope_interface-8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:99c84e12efe0e17f03c6bb5a8ea18fb2841e6666ee0b8331d5967fec84337884", size = 210726, upload-time = "2026-04-10T06:21:52.375Z" }, - { url = "https://files.pythonhosted.org/packages/cb/55/ddf1aeb3e4d5f7a343599a76dafc0766ec42b32112bfedc37f7ddeff753f/zope_interface-8.3-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a918f8e73c35a1352a4b49db67b90b37d33fb7651c834def3f0e3784437bb3a8", size = 254046, upload-time = "2026-04-10T06:21:54.332Z" }, - { url = "https://files.pythonhosted.org/packages/b6/4f/a52a78b389c79d85d3d4afbf71b2984bd4a8a682beec248cdc21576b13a6/zope_interface-8.3-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5a5b50d0dcdb4200f1936f75b6688bd86de5c14c5d20bed2e004300a04521826", size = 258910, upload-time = "2026-04-10T06:21:56.588Z" }, - { url = "https://files.pythonhosted.org/packages/08/34/2841cb5c1dea43a1e3893deb0ed412d4eeb16f4a3eb4daf2465d24b71069/zope_interface-8.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:731eaf0a0f2a683315a2dfc2953ef831ae51e062b87cff6220e0e5102a83b612", size = 259521, upload-time = "2026-04-10T06:21:58.505Z" }, - { url = "https://files.pythonhosted.org/packages/23/ff/66ba0f3aba2d3724e425fdb99122d6f7927a37d623492a606477094a6891/zope_interface-8.3-cp310-cp310-win_amd64.whl", hash = "sha256:5e9861493457268f923d8aae4052383922162c3d56094c4e3a9ff83173d64be3", size = 214205, upload-time = "2026-04-10T06:22:00.611Z" }, { url = "https://files.pythonhosted.org/packages/0d/99/cee01c7e8be6c5889f2c74914196decd91170011f420c9912792336f284c/zope_interface-8.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e8964f1a13b07c8770eab88b7a6cd0870c3e36442e4ef4937f36fd0b6d1cea2c", size = 210875, upload-time = "2026-04-10T06:22:02.746Z" }, { url = "https://files.pythonhosted.org/packages/e2/f1/cf7a49b36385ed1ee0cc7f6b8861904f1533a3286e01cd1e3c2eb72976b9/zope_interface-8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ec2728e3cf685126ccd2e0f7635fb60edf116f76f402dd66f4df13d9d9348b4b", size = 211199, upload-time = "2026-04-10T06:22:04.596Z" }, { url = "https://files.pythonhosted.org/packages/cc/86/1ccb73ce9189b1345b7824830a18796ae0b33317d3725d8a034a6ce06501/zope_interface-8.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:568b97cb701fd2830b52198a2885e851317a019e1912eaad107860e3cca71964", size = 259885, upload-time = "2026-04-10T06:22:06.403Z" }, From 8778af860437c5865f503327b877178356ef8496 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Thu, 16 Apr 2026 14:19:39 +0200 Subject: [PATCH 2/7] Fix lint and type check issues after apify-client v3 migration Co-Authored-By: Claude Sonnet 4.6 --- docs/02_concepts/code/07_webhook.py | 2 +- docs/02_concepts/code/07_webhook_preventing.py | 2 +- pyproject.toml | 2 +- src/apify/_actor.py | 17 ++++++++--------- src/apify/_charging.py | 4 ++-- src/apify/_configuration.py | 4 ++-- src/apify/_proxy_configuration.py | 5 ++--- src/apify/events/_apify_event_manager.py | 4 ++-- .../storage_clients/_apify/_alias_resolving.py | 1 - .../_apify/_api_client_creation.py | 1 - .../_apify/_request_queue_shared_client.py | 4 ++-- .../_apify/_request_queue_single_client.py | 9 ++++++--- .../_file_system/_dataset_client.py | 4 ++-- .../_file_system/_key_value_store_client.py | 3 ++- tests/e2e/conftest.py | 2 +- tests/e2e/test_actor_api_helpers.py | 2 +- tests/e2e/test_actor_call_timeouts.py | 9 +++++---- tests/e2e/test_actor_charge.py | 4 ++-- tests/e2e/test_actor_lifecycle.py | 8 ++++---- tests/e2e/test_fixtures.py | 6 +++--- tests/integration/test_request_queue.py | 4 ++-- .../test_actor_create_proxy_configuration.py | 2 +- tests/unit/actor/test_actor_helpers.py | 8 ++++---- tests/unit/actor/test_actor_lifecycle.py | 4 ++-- tests/unit/conftest.py | 2 +- tests/unit/test_apify_storages.py | 4 ++-- tests/unit/test_proxy_configuration.py | 10 +--------- website/generate_module_shortcuts.py | 2 +- 28 files changed, 61 insertions(+), 68 deletions(-) diff --git a/docs/02_concepts/code/07_webhook.py b/docs/02_concepts/code/07_webhook.py index a817b399..0fd15abe 100644 --- a/docs/02_concepts/code/07_webhook.py +++ b/docs/02_concepts/code/07_webhook.py @@ -1,6 +1,6 @@ import asyncio -from apify import Actor, Webhook, WebhookEventType +from apify import Actor, Webhook async def main() -> None: diff --git a/docs/02_concepts/code/07_webhook_preventing.py b/docs/02_concepts/code/07_webhook_preventing.py index 8d2b12a2..04c6d5f6 100644 --- a/docs/02_concepts/code/07_webhook_preventing.py +++ b/docs/02_concepts/code/07_webhook_preventing.py @@ -1,6 +1,6 @@ import asyncio -from apify import Actor, Webhook, WebhookEventType +from apify import Actor, Webhook async def main() -> None: diff --git a/pyproject.toml b/pyproject.toml index c482d003..e58f8241 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -200,7 +200,7 @@ asyncio_mode = "auto" timeout = 1800 [tool.ty.environment] -python-version = "3.10" +python-version = "3.11" [tool.ty.src] include = ["src", "tests", "scripts", "docs", "website"] diff --git a/src/apify/_actor.py b/src/apify/_actor.py index f61bb52c..50d7f983 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -4,7 +4,7 @@ import sys import warnings from contextlib import suppress -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from functools import cached_property from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast, overload @@ -43,8 +43,7 @@ import logging from collections.abc import Callable from types import TracebackType - - from typing_extensions import Self + from typing import Self from crawlee._types import JsonSerializable from crawlee.proxy_configuration import _NewUrlFunction @@ -506,7 +505,7 @@ def new_client( (increases exponentially from this value). timeout: The socket timeout of the HTTP requests sent to the Apify API. """ - kwargs = { + kwargs: dict[str, Any] = { 'token': token or self.configuration.token, 'api_url': api_url or self.configuration.api_base_url, } @@ -520,7 +519,7 @@ def new_client( if timeout is not None: kwargs['timeout_secs'] = int(timeout.total_seconds()) - return ApifyClientAsync(**kwargs) # ty: ignore[invalid-argument-type] + return ApifyClientAsync(**kwargs) @_ensure_context async def open_dataset( @@ -929,7 +928,7 @@ async def start( content_type=content_type, build=build, memory_mbytes=memory_mbytes, - timeout=actor_start_timeout, + timeout=actor_start_timeout if actor_start_timeout is not None else 'medium', wait_for_finish=wait_for_finish, webhooks=serialized_webhooks, ) @@ -1052,7 +1051,7 @@ async def call( content_type=content_type, build=build, memory_mbytes=memory_mbytes, - timeout=actor_call_timeout, + timeout=actor_call_timeout if actor_call_timeout is not None else 'no_timeout', webhooks=serialized_webhooks, wait_duration=wait, logger=logger, @@ -1127,7 +1126,7 @@ async def call_task( task_input=task_input, build=build, memory_mbytes=memory_mbytes, - timeout=task_call_timeout, + timeout=task_call_timeout if task_call_timeout is not None else 'no_timeout', webhooks=serialized_webhooks, wait_duration=wait, ) @@ -1429,7 +1428,7 @@ def _get_default_exit_process(self) -> bool: def _get_remaining_time(self) -> timedelta | None: """Get time remaining from the Actor timeout. Returns `None` if not on an Apify platform.""" if self.is_at_home() and self.configuration.timeout_at: - return max(self.configuration.timeout_at - datetime.now(tz=timezone.utc), timedelta(0)) + return max(self.configuration.timeout_at - datetime.now(tz=UTC), timedelta(0)) self.log.warning( 'Using `inherit` or `RemainingTime` argument is only possible when the Actor' diff --git a/src/apify/_charging.py b/src/apify/_charging.py index 36d38e71..539e15b1 100644 --- a/src/apify/_charging.py +++ b/src/apify/_charging.py @@ -3,7 +3,7 @@ import math from contextvars import ContextVar from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import UTC, datetime from decimal import Decimal from typing import TYPE_CHECKING, Protocol, TypedDict @@ -318,7 +318,7 @@ async def charge(self, event_name: str, count: int = 1) -> ChargeResult: 'event_title': pricing_info.title, 'event_price_usd': round(pricing_info.price, 3), 'charged_count': charged_count, - 'timestamp': datetime.now(timezone.utc).isoformat(), + 'timestamp': datetime.now(UTC).isoformat(), } ) diff --git a/src/apify/_configuration.py b/src/apify/_configuration.py index b88f9ae5..97dc2d05 100644 --- a/src/apify/_configuration.py +++ b/src/apify/_configuration.py @@ -5,10 +5,10 @@ from decimal import Decimal from logging import getLogger from pathlib import Path -from typing import Annotated, Any +from typing import Annotated, Any, Self from pydantic import AliasChoices, BeforeValidator, Field, model_validator -from typing_extensions import Self, TypedDict, deprecated +from typing_extensions import TypedDict, deprecated from crawlee import service_locator from crawlee._utils.models import timedelta_ms diff --git a/src/apify/_proxy_configuration.py b/src/apify/_proxy_configuration.py index a654cdd8..98579598 100644 --- a/src/apify/_proxy_configuration.py +++ b/src/apify/_proxy_configuration.py @@ -265,9 +265,8 @@ async def _maybe_fetch_password(self) -> None: if token and self._apify_client: user_info = await self._apify_client.user().get() - if user_info: - password = user_info['proxy']['password'] - self._password = password + if user_info and (proxy := getattr(user_info, 'proxy', None)): + self._password = proxy.password async def _check_access(self) -> None: proxy_status_url = f'{self._configuration.proxy_status_url}/?format=json' diff --git a/src/apify/events/_apify_event_manager.py b/src/apify/events/_apify_event_manager.py index 55e36829..a2f17ee8 100644 --- a/src/apify/events/_apify_event_manager.py +++ b/src/apify/events/_apify_event_manager.py @@ -2,11 +2,11 @@ import asyncio import contextlib -from typing import TYPE_CHECKING, Annotated +from typing import TYPE_CHECKING, Annotated, Self import websockets.asyncio.client from pydantic import Discriminator, TypeAdapter -from typing_extensions import Self, Unpack, override +from typing_extensions import Unpack, override from crawlee.events import EventManager from crawlee.events._types import Event, EventPersistStateData diff --git a/src/apify/storage_clients/_apify/_alias_resolving.py b/src/apify/storage_clients/_apify/_alias_resolving.py index 8ebb3e54..c94a4816 100644 --- a/src/apify/storage_clients/_apify/_alias_resolving.py +++ b/src/apify/storage_clients/_apify/_alias_resolving.py @@ -254,7 +254,6 @@ async def _get_default_kvs_client(configuration: Configuration) -> KeyValueStore api_url=configuration.api_base_url, max_retries=8, min_delay_between_retries=timedelta(milliseconds=500), - timeout=timedelta(seconds=360), ) if not configuration.default_key_value_store_id: diff --git a/src/apify/storage_clients/_apify/_api_client_creation.py b/src/apify/storage_clients/_apify/_api_client_creation.py index ed5cd6cf..75e95c60 100644 --- a/src/apify/storage_clients/_apify/_api_client_creation.py +++ b/src/apify/storage_clients/_apify/_api_client_creation.py @@ -179,5 +179,4 @@ def _create_api_client(configuration: Configuration) -> ApifyClientAsync: api_public_url=configuration.api_public_base_url, max_retries=8, min_delay_between_retries=timedelta(milliseconds=500), - timeout=timedelta(seconds=360), ) diff --git a/src/apify/storage_clients/_apify/_request_queue_shared_client.py b/src/apify/storage_clients/_apify/_request_queue_shared_client.py index d30921c4..c2758184 100644 --- a/src/apify/storage_clients/_apify/_request_queue_shared_client.py +++ b/src/apify/storage_clients/_apify/_request_queue_shared_client.py @@ -2,7 +2,7 @@ import asyncio from collections import deque -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from logging import getLogger from typing import TYPE_CHECKING, Any, Final @@ -214,7 +214,7 @@ async def mark_request_as_handled(self, request: Request) -> ProcessedRequest | request_id = unique_key_to_request_id(request.unique_key) # Set the handled_at timestamp if not already set if request.handled_at is None: - request.handled_at = datetime.now(tz=timezone.utc) + request.handled_at = datetime.now(tz=UTC) if cached_request := self._requests_cache[request_id]: cached_request.was_already_handled = request.was_already_handled diff --git a/src/apify/storage_clients/_apify/_request_queue_single_client.py b/src/apify/storage_clients/_apify/_request_queue_single_client.py index 9c7004e1..5cf8e91a 100644 --- a/src/apify/storage_clients/_apify/_request_queue_single_client.py +++ b/src/apify/storage_clients/_apify/_request_queue_single_client.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections import deque -from datetime import datetime, timezone +from datetime import UTC, datetime from logging import getLogger from typing import TYPE_CHECKING, Final @@ -204,7 +204,7 @@ async def mark_request_as_handled(self, request: Request) -> ProcessedRequest | cached_request.handled_at = request.handled_at if request.handled_at is None: - request.handled_at = datetime.now(tz=timezone.utc) + request.handled_at = datetime.now(tz=UTC) self.metadata.handled_request_count += 1 self.metadata.pending_request_count -= 1 @@ -383,8 +383,11 @@ async def _init_caches(self) -> None: """ response = await self._api_client.list_requests(limit=10_000) for request_data in response.items: - request = to_crawlee_request(request_data) request_id = request_data.id + if request_id is None: + continue + + request = to_crawlee_request(request_data) if request.was_already_handled: # Cache just id for deduplication diff --git a/src/apify/storage_clients/_file_system/_dataset_client.py b/src/apify/storage_clients/_file_system/_dataset_client.py index 04af9ae2..830a1bb9 100644 --- a/src/apify/storage_clients/_file_system/_dataset_client.py +++ b/src/apify/storage_clients/_file_system/_dataset_client.py @@ -1,8 +1,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Self -from typing_extensions import Self, override +from typing_extensions import override from crawlee.storage_clients._file_system import FileSystemDatasetClient diff --git a/src/apify/storage_clients/_file_system/_key_value_store_client.py b/src/apify/storage_clients/_file_system/_key_value_store_client.py index c21b8bd2..8460dfcc 100644 --- a/src/apify/storage_clients/_file_system/_key_value_store_client.py +++ b/src/apify/storage_clients/_file_system/_key_value_store_client.py @@ -3,8 +3,9 @@ import logging from itertools import chain from pathlib import Path +from typing import Self -from typing_extensions import Self, override +from typing_extensions import override from crawlee._consts import METADATA_FILENAME from crawlee._utils.file import atomic_write, infer_mime_type, json_dumps diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 12bd2bbb..1ea71318 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -314,7 +314,7 @@ async def _make_actor( { 'versionNumber': '0.0', 'buildTag': 'latest', - 'sourceType': VersionSourceType.SOURCE_FILES.value, + 'sourceType': VersionSourceType.SOURCE_FILES, 'sourceFiles': source_files_for_api, } ], diff --git a/tests/e2e/test_actor_api_helpers.py b/tests/e2e/test_actor_api_helpers.py index e5fd7474..e9c9de74 100644 --- a/tests/e2e/test_actor_api_helpers.py +++ b/tests/e2e/test_actor_api_helpers.py @@ -452,7 +452,7 @@ async def main_client() -> None: await Actor.add_webhook( Webhook( - event_types=[WebhookEventType.ACTOR_RUN_SUCCEEDED.value], + event_types=[WebhookEventType.ACTOR_RUN_SUCCEEDED], request_url=server_actor_container_url, ) ) diff --git a/tests/e2e/test_actor_call_timeouts.py b/tests/e2e/test_actor_call_timeouts.py index 796d6a9b..ddada096 100644 --- a/tests/e2e/test_actor_call_timeouts.py +++ b/tests/e2e/test_actor_call_timeouts.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +from datetime import UTC from typing import TYPE_CHECKING from apify import Actor @@ -19,7 +20,7 @@ async def test_actor_start_inherit_timeout( Timeout should be the remaining time of the first Actor run calculated at the moment of the other Actor start.""" async def main() -> None: - from datetime import datetime, timedelta, timezone + from datetime import datetime, timedelta async with Actor: actor_input = (await Actor.get_input()) or {} @@ -42,7 +43,7 @@ async def main() -> None: assert Actor.configuration.timeout_at is not None assert Actor.configuration.started_at is not None - remaining_time_after_actor_start = Actor.configuration.timeout_at - datetime.now(tz=timezone.utc) + remaining_time_after_actor_start = Actor.configuration.timeout_at - datetime.now(tz=UTC) other_timeout = timedelta(seconds=other_run_data.options.timeout_secs) total_timeout = Actor.configuration.timeout_at - Actor.configuration.started_at @@ -69,7 +70,7 @@ async def test_actor_call_inherit_timeout( Timeout should be the remaining time of the first Actor run calculated at the moment of the other Actor call.""" async def main() -> None: - from datetime import datetime, timedelta, timezone + from datetime import datetime, timedelta async with Actor: actor_input = (await Actor.get_input()) or {} @@ -94,7 +95,7 @@ async def main() -> None: assert Actor.configuration.timeout_at is not None assert Actor.configuration.started_at is not None - remaining_time_after_actor_start = Actor.configuration.timeout_at - datetime.now(tz=timezone.utc) + remaining_time_after_actor_start = Actor.configuration.timeout_at - datetime.now(tz=UTC) other_timeout = timedelta(seconds=other_run_data.options.timeout_secs) total_timeout = Actor.configuration.timeout_at - Actor.configuration.started_at diff --git a/tests/e2e/test_actor_charge.py b/tests/e2e/test_actor_charge.py index 51d93b0a..22457526 100644 --- a/tests/e2e/test_actor_charge.py +++ b/tests/e2e/test_actor_charge.py @@ -7,7 +7,7 @@ import pytest_asyncio from apify import Actor -from apify._models import ActorRun +from apify._models import ActorJobStatus, ActorRun if TYPE_CHECKING: from collections.abc import Iterable @@ -54,7 +54,7 @@ async def main() -> None: actor = await actor_client.get() assert actor is not None - return str(actor['id']) + return actor.id @pytest_asyncio.fixture(scope='function', loop_scope='module') diff --git a/tests/e2e/test_actor_lifecycle.py b/tests/e2e/test_actor_lifecycle.py index b4fbffea..3d3792d5 100644 --- a/tests/e2e/test_actor_lifecycle.py +++ b/tests/e2e/test_actor_lifecycle.py @@ -15,7 +15,7 @@ async def test_actor_init_and_double_init_prevention( async def main() -> None: my_actor = Actor await my_actor.init() - assert my_actor._is_initialized is True + assert my_actor._active is True double_init = False try: await my_actor.init() @@ -33,7 +33,7 @@ async def main() -> None: raise await my_actor.exit() assert double_init is False - assert my_actor._is_initialized is False + assert my_actor._active is False actor = await make_actor(label='actor-init', main_func=main) run_result = await run_actor(actor) @@ -49,8 +49,8 @@ async def main() -> None: import apify._actor async with Actor: - assert apify._actor.Actor._is_initialized - assert apify._actor.Actor._is_initialized is False + assert apify._actor.Actor._active + assert apify._actor.Actor._active is False actor = await make_actor(label='with-actor-init', main_func=main) run_result = await run_actor(actor) diff --git a/tests/e2e/test_fixtures.py b/tests/e2e/test_fixtures.py index d6735b3e..3650b8f5 100644 --- a/tests/e2e/test_fixtures.py +++ b/tests/e2e/test_fixtures.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import TYPE_CHECKING from crawlee._utils.crypto import crypto_random_object_id @@ -64,7 +64,7 @@ async def test_actor_from_source_files( make_actor: MakeActorFunction, run_actor: RunActorFunction, ) -> None: - test_started_at = datetime.now(timezone.utc) + test_started_at = datetime.now(UTC) actor_source_files = { 'src/utils.py': """ from datetime import datetime, timezone @@ -93,7 +93,7 @@ async def main(): output_datetime = datetime.fromisoformat(output_record['value']) assert output_datetime > test_started_at - assert output_datetime < datetime.now(timezone.utc) + assert output_datetime < datetime.now(UTC) async def test_apify_client_async_works(apify_client_async: ApifyClientAsync) -> None: diff --git a/tests/integration/test_request_queue.py b/tests/integration/test_request_queue.py index 5f088513..a34cade0 100644 --- a/tests/integration/test_request_queue.py +++ b/tests/integration/test_request_queue.py @@ -2,7 +2,7 @@ import asyncio import logging -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import TYPE_CHECKING, Any, Literal, cast from unittest import mock @@ -974,7 +974,7 @@ async def test_cache_initialization(apify_token: str, monkeypatch: pytest.Monkey request_queue_name = generate_unique_resource_name('request_queue') monkeypatch.setenv(ApifyEnvVars.TOKEN, apify_token) - requests = [Request.from_url(f'http://example.com/{i}', handled_at=datetime.now(timezone.utc)) for i in range(10)] + requests = [Request.from_url(f'http://example.com/{i}', handled_at=datetime.now(UTC)) for i in range(10)] async with Actor: rq = await Actor.open_request_queue(name=request_queue_name, force_cloud=True) diff --git a/tests/unit/actor/test_actor_create_proxy_configuration.py b/tests/unit/actor/test_actor_create_proxy_configuration.py index b441af11..1b31c25c 100644 --- a/tests/unit/actor/test_actor_create_proxy_configuration.py +++ b/tests/unit/actor/test_actor_create_proxy_configuration.py @@ -21,7 +21,7 @@ @pytest.fixture def patched_apify_client(apify_client_async_patcher: ApifyClientAsyncPatcher) -> ApifyClientAsync: - apify_client_async_patcher.patch('user', 'get', return_value={'proxy': {'password': DUMMY_PASSWORD}}) + apify_client_async_patcher.patch('user', 'get', return_value=Mock(proxy=Mock(password=DUMMY_PASSWORD))) return ApifyClientAsync() diff --git a/tests/unit/actor/test_actor_helpers.py b/tests/unit/actor/test_actor_helpers.py index 10227ad2..d1d995e5 100644 --- a/tests/unit/actor/test_actor_helpers.py +++ b/tests/unit/actor/test_actor_helpers.py @@ -3,7 +3,7 @@ import asyncio import logging import warnings -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from typing import TYPE_CHECKING import pytest @@ -157,7 +157,7 @@ async def test_add_webhook_fails_locally(caplog: pytest.LogCaptureFixture) -> No caplog.set_level('WARNING') async with Actor: await Actor.add_webhook( - Webhook(event_types=[WebhookEventType.ACTOR_BUILD_ABORTED.value], request_url='https://example.com') + Webhook(event_types=[WebhookEventType.ACTOR_BUILD_ABORTED], request_url='https://example.com') ) matching = [r for r in caplog.records if 'Actor.add_webhook()' in r.message] @@ -332,7 +332,7 @@ async def test_get_remaining_time_clamps_negative_to_zero() -> None: """Test that _get_remaining_time returns timedelta(0) instead of a negative value when timeout is in the past.""" async with Actor: Actor.configuration.is_at_home = True - Actor.configuration.timeout_at = datetime.now(tz=timezone.utc) - timedelta(minutes=5) + Actor.configuration.timeout_at = datetime.now(tz=UTC) - timedelta(minutes=5) result = Actor._get_remaining_time() assert result is not None @@ -343,7 +343,7 @@ async def test_get_remaining_time_returns_positive_when_timeout_in_future() -> N """Test that _get_remaining_time returns a positive timedelta when timeout is in the future.""" async with Actor: Actor.configuration.is_at_home = True - Actor.configuration.timeout_at = datetime.now(tz=timezone.utc) + timedelta(minutes=5) + Actor.configuration.timeout_at = datetime.now(tz=UTC) + timedelta(minutes=5) result = Actor._get_remaining_time() assert result is not None diff --git a/tests/unit/actor/test_actor_lifecycle.py b/tests/unit/actor/test_actor_lifecycle.py index ed5358ea..22a14cb7 100644 --- a/tests/unit/actor/test_actor_lifecycle.py +++ b/tests/unit/actor/test_actor_lifecycle.py @@ -4,7 +4,7 @@ import contextlib import json import logging -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import TYPE_CHECKING, Any from unittest import mock from unittest.mock import AsyncMock, Mock @@ -245,7 +245,7 @@ async def handler(websocket: websockets.asyncio.server.ServerConnection) -> None 'id': 'asdf', 'actId': 'asdf', 'userId': 'adsf', - 'startedAt': datetime.now(timezone.utc).isoformat(), + 'startedAt': datetime.now(UTC).isoformat(), 'status': 'RUNNING', 'meta': {'origin': 'DEVELOPMENT'}, 'buildId': 'hjkl', diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 94f1f5f2..e7b59341 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -63,7 +63,7 @@ def _prepare_test_env() -> None: if hasattr(apify._actor.Actor, '__wrapped__'): delattr(apify._actor.Actor, '__wrapped__') - apify._actor.Actor._is_initialized = False + apify._actor.Actor._active = False # Set the environment variable for the local storage directory to the temporary path. monkeypatch.setenv(ApifyEnvVars.LOCAL_STORAGE_DIR, str(tmp_path)) diff --git a/tests/unit/test_apify_storages.py b/tests/unit/test_apify_storages.py index d1b7021d..cbd624b7 100644 --- a/tests/unit/test_apify_storages.py +++ b/tests/unit/test_apify_storages.py @@ -1,6 +1,6 @@ import asyncio import json -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path from unittest import mock from unittest.mock import AsyncMock @@ -38,7 +38,7 @@ async def test_get_additional_cache_key( additional cache key.""" def create_metadata(id: str) -> StorageMetadata: - now = datetime.now(tz=timezone.utc) + now = datetime.now(tz=UTC) return StorageMetadata(id=id, name=None, accessed_at=now, created_at=now, modified_at=now) storage_ids = iter(['1', '2', '3', '1', '3']) diff --git a/tests/unit/test_proxy_configuration.py b/tests/unit/test_proxy_configuration.py index e43d2f9e..ba788c09 100644 --- a/tests/unit/test_proxy_configuration.py +++ b/tests/unit/test_proxy_configuration.py @@ -26,15 +26,7 @@ @pytest.fixture def patched_apify_client(apify_client_async_patcher: ApifyClientAsyncPatcher) -> ApifyClientAsync: - apify_client_async_patcher.patch( - 'user', - 'get', - return_value={ - 'proxy': { - 'password': DUMMY_PASSWORD, - }, - }, - ) + apify_client_async_patcher.patch('user', 'get', return_value=Mock(proxy=Mock(password=DUMMY_PASSWORD))) return ApifyClientAsync() diff --git a/website/generate_module_shortcuts.py b/website/generate_module_shortcuts.py index 18516ef5..ce312f04 100755 --- a/website/generate_module_shortcuts.py +++ b/website/generate_module_shortcuts.py @@ -50,7 +50,7 @@ def resolve_shortcuts(shortcuts: dict) -> None: module = importlib.import_module(module_name) module_shortcuts = get_module_shortcuts(module) shortcuts.update(module_shortcuts) - except ModuleNotFoundError: # noqa: PERF203 + except ModuleNotFoundError: pass resolve_shortcuts(shortcuts) From 56c6514e5641ba4e1a12fab8af6b1ede67637ba4 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Thu, 16 Apr 2026 14:23:24 +0200 Subject: [PATCH 3/7] Drop Python 3.10 support (apify-client v3 requires >=3.11) Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/_check_code.yaml | 4 ++-- .github/workflows/_tests.yaml | 6 +++--- pyproject.toml | 1 - 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/_check_code.yaml b/.github/workflows/_check_code.yaml index 21d9df33..4d9569fd 100644 --- a/.github/workflows/_check_code.yaml +++ b/.github/workflows/_check_code.yaml @@ -33,10 +33,10 @@ jobs: name: Lint check uses: apify/workflows/.github/workflows/python_lint_check.yaml@main with: - python_versions: '["3.10", "3.11", "3.12", "3.13", "3.14"]' + python_versions: '["3.11", "3.12", "3.13", "3.14"]' type_check: name: Type check uses: apify/workflows/.github/workflows/python_type_check.yaml@main with: - python_versions: '["3.10", "3.11", "3.12", "3.13", "3.14"]' + python_versions: '["3.11", "3.12", "3.13", "3.14"]' diff --git a/.github/workflows/_tests.yaml b/.github/workflows/_tests.yaml index 15aeb15b..19b984ba 100644 --- a/.github/workflows/_tests.yaml +++ b/.github/workflows/_tests.yaml @@ -16,7 +16,7 @@ jobs: uses: apify/workflows/.github/workflows/python_unit_tests.yaml@main secrets: inherit with: - python_versions: '["3.10", "3.11", "3.12", "3.13", "3.14"]' + python_versions: '["3.11", "3.12", "3.13", "3.14"]' operating_systems: '["ubuntu-latest", "windows-latest"]' python_version_for_codecov: "3.14" operating_system_for_codecov: ubuntu-latest @@ -34,7 +34,7 @@ jobs: strategy: matrix: os: ["ubuntu-latest"] - python-version: ["3.10", "3.14"] + python-version: ["3.11", "3.14"] runs-on: ${{ matrix.os }} @@ -91,7 +91,7 @@ jobs: strategy: matrix: os: ["ubuntu-latest"] - python-version: ["3.10", "3.14"] + python-version: ["3.11", "3.14"] runs-on: ${{ matrix.os }} diff --git a/pyproject.toml b/pyproject.toml index e58f8241..161716e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,6 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", From 5f46e3f0a091afb117a9fa15f3ee06abde97f9fe Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Thu, 16 Apr 2026 14:31:01 +0200 Subject: [PATCH 4/7] Remove apify-shared dependency, define constants locally Move ActorEnvVars, ApifyEnvVars, ActorExitCodes, ActorEventTypes, and env var type classification lists from apify-shared into apify._consts. Update all imports across src/ and tests/. Co-Authored-By: Claude Sonnet 4.6 --- pyproject.toml | 3 +- src/apify/_actor.py | 3 +- src/apify/_consts.py | 165 ++++++++++++++++++ src/apify/_proxy_configuration.py | 2 +- tests/e2e/conftest.py | 2 +- tests/e2e/test_actor_api_helpers.py | 6 +- tests/e2e/test_actor_events.py | 9 +- tests/e2e/test_fixtures.py | 2 +- tests/integration/conftest.py | 2 +- tests/integration/test_dataset.py | 3 +- tests/integration/test_key_value_store.py | 2 +- tests/integration/test_request_queue.py | 2 +- .../test_actor_create_proxy_configuration.py | 2 +- tests/unit/actor/test_actor_env_helpers.py | 5 +- tests/unit/actor/test_actor_helpers.py | 2 +- .../unit/actor/test_actor_key_value_store.py | 3 +- tests/unit/actor/test_actor_lifecycle.py | 2 +- tests/unit/conftest.py | 2 +- tests/unit/events/test_apify_event_manager.py | 2 +- tests/unit/test_proxy_configuration.py | 2 +- uv.lock | 13 +- 21 files changed, 192 insertions(+), 42 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 161716e2..a016a8aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,6 @@ keywords = [ ] dependencies = [ "apify-client @ git+https://github.com/apify/apify-client-python.git@master", - "apify-shared>=2.0.0,<3.0.0", "crawlee>=1.0.4,<2.0.0", "cachetools>=5.5.0", "cryptography>=42.0.0", @@ -187,7 +186,7 @@ builtins-ignorelist = ["id"] [tool.ruff.lint.isort] known-local-folder = ["apify"] -known-first-party = ["apify_client", "apify_shared", "crawlee"] +known-first-party = ["apify_client", "crawlee"] [tool.ruff.lint.pylint] max-branches = 18 diff --git a/src/apify/_actor.py b/src/apify/_actor.py index 50d7f983..56b42dcd 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -13,7 +13,6 @@ from pydantic import AliasChoices from apify_client import ApifyClientAsync -from apify_shared.consts import ActorEnvVars, ActorExitCodes, ApifyEnvVars from crawlee import service_locator from crawlee.errors import ServiceConflictError from crawlee.events import ( @@ -28,7 +27,7 @@ from apify._charging import DEFAULT_DATASET_ITEM_EVENT, ChargeResult, ChargingManager, ChargingManagerImplementation from apify._configuration import Configuration -from apify._consts import EVENT_LISTENERS_TIMEOUT +from apify._consts import EVENT_LISTENERS_TIMEOUT, ActorEnvVars, ActorExitCodes, ApifyEnvVars from apify._crypto import decrypt_input_secrets, load_private_key from apify._models import ActorRun from apify._proxy_configuration import ProxyConfiguration diff --git a/src/apify/_consts.py b/src/apify/_consts.py index 9bef9cdb..7c519558 100644 --- a/src/apify/_consts.py +++ b/src/apify/_consts.py @@ -2,6 +2,8 @@ import re from datetime import timedelta +from enum import Enum, StrEnum +from typing import Literal, get_args EVENT_LISTENERS_TIMEOUT = timedelta(seconds=5) @@ -11,3 +13,166 @@ ENCRYPTED_INPUT_VALUE_REGEXP = re.compile( f'^({ENCRYPTED_STRING_VALUE_PREFIX}|{ENCRYPTED_JSON_VALUE_PREFIX}):(?:({BASE64_REGEXP}):)?({BASE64_REGEXP}):({BASE64_REGEXP})$' ) + + +class ActorEventTypes(StrEnum): + """Event types that can be sent to Actors during execution.""" + + SYSTEM_INFO = 'systemInfo' + MIGRATING = 'migrating' + PERSIST_STATE = 'persistState' + ABORTING = 'aborting' + + +class ActorEnvVars(StrEnum): + """Environment variables with ACTOR_ prefix set by the Apify platform.""" + + BUILD_ID = 'ACTOR_BUILD_ID' + BUILD_NUMBER = 'ACTOR_BUILD_NUMBER' + BUILD_TAGS = 'ACTOR_BUILD_TAGS' + DEFAULT_DATASET_ID = 'ACTOR_DEFAULT_DATASET_ID' + DEFAULT_KEY_VALUE_STORE_ID = 'ACTOR_DEFAULT_KEY_VALUE_STORE_ID' + DEFAULT_REQUEST_QUEUE_ID = 'ACTOR_DEFAULT_REQUEST_QUEUE_ID' + EVENTS_WEBSOCKET_URL = 'ACTOR_EVENTS_WEBSOCKET_URL' + FULL_NAME = 'ACTOR_FULL_NAME' + ID = 'ACTOR_ID' + INPUT_KEY = 'ACTOR_INPUT_KEY' + MAX_PAID_DATASET_ITEMS = 'ACTOR_MAX_PAID_DATASET_ITEMS' + MAX_TOTAL_CHARGE_USD = 'ACTOR_MAX_TOTAL_CHARGE_USD' + MEMORY_MBYTES = 'ACTOR_MEMORY_MBYTES' + PERMISSION_LEVEL = 'ACTOR_PERMISSION_LEVEL' + RUN_ID = 'ACTOR_RUN_ID' + STANDBY_PORT = 'ACTOR_STANDBY_PORT' + STANDBY_URL = 'ACTOR_STANDBY_URL' + STARTED_AT = 'ACTOR_STARTED_AT' + TASK_ID = 'ACTOR_TASK_ID' + TIMEOUT_AT = 'ACTOR_TIMEOUT_AT' + WEB_SERVER_PORT = 'ACTOR_WEB_SERVER_PORT' + WEB_SERVER_URL = 'ACTOR_WEB_SERVER_URL' + + +class ApifyEnvVars(StrEnum): + """Environment variables with APIFY_ prefix set by the Apify platform.""" + + API_BASE_URL = 'APIFY_API_BASE_URL' + API_PUBLIC_BASE_URL = 'APIFY_API_PUBLIC_BASE_URL' + DEDICATED_CPUS = 'APIFY_DEDICATED_CPUS' + DEFAULT_BROWSER_PATH = 'APIFY_DEFAULT_BROWSER_PATH' + DISABLE_BROWSER_SANDBOX = 'APIFY_DISABLE_BROWSER_SANDBOX' + DISABLE_OUTDATED_WARNING = 'APIFY_DISABLE_OUTDATED_WARNING' + FACT = 'APIFY_FACT' + HEADLESS = 'APIFY_HEADLESS' + INPUT_SECRETS_PRIVATE_KEY_FILE = 'APIFY_INPUT_SECRETS_PRIVATE_KEY_FILE' + INPUT_SECRETS_PRIVATE_KEY_PASSPHRASE = 'APIFY_INPUT_SECRETS_PRIVATE_KEY_PASSPHRASE' + IS_AT_HOME = 'APIFY_IS_AT_HOME' + LOCAL_STORAGE_DIR = 'APIFY_LOCAL_STORAGE_DIR' + LOG_FORMAT = 'APIFY_LOG_FORMAT' + LOG_LEVEL = 'APIFY_LOG_LEVEL' + MAX_USED_CPU_RATIO = 'APIFY_MAX_USED_CPU_RATIO' + META_ORIGIN = 'APIFY_META_ORIGIN' + METAMORPH_AFTER_SLEEP_MILLIS = 'APIFY_METAMORPH_AFTER_SLEEP_MILLIS' + PERSIST_STATE_INTERVAL_MILLIS = 'APIFY_PERSIST_STATE_INTERVAL_MILLIS' + PERSIST_STORAGE = 'APIFY_PERSIST_STORAGE' + PROXY_HOSTNAME = 'APIFY_PROXY_HOSTNAME' + PROXY_PASSWORD = 'APIFY_PROXY_PASSWORD' + PROXY_PORT = 'APIFY_PROXY_PORT' + PROXY_STATUS_URL = 'APIFY_PROXY_STATUS_URL' + PURGE_ON_START = 'APIFY_PURGE_ON_START' + SDK_LATEST_VERSION = 'APIFY_SDK_LATEST_VERSION' + SYSTEM_INFO_INTERVAL_MILLIS = 'APIFY_SYSTEM_INFO_INTERVAL_MILLIS' + TOKEN = 'APIFY_TOKEN' + USER_ID = 'APIFY_USER_ID' + USER_IS_PAYING = 'APIFY_USER_IS_PAYING' + WORKFLOW_KEY = 'APIFY_WORKFLOW_KEY' + + +class ActorExitCodes(int, Enum): + """Standard exit codes used by Actors to indicate run completion status.""" + + SUCCESS = 0 + ERROR_USER_FUNCTION_THREW = 91 + + +# Environment variable type classification lists. + +INTEGER_ENV_VARS_TYPE = Literal[ + ActorEnvVars.MAX_PAID_DATASET_ITEMS, + ActorEnvVars.MEMORY_MBYTES, + ActorEnvVars.STANDBY_PORT, + ActorEnvVars.WEB_SERVER_PORT, + ApifyEnvVars.DEDICATED_CPUS, + ApifyEnvVars.LOG_LEVEL, + ApifyEnvVars.METAMORPH_AFTER_SLEEP_MILLIS, + ApifyEnvVars.PERSIST_STATE_INTERVAL_MILLIS, + ApifyEnvVars.PROXY_PORT, + ApifyEnvVars.SYSTEM_INFO_INTERVAL_MILLIS, +] + +INTEGER_ENV_VARS: list[INTEGER_ENV_VARS_TYPE] = list(get_args(INTEGER_ENV_VARS_TYPE)) + +FLOAT_ENV_VARS_TYPE = Literal[ + ActorEnvVars.MAX_TOTAL_CHARGE_USD, + ApifyEnvVars.MAX_USED_CPU_RATIO, +] + +FLOAT_ENV_VARS: list[FLOAT_ENV_VARS_TYPE] = list(get_args(FLOAT_ENV_VARS_TYPE)) + +BOOL_ENV_VARS_TYPE = Literal[ + ApifyEnvVars.DISABLE_BROWSER_SANDBOX, + ApifyEnvVars.DISABLE_OUTDATED_WARNING, + ApifyEnvVars.HEADLESS, + ApifyEnvVars.IS_AT_HOME, + ApifyEnvVars.PERSIST_STORAGE, + ApifyEnvVars.PURGE_ON_START, + ApifyEnvVars.USER_IS_PAYING, +] + +BOOL_ENV_VARS: list[BOOL_ENV_VARS_TYPE] = list(get_args(BOOL_ENV_VARS_TYPE)) + +DATETIME_ENV_VARS_TYPE = Literal[ + ActorEnvVars.STARTED_AT, + ActorEnvVars.TIMEOUT_AT, +] + +DATETIME_ENV_VARS: list[DATETIME_ENV_VARS_TYPE] = list(get_args(DATETIME_ENV_VARS_TYPE)) + +STRING_ENV_VARS_TYPE = Literal[ + ActorEnvVars.BUILD_ID, + ActorEnvVars.BUILD_NUMBER, + ActorEnvVars.DEFAULT_DATASET_ID, + ActorEnvVars.DEFAULT_KEY_VALUE_STORE_ID, + ActorEnvVars.DEFAULT_REQUEST_QUEUE_ID, + ActorEnvVars.EVENTS_WEBSOCKET_URL, + ActorEnvVars.FULL_NAME, + ActorEnvVars.ID, + ActorEnvVars.INPUT_KEY, + ActorEnvVars.PERMISSION_LEVEL, + ActorEnvVars.RUN_ID, + ActorEnvVars.STANDBY_URL, + ActorEnvVars.TASK_ID, + ActorEnvVars.WEB_SERVER_URL, + ApifyEnvVars.API_BASE_URL, + ApifyEnvVars.API_PUBLIC_BASE_URL, + ApifyEnvVars.DEFAULT_BROWSER_PATH, + ApifyEnvVars.FACT, + ApifyEnvVars.INPUT_SECRETS_PRIVATE_KEY_FILE, + ApifyEnvVars.INPUT_SECRETS_PRIVATE_KEY_PASSPHRASE, + ApifyEnvVars.LOCAL_STORAGE_DIR, + ApifyEnvVars.LOG_FORMAT, + ApifyEnvVars.META_ORIGIN, + ApifyEnvVars.PROXY_HOSTNAME, + ApifyEnvVars.PROXY_PASSWORD, + ApifyEnvVars.PROXY_STATUS_URL, + ApifyEnvVars.SDK_LATEST_VERSION, + ApifyEnvVars.TOKEN, + ApifyEnvVars.USER_ID, + ApifyEnvVars.WORKFLOW_KEY, +] + +STRING_ENV_VARS: list[STRING_ENV_VARS_TYPE] = list(get_args(STRING_ENV_VARS_TYPE)) + +COMMA_SEPARATED_LIST_ENV_VARS_TYPE = Literal[ActorEnvVars.BUILD_TAGS] + +COMMA_SEPARATED_LIST_ENV_VARS: list[COMMA_SEPARATED_LIST_ENV_VARS_TYPE] = list( + get_args(COMMA_SEPARATED_LIST_ENV_VARS_TYPE) +) diff --git a/src/apify/_proxy_configuration.py b/src/apify/_proxy_configuration.py index 98579598..e7089f80 100644 --- a/src/apify/_proxy_configuration.py +++ b/src/apify/_proxy_configuration.py @@ -11,12 +11,12 @@ import impit from yarl import URL -from apify_shared.consts import ApifyEnvVars from crawlee.proxy_configuration import ProxyConfiguration as CrawleeProxyConfiguration from crawlee.proxy_configuration import ProxyInfo as CrawleeProxyInfo from crawlee.proxy_configuration import _NewUrlFunction from apify._configuration import Configuration +from apify._consts import ApifyEnvVars from apify._utils import docs_group from apify.log import logger diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 1ea71318..d6c97830 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -15,11 +15,11 @@ from apify_client import ApifyClient, ApifyClientAsync from apify_client._models import ActorPermissionLevel, VersionSourceType -from apify_shared.consts import ApifyEnvVars from crawlee import service_locator import apify._actor from ._utils import generate_unique_resource_name +from apify._consts import ApifyEnvVars from apify._models import ActorRun from apify.storage_clients._apify._alias_resolving import AliasResolver diff --git a/tests/e2e/test_actor_api_helpers.py b/tests/e2e/test_actor_api_helpers.py index e9c9de74..3b494c0e 100644 --- a/tests/e2e/test_actor_api_helpers.py +++ b/tests/e2e/test_actor_api_helpers.py @@ -63,7 +63,7 @@ async def test_actor_creates_new_client_instance( async def main() -> None: import os - from apify_shared.consts import ActorEnvVars + from apify._consts import ActorEnvVars async with Actor: new_client = Actor.new_client() @@ -316,7 +316,7 @@ async def test_actor_metamorphs_into_another_actor( async def main_inner() -> None: import os - from apify_shared.consts import ActorEnvVars + from apify._consts import ActorEnvVars async with Actor: assert os.getenv(ActorEnvVars.INPUT_KEY) is not None @@ -414,7 +414,7 @@ async def main_server() -> None: import os from http.server import BaseHTTPRequestHandler, HTTPServer - from apify_shared.consts import ActorEnvVars + from apify._consts import ActorEnvVars webhook_body = '' diff --git a/tests/e2e/test_actor_events.py b/tests/e2e/test_actor_events.py index 0617f0e0..61bfd88b 100644 --- a/tests/e2e/test_actor_events.py +++ b/tests/e2e/test_actor_events.py @@ -3,9 +3,8 @@ import asyncio from typing import TYPE_CHECKING -from apify_shared.consts import ActorEventTypes - from apify import Actor +from apify._consts import ActorEventTypes if TYPE_CHECKING: from .conftest import MakeActorFunction, RunActorFunction @@ -22,9 +21,10 @@ async def main() -> None: from datetime import datetime from typing import Any - from apify_shared.consts import ActorEventTypes, ApifyEnvVars from crawlee.events._types import Event, EventSystemInfoData + from apify._consts import ActorEventTypes, ApifyEnvVars + os.environ[ApifyEnvVars.PERSIST_STATE_INTERVAL_MILLIS] = '900' was_system_info_emitted = False @@ -81,9 +81,10 @@ async def main() -> None: import os from typing import Any - from apify_shared.consts import ApifyEnvVars from crawlee.events._types import Event + from apify._consts import ApifyEnvVars + os.environ[ApifyEnvVars.PERSIST_STATE_INTERVAL_MILLIS] = '100' counter = 0 diff --git a/tests/e2e/test_fixtures.py b/tests/e2e/test_fixtures.py index 3650b8f5..f585733f 100644 --- a/tests/e2e/test_fixtures.py +++ b/tests/e2e/test_fixtures.py @@ -20,7 +20,7 @@ async def test_actor_from_main_func( async def main() -> None: import os - from apify_shared.consts import ActorEnvVars + from apify._consts import ActorEnvVars async with Actor: await Actor.set_value('OUTPUT', os.getenv(ActorEnvVars.ID)) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index ee67aa90..512b2732 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -6,11 +6,11 @@ import pytest from apify_client import ApifyClientAsync -from apify_shared.consts import ApifyEnvVars from crawlee import service_locator import apify._actor from apify import Actor +from apify._consts import ApifyEnvVars from apify.storage_clients import ApifyStorageClient from apify.storage_clients._apify._alias_resolving import AliasResolver from apify.storages import RequestQueue diff --git a/tests/integration/test_dataset.py b/tests/integration/test_dataset.py index 63598023..2cb28b02 100644 --- a/tests/integration/test_dataset.py +++ b/tests/integration/test_dataset.py @@ -4,10 +4,9 @@ import pytest -from apify_shared.consts import ApifyEnvVars - from ._utils import generate_unique_resource_name from apify import Actor +from apify._consts import ApifyEnvVars from apify.storage_clients import ApifyStorageClient from apify.storages import Dataset diff --git a/tests/integration/test_key_value_store.py b/tests/integration/test_key_value_store.py index 8b94acd4..8673eb09 100644 --- a/tests/integration/test_key_value_store.py +++ b/tests/integration/test_key_value_store.py @@ -4,11 +4,11 @@ import pytest -from apify_shared.consts import ApifyEnvVars from crawlee import service_locator from ._utils import generate_unique_resource_name from apify import Actor +from apify._consts import ApifyEnvVars from apify.storage_clients import ApifyStorageClient from apify.storage_clients._apify._alias_resolving import AliasResolver from apify.storages import KeyValueStore diff --git a/tests/integration/test_request_queue.py b/tests/integration/test_request_queue.py index a34cade0..142cd62d 100644 --- a/tests/integration/test_request_queue.py +++ b/tests/integration/test_request_queue.py @@ -9,12 +9,12 @@ import pytest from apify_client._models import BatchAddResult, RequestDraft -from apify_shared.consts import ApifyEnvVars from crawlee import service_locator from crawlee.crawlers import BasicCrawler from ._utils import call_with_exp_backoff, generate_unique_resource_name, poll_until_condition from apify import Actor, Request +from apify._consts import ApifyEnvVars from apify.storage_clients import ApifyStorageClient from apify.storage_clients._apify import ApifyRequestQueueClient from apify.storage_clients._apify._utils import unique_key_to_request_id diff --git a/tests/unit/actor/test_actor_create_proxy_configuration.py b/tests/unit/actor/test_actor_create_proxy_configuration.py index 1b31c25c..f7bf95a7 100644 --- a/tests/unit/actor/test_actor_create_proxy_configuration.py +++ b/tests/unit/actor/test_actor_create_proxy_configuration.py @@ -6,9 +6,9 @@ import pytest from apify_client import ApifyClientAsync -from apify_shared.consts import ApifyEnvVars from apify import Actor +from apify._consts import ApifyEnvVars if TYPE_CHECKING: from pytest_httpserver import HTTPServer diff --git a/tests/unit/actor/test_actor_env_helpers.py b/tests/unit/actor/test_actor_env_helpers.py index 25e337bb..3b44f879 100644 --- a/tests/unit/actor/test_actor_env_helpers.py +++ b/tests/unit/actor/test_actor_env_helpers.py @@ -8,7 +8,8 @@ from pydantic_core import TzInfo -from apify_shared.consts import ( +from apify import Actor +from apify._consts import ( BOOL_ENV_VARS, COMMA_SEPARATED_LIST_ENV_VARS, DATETIME_ENV_VARS, @@ -19,8 +20,6 @@ ApifyEnvVars, ) -from apify import Actor - if TYPE_CHECKING: from pathlib import Path diff --git a/tests/unit/actor/test_actor_helpers.py b/tests/unit/actor/test_actor_helpers.py index d1d995e5..bc7cfd46 100644 --- a/tests/unit/actor/test_actor_helpers.py +++ b/tests/unit/actor/test_actor_helpers.py @@ -10,11 +10,11 @@ from apify_client import ApifyClientAsync from apify_client._models import Run, WebhookEventType -from apify_shared.consts import ApifyEnvVars from crawlee.events._types import Event from apify import Actor, Webhook from apify._actor import _ActorType +from apify._consts import ApifyEnvVars if TYPE_CHECKING: from ..conftest import ApifyClientAsyncPatcher diff --git a/tests/unit/actor/test_actor_key_value_store.py b/tests/unit/actor/test_actor_key_value_store.py index 581d775d..94bd959f 100644 --- a/tests/unit/actor/test_actor_key_value_store.py +++ b/tests/unit/actor/test_actor_key_value_store.py @@ -4,12 +4,11 @@ import pytest -from apify_shared.consts import ApifyEnvVars from crawlee._utils.file import json_dumps from ..test_crypto import PRIVATE_KEY_PASSWORD, PRIVATE_KEY_PEM_BASE64, PUBLIC_KEY from apify import Actor -from apify._consts import ENCRYPTED_JSON_VALUE_PREFIX, ENCRYPTED_STRING_VALUE_PREFIX +from apify._consts import ENCRYPTED_JSON_VALUE_PREFIX, ENCRYPTED_STRING_VALUE_PREFIX, ApifyEnvVars from apify._crypto import public_encrypt diff --git a/tests/unit/actor/test_actor_lifecycle.py b/tests/unit/actor/test_actor_lifecycle.py index 22a14cb7..04dbd4d8 100644 --- a/tests/unit/actor/test_actor_lifecycle.py +++ b/tests/unit/actor/test_actor_lifecycle.py @@ -14,10 +14,10 @@ import websockets.asyncio.server from apify_client._models import Run -from apify_shared.consts import ActorEnvVars, ActorExitCodes, ApifyEnvVars from crawlee.events._types import Event, EventPersistStateData from apify import Actor +from apify._consts import ActorEnvVars, ActorExitCodes, ApifyEnvVars if TYPE_CHECKING: from collections.abc import AsyncGenerator, Callable diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index e7b59341..656d43f3 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -12,11 +12,11 @@ from pytest_httpserver import HTTPServer from apify_client import ApifyClientAsync -from apify_shared.consts import ApifyEnvVars from crawlee import service_locator import apify._actor import apify.log +from apify._consts import ApifyEnvVars from apify.storage_clients._apify._alias_resolving import AliasResolver if TYPE_CHECKING: diff --git a/tests/unit/events/test_apify_event_manager.py b/tests/unit/events/test_apify_event_manager.py index af53fa8c..780d1b6f 100644 --- a/tests/unit/events/test_apify_event_manager.py +++ b/tests/unit/events/test_apify_event_manager.py @@ -13,10 +13,10 @@ import websockets import websockets.asyncio.server -from apify_shared.consts import ActorEnvVars from crawlee.events._types import Event from apify import Configuration +from apify._consts import ActorEnvVars from apify.events import ApifyEventManager from apify.events._types import SystemInfoEventData diff --git a/tests/unit/test_proxy_configuration.py b/tests/unit/test_proxy_configuration.py index ba788c09..fa000b1f 100644 --- a/tests/unit/test_proxy_configuration.py +++ b/tests/unit/test_proxy_configuration.py @@ -11,8 +11,8 @@ import pytest from apify_client import ApifyClientAsync -from apify_shared.consts import ApifyEnvVars +from apify._consts import ApifyEnvVars from apify._proxy_configuration import ProxyConfiguration, is_url if TYPE_CHECKING: diff --git a/uv.lock b/uv.lock index 29217123..5bcb5da2 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.11" [options] -exclude-newer = "2026-04-15T11:49:40.072177007Z" +exclude-newer = "2026-04-15T12:29:43.191413152Z" exclude-newer-span = "PT24H" [[package]] @@ -34,7 +34,6 @@ version = "3.3.2" source = { editable = "." } dependencies = [ { name = "apify-client" }, - { name = "apify-shared" }, { name = "cachetools" }, { name = "crawlee" }, { name = "cryptography" }, @@ -79,7 +78,6 @@ dev = [ [package.metadata] requires-dist = [ { name = "apify-client", git = "https://github.com/apify/apify-client-python.git?rev=master" }, - { name = "apify-shared", specifier = ">=2.0.0,<3.0.0" }, { name = "cachetools", specifier = ">=5.5.0" }, { name = "crawlee", specifier = ">=1.0.4,<2.0.0" }, { name = "cryptography", specifier = ">=42.0.0" }, @@ -129,15 +127,6 @@ dependencies = [ { name = "pydantic", extra = ["email"] }, ] -[[package]] -name = "apify-shared" -version = "2.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/88/8833a8bba9044ce134bb2e57fbb626f1ddbeecac964bc2e2b652a50fadd1/apify_shared-2.2.0.tar.gz", hash = "sha256:ad48a96084e3c38faa1bac723a47929a1bb2c771544da2f0cb503eabdecfc79a", size = 45534, upload-time = "2026-01-15T10:17:14.592Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/7c/9607852e2bb324fa40a5b967e162dea1b3c76b429cf90b602e4a202c101a/apify_shared-2.2.0-py3-none-any.whl", hash = "sha256:667d4d00ac3cf8091702640547387ac5c72a1df402bbb3923f7a401bc25d9d50", size = 16408, upload-time = "2026-01-15T10:17:13.103Z" }, -] - [[package]] name = "async-timeout" version = "5.0.1" From 05cf57ec77fca5139c1ac1f7f4fa52e37d35265b Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Thu, 16 Apr 2026 14:41:37 +0200 Subject: [PATCH 5/7] Clean up _consts.py: remove unused ActorEventTypes, move env var classification lists to test file, inline BASE64_REGEXP Co-Authored-By: Claude Sonnet 4.6 --- src/apify/_consts.py | 104 ++------------------- tests/e2e/test_actor_events.py | 19 ++-- tests/unit/actor/test_actor_env_helpers.py | 81 ++++++++++++++-- 3 files changed, 85 insertions(+), 119 deletions(-) diff --git a/src/apify/_consts.py b/src/apify/_consts.py index 7c519558..c3874e27 100644 --- a/src/apify/_consts.py +++ b/src/apify/_consts.py @@ -3,25 +3,20 @@ import re from datetime import timedelta from enum import Enum, StrEnum -from typing import Literal, get_args EVENT_LISTENERS_TIMEOUT = timedelta(seconds=5) +"""Timeout for waiting on event listeners to finish during Actor exit.""" -BASE64_REGEXP = '[-A-Za-z0-9+/]*={0,3}' ENCRYPTED_STRING_VALUE_PREFIX = 'ENCRYPTED_VALUE' +"""Prefix for encrypted string values in Actor input.""" + ENCRYPTED_JSON_VALUE_PREFIX = 'ENCRYPTED_JSON' +"""Prefix for encrypted JSON values in Actor input.""" + ENCRYPTED_INPUT_VALUE_REGEXP = re.compile( - f'^({ENCRYPTED_STRING_VALUE_PREFIX}|{ENCRYPTED_JSON_VALUE_PREFIX}):(?:({BASE64_REGEXP}):)?({BASE64_REGEXP}):({BASE64_REGEXP})$' + r'^(ENCRYPTED_VALUE|ENCRYPTED_JSON):(?:([-A-Za-z0-9+/]*={0,3}):)?([-A-Za-z0-9+/]*={0,3}):([-A-Za-z0-9+/]*={0,3})$' ) - - -class ActorEventTypes(StrEnum): - """Event types that can be sent to Actors during execution.""" - - SYSTEM_INFO = 'systemInfo' - MIGRATING = 'migrating' - PERSIST_STATE = 'persistState' - ABORTING = 'aborting' +"""Regex matching encrypted input values with base64-encoded components.""" class ActorEnvVars(StrEnum): @@ -91,88 +86,3 @@ class ActorExitCodes(int, Enum): SUCCESS = 0 ERROR_USER_FUNCTION_THREW = 91 - - -# Environment variable type classification lists. - -INTEGER_ENV_VARS_TYPE = Literal[ - ActorEnvVars.MAX_PAID_DATASET_ITEMS, - ActorEnvVars.MEMORY_MBYTES, - ActorEnvVars.STANDBY_PORT, - ActorEnvVars.WEB_SERVER_PORT, - ApifyEnvVars.DEDICATED_CPUS, - ApifyEnvVars.LOG_LEVEL, - ApifyEnvVars.METAMORPH_AFTER_SLEEP_MILLIS, - ApifyEnvVars.PERSIST_STATE_INTERVAL_MILLIS, - ApifyEnvVars.PROXY_PORT, - ApifyEnvVars.SYSTEM_INFO_INTERVAL_MILLIS, -] - -INTEGER_ENV_VARS: list[INTEGER_ENV_VARS_TYPE] = list(get_args(INTEGER_ENV_VARS_TYPE)) - -FLOAT_ENV_VARS_TYPE = Literal[ - ActorEnvVars.MAX_TOTAL_CHARGE_USD, - ApifyEnvVars.MAX_USED_CPU_RATIO, -] - -FLOAT_ENV_VARS: list[FLOAT_ENV_VARS_TYPE] = list(get_args(FLOAT_ENV_VARS_TYPE)) - -BOOL_ENV_VARS_TYPE = Literal[ - ApifyEnvVars.DISABLE_BROWSER_SANDBOX, - ApifyEnvVars.DISABLE_OUTDATED_WARNING, - ApifyEnvVars.HEADLESS, - ApifyEnvVars.IS_AT_HOME, - ApifyEnvVars.PERSIST_STORAGE, - ApifyEnvVars.PURGE_ON_START, - ApifyEnvVars.USER_IS_PAYING, -] - -BOOL_ENV_VARS: list[BOOL_ENV_VARS_TYPE] = list(get_args(BOOL_ENV_VARS_TYPE)) - -DATETIME_ENV_VARS_TYPE = Literal[ - ActorEnvVars.STARTED_AT, - ActorEnvVars.TIMEOUT_AT, -] - -DATETIME_ENV_VARS: list[DATETIME_ENV_VARS_TYPE] = list(get_args(DATETIME_ENV_VARS_TYPE)) - -STRING_ENV_VARS_TYPE = Literal[ - ActorEnvVars.BUILD_ID, - ActorEnvVars.BUILD_NUMBER, - ActorEnvVars.DEFAULT_DATASET_ID, - ActorEnvVars.DEFAULT_KEY_VALUE_STORE_ID, - ActorEnvVars.DEFAULT_REQUEST_QUEUE_ID, - ActorEnvVars.EVENTS_WEBSOCKET_URL, - ActorEnvVars.FULL_NAME, - ActorEnvVars.ID, - ActorEnvVars.INPUT_KEY, - ActorEnvVars.PERMISSION_LEVEL, - ActorEnvVars.RUN_ID, - ActorEnvVars.STANDBY_URL, - ActorEnvVars.TASK_ID, - ActorEnvVars.WEB_SERVER_URL, - ApifyEnvVars.API_BASE_URL, - ApifyEnvVars.API_PUBLIC_BASE_URL, - ApifyEnvVars.DEFAULT_BROWSER_PATH, - ApifyEnvVars.FACT, - ApifyEnvVars.INPUT_SECRETS_PRIVATE_KEY_FILE, - ApifyEnvVars.INPUT_SECRETS_PRIVATE_KEY_PASSPHRASE, - ApifyEnvVars.LOCAL_STORAGE_DIR, - ApifyEnvVars.LOG_FORMAT, - ApifyEnvVars.META_ORIGIN, - ApifyEnvVars.PROXY_HOSTNAME, - ApifyEnvVars.PROXY_PASSWORD, - ApifyEnvVars.PROXY_STATUS_URL, - ApifyEnvVars.SDK_LATEST_VERSION, - ApifyEnvVars.TOKEN, - ApifyEnvVars.USER_ID, - ApifyEnvVars.WORKFLOW_KEY, -] - -STRING_ENV_VARS: list[STRING_ENV_VARS_TYPE] = list(get_args(STRING_ENV_VARS_TYPE)) - -COMMA_SEPARATED_LIST_ENV_VARS_TYPE = Literal[ActorEnvVars.BUILD_TAGS] - -COMMA_SEPARATED_LIST_ENV_VARS: list[COMMA_SEPARATED_LIST_ENV_VARS_TYPE] = list( - get_args(COMMA_SEPARATED_LIST_ENV_VARS_TYPE) -) diff --git a/tests/e2e/test_actor_events.py b/tests/e2e/test_actor_events.py index 61bfd88b..eb1834bc 100644 --- a/tests/e2e/test_actor_events.py +++ b/tests/e2e/test_actor_events.py @@ -4,7 +4,6 @@ from typing import TYPE_CHECKING from apify import Actor -from apify._consts import ActorEventTypes if TYPE_CHECKING: from .conftest import MakeActorFunction, RunActorFunction @@ -23,28 +22,28 @@ async def main() -> None: from crawlee.events._types import Event, EventSystemInfoData - from apify._consts import ActorEventTypes, ApifyEnvVars + from apify._consts import ApifyEnvVars os.environ[ApifyEnvVars.PERSIST_STATE_INTERVAL_MILLIS] = '900' was_system_info_emitted = False system_infos = list[EventSystemInfoData]() - def on_event(event_type: ActorEventTypes) -> Callable: + def on_event(event_type: str) -> Callable: async def log_event(data: Any) -> None: nonlocal was_system_info_emitted nonlocal system_infos print(f'Got actor event ({event_type=}, {data=})') await Actor.push_data({'event_type': event_type, 'data': data}) - if event_type == ActorEventTypes.SYSTEM_INFO: + if event_type == 'systemInfo': was_system_info_emitted = True system_infos.append(data) return log_event async with Actor: - Actor.on(Event.SYSTEM_INFO, on_event(ActorEventTypes.SYSTEM_INFO)) - Actor.on(Event.PERSIST_STATE, on_event(ActorEventTypes.PERSIST_STATE)) + Actor.on(Event.SYSTEM_INFO, on_event('systemInfo')) + Actor.on(Event.PERSIST_STATE, on_event('persistState')) await asyncio.sleep(3) # The SYSTEM_INFO event sometimes takes a while to appear, let's wait for it for a while longer. @@ -63,12 +62,8 @@ async def log_event(data: Any) -> None: assert run_result.status.value == 'SUCCEEDED' dataset_items_page = await actor.last_run().dataset().list_items() - persist_state_events = [ - item for item in dataset_items_page.items if item['event_type'] == ActorEventTypes.PERSIST_STATE - ] - system_info_events = [ - item for item in dataset_items_page.items if item['event_type'] == ActorEventTypes.SYSTEM_INFO - ] + persist_state_events = [item for item in dataset_items_page.items if item['event_type'] == 'persistState'] + system_info_events = [item for item in dataset_items_page.items if item['event_type'] == 'systemInfo'] assert len(persist_state_events) > 2 assert len(system_info_events) > 0 diff --git a/tests/unit/actor/test_actor_env_helpers.py b/tests/unit/actor/test_actor_env_helpers.py index 3b44f879..b539b5a2 100644 --- a/tests/unit/actor/test_actor_env_helpers.py +++ b/tests/unit/actor/test_actor_env_helpers.py @@ -9,16 +9,77 @@ from pydantic_core import TzInfo from apify import Actor -from apify._consts import ( - BOOL_ENV_VARS, - COMMA_SEPARATED_LIST_ENV_VARS, - DATETIME_ENV_VARS, - FLOAT_ENV_VARS, - INTEGER_ENV_VARS, - STRING_ENV_VARS, - ActorEnvVars, - ApifyEnvVars, -) +from apify._consts import ActorEnvVars, ApifyEnvVars + +INTEGER_ENV_VARS: list[ActorEnvVars | ApifyEnvVars] = [ + ActorEnvVars.MAX_PAID_DATASET_ITEMS, + ActorEnvVars.MEMORY_MBYTES, + ActorEnvVars.STANDBY_PORT, + ActorEnvVars.WEB_SERVER_PORT, + ApifyEnvVars.DEDICATED_CPUS, + ApifyEnvVars.LOG_LEVEL, + ApifyEnvVars.METAMORPH_AFTER_SLEEP_MILLIS, + ApifyEnvVars.PERSIST_STATE_INTERVAL_MILLIS, + ApifyEnvVars.PROXY_PORT, + ApifyEnvVars.SYSTEM_INFO_INTERVAL_MILLIS, +] + +FLOAT_ENV_VARS: list[ActorEnvVars | ApifyEnvVars] = [ + ActorEnvVars.MAX_TOTAL_CHARGE_USD, + ApifyEnvVars.MAX_USED_CPU_RATIO, +] + +BOOL_ENV_VARS: list[ApifyEnvVars] = [ + ApifyEnvVars.DISABLE_BROWSER_SANDBOX, + ApifyEnvVars.DISABLE_OUTDATED_WARNING, + ApifyEnvVars.HEADLESS, + ApifyEnvVars.IS_AT_HOME, + ApifyEnvVars.PERSIST_STORAGE, + ApifyEnvVars.PURGE_ON_START, + ApifyEnvVars.USER_IS_PAYING, +] + +DATETIME_ENV_VARS: list[ActorEnvVars] = [ + ActorEnvVars.STARTED_AT, + ActorEnvVars.TIMEOUT_AT, +] + +STRING_ENV_VARS: list[ActorEnvVars | ApifyEnvVars] = [ + ActorEnvVars.BUILD_ID, + ActorEnvVars.BUILD_NUMBER, + ActorEnvVars.DEFAULT_DATASET_ID, + ActorEnvVars.DEFAULT_KEY_VALUE_STORE_ID, + ActorEnvVars.DEFAULT_REQUEST_QUEUE_ID, + ActorEnvVars.EVENTS_WEBSOCKET_URL, + ActorEnvVars.FULL_NAME, + ActorEnvVars.ID, + ActorEnvVars.INPUT_KEY, + ActorEnvVars.PERMISSION_LEVEL, + ActorEnvVars.RUN_ID, + ActorEnvVars.STANDBY_URL, + ActorEnvVars.TASK_ID, + ActorEnvVars.WEB_SERVER_URL, + ApifyEnvVars.API_BASE_URL, + ApifyEnvVars.API_PUBLIC_BASE_URL, + ApifyEnvVars.DEFAULT_BROWSER_PATH, + ApifyEnvVars.FACT, + ApifyEnvVars.INPUT_SECRETS_PRIVATE_KEY_FILE, + ApifyEnvVars.INPUT_SECRETS_PRIVATE_KEY_PASSPHRASE, + ApifyEnvVars.LOCAL_STORAGE_DIR, + ApifyEnvVars.LOG_FORMAT, + ApifyEnvVars.META_ORIGIN, + ApifyEnvVars.PROXY_HOSTNAME, + ApifyEnvVars.PROXY_PASSWORD, + ApifyEnvVars.PROXY_STATUS_URL, + ApifyEnvVars.SDK_LATEST_VERSION, + ApifyEnvVars.TOKEN, + ApifyEnvVars.USER_ID, + ApifyEnvVars.WORKFLOW_KEY, +] + +COMMA_SEPARATED_LIST_ENV_VARS: list[ActorEnvVars] = [ + ActorEnvVars.BUILD_TAGS, +] if TYPE_CHECKING: from pathlib import Path From 05a8aba86e4d32cf7b01196de5c88354e2751a1f Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Thu, 16 Apr 2026 14:56:06 +0200 Subject: [PATCH 6/7] Replace SDK models with apify-client models: use Run instead of ActorRun, import pricing models from client Co-Authored-By: Claude Sonnet 4.6 --- src/apify/_actor.py | 22 +- src/apify/_charging.py | 21 +- src/apify/_configuration.py | 12 +- src/apify/_models.py | 304 +--------------------- tests/e2e/conftest.py | 9 +- tests/e2e/test_actor_api_helpers.py | 5 +- tests/e2e/test_actor_charge.py | 13 +- tests/e2e/test_actor_request_queue.py | 3 +- tests/e2e/test_crawlee/conftest.py | 5 +- tests/e2e/test_scrapy/conftest.py | 5 +- tests/unit/actor/test_actor_charge.py | 13 +- tests/unit/actor/test_charging_manager.py | 14 +- tests/unit/actor/test_configuration.py | 11 +- 13 files changed, 81 insertions(+), 356 deletions(-) diff --git a/src/apify/_actor.py b/src/apify/_actor.py index 56b42dcd..18ae961d 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -29,7 +29,6 @@ from apify._configuration import Configuration from apify._consts import EVENT_LISTENERS_TIMEOUT, ActorEnvVars, ActorExitCodes, ApifyEnvVars from apify._crypto import decrypt_input_secrets, load_private_key -from apify._models import ActorRun from apify._proxy_configuration import ProxyConfiguration from apify._utils import docs_group, docs_name, ensure_context, get_system_info, is_running_in_ipython from apify.events import ApifyEventManager, EventManager, LocalEventManager @@ -44,6 +43,7 @@ from types import TracebackType from typing import Self + from apify_client._models import Run from crawlee._types import JsonSerializable from crawlee.proxy_configuration import _NewUrlFunction @@ -869,7 +869,7 @@ async def start( timeout: timedelta | None | Literal['inherit', 'RemainingTime'] = None, wait_for_finish: int | None = None, webhooks: list[Webhook] | None = None, - ) -> ActorRun: + ) -> Run: """Run an Actor on the Apify platform. Unlike `Actor.call`, this method just starts the run without waiting for finish. @@ -935,7 +935,7 @@ async def start( if run is None: raise RuntimeError(f'Failed to start Actor with ID "{actor_id}".') - return ActorRun.from_client_actor_run(run) + return run @_ensure_context async def abort( @@ -945,7 +945,7 @@ async def abort( token: str | None = None, status_message: str | None = None, gracefully: bool | None = None, - ) -> ActorRun: + ) -> Run: """Abort given Actor run on the Apify platform using the current user account. The user account is determined by the `APIFY_TOKEN` environment variable. @@ -972,7 +972,7 @@ async def abort( if run is None: raise RuntimeError(f'Failed to abort Actor run with ID "{run_id}".') - return ActorRun.from_client_actor_run(run) + return run @_ensure_context async def call( @@ -988,7 +988,7 @@ async def call( webhooks: list[Webhook] | None = None, wait: timedelta | None = None, logger: logging.Logger | None | Literal['default'] = 'default', - ) -> ActorRun | None: + ) -> Run | None: """Start an Actor on the Apify Platform and wait for it to finish before returning. It waits indefinitely, unless the wait argument is provided. @@ -1059,7 +1059,7 @@ async def call( if run is None: raise RuntimeError(f'Failed to call Actor with ID "{actor_id}".') - return ActorRun.from_client_actor_run(run) + return run @_ensure_context async def call_task( @@ -1073,7 +1073,7 @@ async def call_task( webhooks: list[Webhook] | None = None, wait: timedelta | None = None, token: str | None = None, - ) -> ActorRun | None: + ) -> Run | None: """Start an Actor task on the Apify Platform and wait for it to finish before returning. It waits indefinitely, unless the wait argument is provided. @@ -1133,7 +1133,7 @@ async def call_task( if run is None: raise RuntimeError(f'Failed to call Task with ID "{task_id}".') - return ActorRun.from_client_actor_run(run) + return run @_ensure_context async def metamorph( @@ -1292,7 +1292,7 @@ async def set_status_message( status_message: str, *, is_terminal: bool | None = None, - ) -> ActorRun | None: + ) -> Run | None: """Set the status message for the current Actor run. Args: @@ -1322,7 +1322,7 @@ async def set_status_message( f'Failed to set status message for Actor run with ID "{self.configuration.actor_run_id}".' ) - return ActorRun.from_client_actor_run(run) + return run @_ensure_context async def create_proxy_configuration( diff --git a/src/apify/_charging.py b/src/apify/_charging.py index 539e15b1..1a3e31bc 100644 --- a/src/apify/_charging.py +++ b/src/apify/_charging.py @@ -7,16 +7,13 @@ from decimal import Decimal from typing import TYPE_CHECKING, Protocol, TypedDict -from pydantic import TypeAdapter - -from apify._models import ( - ActorRun, +from apify_client._models import ( FlatPricePerMonthActorPricingInfo, FreeActorPricingInfo, PayPerEventActorPricingInfo, PricePerDatasetItemActorPricingInfo, - PricingModel, ) + from apify._utils import ReentrantLock, docs_group, ensure_context from apify.log import logger from apify.storages import Dataset @@ -27,8 +24,7 @@ from apify_client import ApifyClientAsync from apify._configuration import Configuration - -run_validator = TypeAdapter[ActorRun | None](ActorRun | None) + from apify._models import PricingModel DEFAULT_DATASET_ITEM_EVENT = 'apify-default-dataset-item' @@ -426,15 +422,10 @@ async def _fetch_pricing_info(self) -> _FetchedPricingInfoDict: if run is None: raise RuntimeError('Actor run not found') - actor_run = ActorRun.from_client_actor_run(run) - - if actor_run is None: - raise RuntimeError('Actor run not found') - - max_charge = actor_run.options.max_total_charge_usd + max_charge = run.options.max_total_charge_usd return _FetchedPricingInfoDict( - pricing_info=actor_run.pricing_info, - charged_event_counts=actor_run.charged_event_counts or {}, + pricing_info=run.pricing_info, + charged_event_counts=run.charged_event_counts or {}, max_total_charge_usd=Decimal(str(max_charge)) if max_charge is not None else Decimal('inf'), ) diff --git a/src/apify/_configuration.py b/src/apify/_configuration.py index 97dc2d05..fe439def 100644 --- a/src/apify/_configuration.py +++ b/src/apify/_configuration.py @@ -10,17 +10,17 @@ from pydantic import AliasChoices, BeforeValidator, Field, model_validator from typing_extensions import TypedDict, deprecated -from crawlee import service_locator -from crawlee._utils.models import timedelta_ms -from crawlee._utils.urls import validate_http_url -from crawlee.configuration import Configuration as CrawleeConfiguration - -from apify._models import ( +from apify_client._models import ( FlatPricePerMonthActorPricingInfo, FreeActorPricingInfo, PayPerEventActorPricingInfo, PricePerDatasetItemActorPricingInfo, ) +from crawlee import service_locator +from crawlee._utils.models import timedelta_ms +from crawlee._utils.urls import validate_http_url +from crawlee.configuration import Configuration as CrawleeConfiguration + from apify._utils import docs_group logger = getLogger(__name__) diff --git a/src/apify/_models.py b/src/apify/_models.py index 38db1d6d..23ed3cdc 100644 --- a/src/apify/_models.py +++ b/src/apify/_models.py @@ -1,11 +1,14 @@ from __future__ import annotations -from datetime import datetime from typing import Annotated, Literal from pydantic import BaseModel, BeforeValidator, ConfigDict, Field -from apify_client._models import ActorJobStatus, Run +from apify_client._models import ( + ExampleWebhookDispatch, + WebhookCondition, + WebhookStats, +) from crawlee._utils.urls import validate_http_url from apify._utils import docs_group @@ -13,40 +16,6 @@ PricingModel = Literal['PAY_PER_EVENT', 'PRICE_PER_DATASET_ITEM', 'FLAT_PRICE_PER_MONTH', 'FREE'] """Pricing model for an Actor.""" -GeneralAccess = Literal['ANYONE_WITH_ID_CAN_READ', 'ANYONE_WITH_NAME_CAN_READ', 'FOLLOW_USER_SETTING', 'RESTRICTED'] -"""Defines the general access level for the resource.""" - - -class WebhookCondition(BaseModel): - """Condition for triggering a webhook.""" - - model_config = ConfigDict(populate_by_name=True, extra='allow') - - actor_id: Annotated[str | None, Field(alias='actorId')] = None - actor_task_id: Annotated[str | None, Field(alias='actorTaskId')] = None - actor_run_id: Annotated[str | None, Field(alias='actorRunId')] = None - - -WebhookDispatchStatus = Literal['ACTIVE', 'SUCCEEDED', 'FAILED'] -"""Status of a webhook dispatch.""" - - -class ExampleWebhookDispatch(BaseModel): - """Information about a webhook dispatch.""" - - model_config = ConfigDict(populate_by_name=True, extra='allow') - - status: WebhookDispatchStatus - finished_at: Annotated[datetime, Field(alias='finishedAt')] - - -class WebhookStats(BaseModel): - """Statistics about webhook dispatches.""" - - model_config = ConfigDict(populate_by_name=True, extra='allow') - - total_dispatches: Annotated[int, Field(alias='totalDispatches')] - @docs_group('Actor') class Webhook(BaseModel): @@ -62,8 +31,8 @@ class Webhook(BaseModel): BeforeValidator(validate_http_url), ] id: Annotated[str | None, Field(alias='id')] = None - created_at: Annotated[datetime | None, Field(alias='createdAt')] = None - modified_at: Annotated[datetime | None, Field(alias='modifiedAt')] = None + created_at: Annotated[str | None, Field(alias='createdAt')] = None + modified_at: Annotated[str | None, Field(alias='modifiedAt')] = None user_id: Annotated[str | None, Field(alias='userId')] = None is_ad_hoc: Annotated[bool | None, Field(alias='isAdHoc')] = None should_interpolate_strings: Annotated[bool | None, Field(alias='shouldInterpolateStrings')] = None @@ -78,262 +47,3 @@ class Webhook(BaseModel): description: Annotated[str | None, Field(alias='description')] = None last_dispatch: Annotated[ExampleWebhookDispatch | None, Field(alias='lastDispatch')] = None stats: Annotated[WebhookStats | None, Field(alias='stats')] = None - - -@docs_group('Actor') -class ActorRunMeta(BaseModel): - model_config = ConfigDict(populate_by_name=True, extra='allow') - - origin: Annotated[str, Field()] - client_ip: Annotated[str | None, Field(alias='clientIp')] = None - user_agent: Annotated[str | None, Field(alias='userAgent')] = None - schedule_id: Annotated[str | None, Field(alias='scheduleId')] = None - scheduled_at: Annotated[datetime | None, Field(alias='scheduledAt')] = None - - -@docs_group('Actor') -class ActorRunStats(BaseModel): - model_config = ConfigDict(populate_by_name=True, extra='allow') - - input_body_len: Annotated[int | None, Field(alias='inputBodyLen')] = None - migration_count: Annotated[int | None, Field(alias='migrationCount')] = None - reboot_count: Annotated[int | None, Field(alias='rebootCount')] = None - restart_count: Annotated[int, Field(alias='restartCount')] - resurrect_count: Annotated[int, Field(alias='resurrectCount')] - mem_avg_bytes: Annotated[float | None, Field(alias='memAvgBytes')] = None - mem_max_bytes: Annotated[int | None, Field(alias='memMaxBytes')] = None - mem_current_bytes: Annotated[int | None, Field(alias='memCurrentBytes')] = None - cpu_avg_usage: Annotated[float | None, Field(alias='cpuAvgUsage')] = None - cpu_max_usage: Annotated[float | None, Field(alias='cpuMaxUsage')] = None - cpu_current_usage: Annotated[float | None, Field(alias='cpuCurrentUsage')] = None - net_rx_bytes: Annotated[int | None, Field(alias='netRxBytes')] = None - net_tx_bytes: Annotated[int | None, Field(alias='netTxBytes')] = None - duration_millis: Annotated[int | None, Field(alias='durationMillis')] = None - run_time_secs: Annotated[float | None, Field(alias='runTimeSecs')] = None - metamorph: Annotated[int | None, Field(alias='metamorph')] = None - compute_units: Annotated[float, Field(alias='computeUnits')] - - -@docs_group('Actor') -class ActorRunOptions(BaseModel): - model_config = ConfigDict(populate_by_name=True, extra='allow') - - build: str - timeout_secs: Annotated[int, Field(alias='timeoutSecs')] - memory_mbytes: Annotated[int, Field(alias='memoryMbytes')] - disk_mbytes: Annotated[int, Field(alias='diskMbytes')] - max_items: Annotated[int | None, Field(alias='maxItems')] = None - max_total_charge_usd: Annotated[float | None, Field(alias='maxTotalChargeUsd')] = None - - -@docs_group('Actor') -class ActorRunUsage(BaseModel): - model_config = ConfigDict(populate_by_name=True, extra='allow') - - actor_compute_units: Annotated[float | None, Field(alias='ACTOR_COMPUTE_UNITS')] = None - dataset_reads: Annotated[int | None, Field(alias='DATASET_READS')] = None - dataset_writes: Annotated[int | None, Field(alias='DATASET_WRITES')] = None - key_value_store_reads: Annotated[int | None, Field(alias='KEY_VALUE_STORE_READS')] = None - key_value_store_writes: Annotated[int | None, Field(alias='KEY_VALUE_STORE_WRITES')] = None - key_value_store_lists: Annotated[int | None, Field(alias='KEY_VALUE_STORE_LISTS')] = None - request_queue_reads: Annotated[int | None, Field(alias='REQUEST_QUEUE_READS')] = None - request_queue_writes: Annotated[int | None, Field(alias='REQUEST_QUEUE_WRITES')] = None - data_transfer_internal_gbytes: Annotated[float | None, Field(alias='DATA_TRANSFER_INTERNAL_GBYTES')] = None - data_transfer_external_gbytes: Annotated[float | None, Field(alias='DATA_TRANSFER_EXTERNAL_GBYTES')] = None - proxy_residential_transfer_gbytes: Annotated[float | None, Field(alias='PROXY_RESIDENTIAL_TRANSFER_GBYTES')] = None - proxy_serps: Annotated[int | None, Field(alias='PROXY_SERPS')] = None - - -@docs_group('Actor') -class ActorRunUsageUsd(BaseModel): - """Resource usage costs in USD.""" - - model_config = ConfigDict(populate_by_name=True, extra='allow') - - actor_compute_units: Annotated[float | None, Field(alias='ACTOR_COMPUTE_UNITS')] = None - dataset_reads: Annotated[float | None, Field(alias='DATASET_READS')] = None - dataset_writes: Annotated[float | None, Field(alias='DATASET_WRITES')] = None - key_value_store_reads: Annotated[float | None, Field(alias='KEY_VALUE_STORE_READS')] = None - key_value_store_writes: Annotated[float | None, Field(alias='KEY_VALUE_STORE_WRITES')] = None - key_value_store_lists: Annotated[float | None, Field(alias='KEY_VALUE_STORE_LISTS')] = None - request_queue_reads: Annotated[float | None, Field(alias='REQUEST_QUEUE_READS')] = None - request_queue_writes: Annotated[float | None, Field(alias='REQUEST_QUEUE_WRITES')] = None - data_transfer_internal_gbytes: Annotated[float | None, Field(alias='DATA_TRANSFER_INTERNAL_GBYTES')] = None - data_transfer_external_gbytes: Annotated[float | None, Field(alias='DATA_TRANSFER_EXTERNAL_GBYTES')] = None - proxy_residential_transfer_gbytes: Annotated[float | None, Field(alias='PROXY_RESIDENTIAL_TRANSFER_GBYTES')] = None - proxy_serps: Annotated[float | None, Field(alias='PROXY_SERPS')] = None - - -class Metamorph(BaseModel): - """Information about a metamorph event that occurred during the run.""" - - model_config = ConfigDict(populate_by_name=True, extra='allow') - - created_at: Annotated[datetime, Field(alias='createdAt')] - actor_id: Annotated[str, Field(alias='actorId')] - build_id: Annotated[str, Field(alias='buildId')] - input_key: Annotated[str | None, Field(alias='inputKey')] = None - - -class CommonActorPricingInfo(BaseModel): - model_config = ConfigDict(populate_by_name=True, extra='allow') - - apify_margin_percentage: Annotated[float | None, Field(alias='apifyMarginPercentage')] = None - created_at: Annotated[datetime | None, Field(alias='createdAt')] = None - started_at: Annotated[datetime | None, Field(alias='startedAt')] = None - notified_about_future_change_at: Annotated[datetime | None, Field(alias='notifiedAboutFutureChangeAt')] = None - notified_about_change_at: Annotated[datetime | None, Field(alias='notifiedAboutChangeAt')] = None - reason_for_change: Annotated[str | None, Field(alias='reasonForChange')] = None - - -class FreeActorPricingInfo(CommonActorPricingInfo): - pricing_model: Annotated[Literal['FREE'], Field(alias='pricingModel')] - - -class FlatPricePerMonthActorPricingInfo(CommonActorPricingInfo): - pricing_model: Annotated[Literal['FLAT_PRICE_PER_MONTH'], Field(alias='pricingModel')] - trial_minutes: Annotated[int, Field(alias='trialMinutes')] - price_per_unit_usd: Annotated[float, Field(alias='pricePerUnitUsd')] - - -class PricePerDatasetItemActorPricingInfo(CommonActorPricingInfo): - pricing_model: Annotated[Literal['PRICE_PER_DATASET_ITEM'], Field(alias='pricingModel')] - unit_name: Annotated[str, Field(alias='unitName')] - price_per_unit_usd: Annotated[float, Field(alias='pricePerUnitUsd')] - - -class ActorChargeEvent(BaseModel): - model_config = ConfigDict(populate_by_name=True, extra='allow') - - event_price_usd: Annotated[float, Field(alias='eventPriceUsd')] - event_title: Annotated[str, Field(alias='eventTitle')] - event_description: Annotated[str | None, Field(alias='eventDescription')] = None - - -class PricingPerEvent(BaseModel): - model_config = ConfigDict(populate_by_name=True, extra='allow') - - actor_charge_events: Annotated[dict[str, ActorChargeEvent] | None, Field(alias='actorChargeEvents')] = None - - -class PayPerEventActorPricingInfo(CommonActorPricingInfo): - pricing_model: Annotated[Literal['PAY_PER_EVENT'], Field(alias='pricingModel')] - pricing_per_event: Annotated[PricingPerEvent, Field(alias='pricingPerEvent')] - minimal_max_total_charge_usd: Annotated[float | None, Field(alias='minimalMaxTotalChargeUsd')] = None - - -@docs_group('Actor') -class ActorRun(BaseModel): - """Represents an Actor run and its associated data.""" - - model_config = ConfigDict(populate_by_name=True, extra='allow') - - id: Annotated[str, Field(alias='id')] - """Unique identifier of the Actor run.""" - - act_id: Annotated[str, Field(alias='actId')] - """ID of the Actor that was run.""" - - user_id: Annotated[str, Field(alias='userId')] - """ID of the user who started the run.""" - - actor_task_id: Annotated[str | None, Field(alias='actorTaskId')] = None - """ID of the Actor task, if the run was started from a task.""" - - started_at: Annotated[datetime, Field(alias='startedAt')] - """Time when the Actor run started.""" - - finished_at: Annotated[datetime | None, Field(alias='finishedAt')] = None - """Time when the Actor run finished.""" - - status: Annotated[ActorJobStatus, Field(alias='status')] - """Current status of the Actor run.""" - - status_message: Annotated[str | None, Field(alias='statusMessage')] = None - """Detailed message about the run status.""" - - is_status_message_terminal: Annotated[bool | None, Field(alias='isStatusMessageTerminal')] = None - """Whether the status message is terminal (final).""" - - meta: Annotated[ActorRunMeta, Field(alias='meta')] - """Metadata about the Actor run.""" - - stats: Annotated[ActorRunStats, Field(alias='stats')] - """Statistics of the Actor run.""" - - options: Annotated[ActorRunOptions, Field(alias='options')] - """Configuration options for the Actor run.""" - - build_id: Annotated[str, Field(alias='buildId')] - """ID of the Actor build used for this run.""" - - exit_code: Annotated[int | None, Field(alias='exitCode')] = None - """Exit code of the Actor run process.""" - - general_access: Annotated[str | None, Field(alias='generalAccess')] = None - """General access level for the Actor run.""" - - default_key_value_store_id: Annotated[str, Field(alias='defaultKeyValueStoreId')] - """ID of the default key-value store for this run.""" - - default_dataset_id: Annotated[str, Field(alias='defaultDatasetId')] - """ID of the default dataset for this run.""" - - default_request_queue_id: Annotated[str, Field(alias='defaultRequestQueueId')] - """ID of the default request queue for this run.""" - - build_number: Annotated[str | None, Field(alias='buildNumber')] = None - """Build number of the Actor build used for this run.""" - - container_url: Annotated[str | None, Field(alias='containerUrl')] = None - """URL of the container running the Actor.""" - - is_container_server_ready: Annotated[bool | None, Field(alias='isContainerServerReady')] = None - """Whether the container's HTTP server is ready to accept requests.""" - - git_branch_name: Annotated[str | None, Field(alias='gitBranchName')] = None - """Name of the git branch used for the Actor build.""" - - usage: Annotated[ActorRunUsage | None, Field(alias='usage')] = None - """Resource usage statistics for the run.""" - - usage_total_usd: Annotated[float | None, Field(alias='usageTotalUsd')] = None - """Total cost of the run in USD.""" - - usage_usd: Annotated[ActorRunUsageUsd | None, Field(alias='usageUsd')] = None - """Resource usage costs in USD.""" - - pricing_info: Annotated[ - FreeActorPricingInfo - | FlatPricePerMonthActorPricingInfo - | PricePerDatasetItemActorPricingInfo - | PayPerEventActorPricingInfo - | None, - Field(alias='pricingInfo', discriminator='pricing_model'), - ] = None - """Pricing information for the Actor.""" - - charged_event_counts: Annotated[ - dict[str, int] | None, - Field(alias='chargedEventCounts'), - ] = None - """Count of charged events for pay-per-event pricing model.""" - - metamorphs: Annotated[list[Metamorph] | None, Field(alias='metamorphs')] = None - """List of metamorph events that occurred during the run.""" - - @classmethod - def from_client_actor_run(cls, client_actor_run: Run) -> ActorRun: - """Create an `ActorRun` from an Apify API client's `Run` model. - - Args: - client_actor_run: `Run` instance from Apify API client. - - Returns: - `ActorRun` instance with properly converted types. - """ - # Dump to dict first with mode='json' to serialize special types - client_actor_run_dict = client_actor_run.model_dump(by_alias=True, mode='json') - - # Validate and construct ActorRun from the serialized dict - return cls.model_validate(client_actor_run_dict) diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index d6c97830..a647c219 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -14,13 +14,12 @@ from filelock import FileLock from apify_client import ApifyClient, ApifyClientAsync -from apify_client._models import ActorPermissionLevel, VersionSourceType +from apify_client._models import ActorPermissionLevel, Run, VersionSourceType from crawlee import service_locator import apify._actor from ._utils import generate_unique_resource_name from apify._consts import ApifyEnvVars -from apify._models import ActorRun from apify.storage_clients._apify._alias_resolving import AliasResolver if TYPE_CHECKING: @@ -368,7 +367,7 @@ def __call__( run_input: Any = None, max_total_charge_usd: Decimal | None = None, force_permission_level: ActorPermissionLevel | None = None, - ) -> Coroutine[None, None, ActorRun]: + ) -> Coroutine[None, None, Run]: """Initiate an Actor run and wait for its completion. Args: @@ -395,7 +394,7 @@ async def _run_actor( run_input: Any = None, max_total_charge_usd: Decimal | None = None, force_permission_level: ActorPermissionLevel | None = None, - ) -> ActorRun: + ) -> Run: call_result = await actor.call( run_input=run_input, max_total_charge_usd=max_total_charge_usd, @@ -409,6 +408,6 @@ async def _run_actor( assert client_actor_run is not None, 'Actor run did not finish successfully within the expected time.' - return ActorRun.from_client_actor_run(client_actor_run) + return client_actor_run return _run_actor diff --git a/tests/e2e/test_actor_api_helpers.py b/tests/e2e/test_actor_api_helpers.py index 3b494c0e..fba39e8e 100644 --- a/tests/e2e/test_actor_api_helpers.py +++ b/tests/e2e/test_actor_api_helpers.py @@ -10,7 +10,6 @@ from ._utils import generate_unique_resource_name from apify import Actor -from apify._models import ActorRun if TYPE_CHECKING: from apify_client import ApifyClientAsync @@ -301,7 +300,7 @@ async def main_outer() -> None: if inner_actor_run is None: raise AssertionError('Failed to get inner actor run after aborting it.') - inner_actor_last_run = ActorRun.from_client_actor_run(inner_actor_run) + inner_actor_last_run = inner_actor_run assert inner_actor_last_run.status.value == 'ABORTED' @@ -483,7 +482,7 @@ async def main_client() -> None: if sa_run_client_run is None: raise AssertionError('Failed to get server actor run after waiting for finish.') - sa_run_result = ActorRun.from_client_actor_run(sa_run_client_run) + sa_run_result = sa_run_client_run assert sa_run_result.status.value == 'SUCCEEDED' diff --git a/tests/e2e/test_actor_charge.py b/tests/e2e/test_actor_charge.py index 22457526..43ecf14e 100644 --- a/tests/e2e/test_actor_charge.py +++ b/tests/e2e/test_actor_charge.py @@ -6,8 +6,9 @@ import pytest_asyncio +from apify_client._models import ActorJobStatus + from apify import Actor -from apify._models import ActorJobStatus, ActorRun if TYPE_CHECKING: from collections.abc import Iterable @@ -132,7 +133,7 @@ async def test_actor_charge_basic( updated_run = await run_client.get() assert updated_run is not None, 'Updated run should not be None' - run = ActorRun.from_client_actor_run(updated_run) + run = updated_run try: assert run.status.value == 'SUCCEEDED' @@ -158,7 +159,7 @@ async def test_actor_charge_limit( updated_run = await run_client.get() assert updated_run is not None, 'Updated run should not be None' - run_result = ActorRun.from_client_actor_run(updated_run) + run_result = updated_run try: assert run_result.status.value == 'SUCCEEDED' @@ -181,7 +182,8 @@ async def test_actor_push_data_charges_both_events( for is_last_attempt, _ in retry_counter(30): await asyncio.sleep(1) updated_run = await apify_client_async.run(run.id).get() - run = ActorRun.model_validate(updated_run) + assert updated_run is not None + run = updated_run try: assert run.status == ActorJobStatus.SUCCEEDED @@ -210,7 +212,8 @@ async def test_actor_push_data_combined_budget_limit( for is_last_attempt, _ in retry_counter(30): await asyncio.sleep(1) updated_run = await apify_client_async.run(run.id).get() - run = ActorRun.model_validate(updated_run) + assert updated_run is not None + run = updated_run try: assert run.status == ActorJobStatus.SUCCEEDED diff --git a/tests/e2e/test_actor_request_queue.py b/tests/e2e/test_actor_request_queue.py index 45efcae4..5bdb4647 100644 --- a/tests/e2e/test_actor_request_queue.py +++ b/tests/e2e/test_actor_request_queue.py @@ -4,7 +4,6 @@ from typing import TYPE_CHECKING from apify import Actor -from apify._models import ActorRun if TYPE_CHECKING: from apify_client import ApifyClientAsync @@ -43,7 +42,7 @@ async def main() -> None: raw_run_result = await run_client.wait_for_finish(wait_duration=timedelta(seconds=600)) assert raw_run_result is not None - run_result = ActorRun.from_client_actor_run(raw_run_result) + run_result = raw_run_result assert run_result.status == 'SUCCEEDED' diff --git a/tests/e2e/test_crawlee/conftest.py b/tests/e2e/test_crawlee/conftest.py index f074e3ae..0f2344de 100644 --- a/tests/e2e/test_crawlee/conftest.py +++ b/tests/e2e/test_crawlee/conftest.py @@ -5,10 +5,9 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: + from apify_client._models import Run from apify_client._resource_clients import ActorClientAsync - from apify._models import ActorRun - _PYTHON_VERSION = f'{sys.version_info[0]}.{sys.version_info[1]}' _ACTOR_SOURCE_DIR = Path(__file__).parent / 'actor_source' @@ -34,7 +33,7 @@ def get_playwright_dockerfile() -> str: async def verify_crawler_results( actor: ActorClientAsync, - run_result: ActorRun, + run_result: Run, expected_crawler_type: str, ) -> None: """Verify dataset items and KVS record after a crawler Actor run.""" diff --git a/tests/e2e/test_scrapy/conftest.py b/tests/e2e/test_scrapy/conftest.py index e58b8d55..93a09d79 100644 --- a/tests/e2e/test_scrapy/conftest.py +++ b/tests/e2e/test_scrapy/conftest.py @@ -4,10 +4,9 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: + from apify_client._models import Run from apify_client._resource_clients import ActorClientAsync - from apify._models import ActorRun - _ACTOR_SOURCE_DIR = Path(__file__).parent / 'actor_source' @@ -43,7 +42,7 @@ def get_scrapy_source_files( async def verify_spider_results( actor: ActorClientAsync, - run_result: ActorRun, + run_result: Run, *, expected_products: dict[str, dict[str, str]] | None = None, ) -> None: diff --git a/tests/unit/actor/test_actor_charge.py b/tests/unit/actor/test_actor_charge.py index 4e452e78..c632781a 100644 --- a/tests/unit/actor/test_actor_charge.py +++ b/tests/unit/actor/test_actor_charge.py @@ -5,9 +5,10 @@ from typing import NamedTuple from unittest.mock import AsyncMock, Mock, patch +from apify_client._models import PayPerEventActorPricingInfo + from apify import Actor, Configuration from apify._charging import ChargingManagerImplementation, PricingInfoItem -from apify._models import PayPerEventActorPricingInfo class MockedChargingSetup(NamedTuple): @@ -124,15 +125,20 @@ async def test_max_event_charge_count_within_limit_tolerates_overdraw() -> None: actor_pricing_info=PayPerEventActorPricingInfo.model_validate( { 'pricingModel': 'PAY_PER_EVENT', + 'apifyMarginPercentage': 0.0, + 'createdAt': '2024-01-01T00:00:00.000Z', + 'startedAt': '2024-01-01T00:00:00.000Z', 'pricingPerEvent': { 'actorChargeEvents': { 'event': { 'eventPriceUsd': 0.0003, 'eventTitle': 'Event', + 'eventDescription': 'Event description', }, 'apify-actor-start': { 'eventPriceUsd': 0.00005, 'eventTitle': 'Actor start', + 'eventDescription': 'Actor start description', }, } }, @@ -235,15 +241,20 @@ async def test_charge_with_overdrawn_budget() -> None: actor_pricing_info=PayPerEventActorPricingInfo.model_validate( { 'pricingModel': 'PAY_PER_EVENT', + 'apifyMarginPercentage': 0.0, + 'createdAt': '2024-01-01T00:00:00.000Z', + 'startedAt': '2024-01-01T00:00:00.000Z', 'pricingPerEvent': { 'actorChargeEvents': { 'event': { 'eventPriceUsd': 0.0003, 'eventTitle': 'Event', + 'eventDescription': 'Event description', }, 'apify-actor-start': { 'eventPriceUsd': 0.00005, 'eventTitle': 'Actor start', + 'eventDescription': 'Actor start description', }, } }, diff --git a/tests/unit/actor/test_charging_manager.py b/tests/unit/actor/test_charging_manager.py index 10a8474b..74cbc920 100644 --- a/tests/unit/actor/test_charging_manager.py +++ b/tests/unit/actor/test_charging_manager.py @@ -6,13 +6,14 @@ import pytest -from apify._charging import ChargingManagerImplementation -from apify._configuration import Configuration -from apify._models import ( +from apify_client._models import ( ActorChargeEvent, PayPerEventActorPricingInfo, ) +from apify._charging import ChargingManagerImplementation +from apify._configuration import Configuration + def _make_config(**kwargs: Any) -> Configuration: """Helper to create a Configuration with sensible defaults for charging tests. @@ -49,12 +50,17 @@ def _make_ppe_pricing_info(events: dict[str, Decimal] | None = None) -> PayPerEv if events is None: events = {'search': Decimal('0.01'), 'scrape': Decimal('0.05')} charge_events = { - name: ActorChargeEvent.model_validate({'eventPriceUsd': price, 'eventTitle': f'{name} event'}) + name: ActorChargeEvent.model_validate( + {'eventPriceUsd': price, 'eventTitle': f'{name} event', 'eventDescription': f'{name} event description'} + ) for name, price in events.items() } return PayPerEventActorPricingInfo.model_validate( { 'pricingModel': 'PAY_PER_EVENT', + 'apifyMarginPercentage': 0.0, + 'createdAt': '2024-01-01T00:00:00.000Z', + 'startedAt': '2024-01-01T00:00:00.000Z', 'pricingPerEvent': { 'actorChargeEvents': {name: event.model_dump(by_alias=True) for name, event in charge_events.items()} }, diff --git a/tests/unit/actor/test_configuration.py b/tests/unit/actor/test_configuration.py index 1448ecca..91fb4eca 100644 --- a/tests/unit/actor/test_configuration.py +++ b/tests/unit/actor/test_configuration.py @@ -320,8 +320,17 @@ def test_actor_pricing_info_from_json_env_var(monkeypatch: pytest.MonkeyPatch) - pricing_json = json.dumps( { 'pricingModel': 'PAY_PER_EVENT', + 'apifyMarginPercentage': 0.0, + 'createdAt': '2024-01-01T00:00:00.000Z', + 'startedAt': '2024-01-01T00:00:00.000Z', 'pricingPerEvent': { - 'actorChargeEvents': {'search': {'eventPriceUsd': '0.01', 'eventTitle': 'Search event'}} + 'actorChargeEvents': { + 'search': { + 'eventPriceUsd': '0.01', + 'eventTitle': 'Search event', + 'eventDescription': 'Search event description', + } + } }, } ) From 5862f42959a84c37714bcbb218fba77989890267 Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Thu, 16 Apr 2026 15:38:51 +0200 Subject: [PATCH 7/7] fix: update e2e tests for apify-client v3 model validation The v3 client now validates pricing_infos dicts through pydantic models before sending to the API. Add required CommonActorPricingInfo fields (apifyMarginPercentage, createdAt, startedAt) to all pricing info dicts in test fixtures and cleanup code. Also fix UTC import missing inside serialized Actor main functions for timeout tests. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/e2e/conftest.py | 10 +++++++++- tests/e2e/test_actor_call_timeouts.py | 5 ++--- tests/e2e/test_actor_charge.py | 6 ++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index a647c219..2de844a9 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -351,7 +351,15 @@ async def _make_actor( if actor is not None and actor.pricing_infos is not None: # Convert Pydantic models to dicts before mixing with plain dict existing_pricing_infos = [pi.model_dump(by_alias=True, exclude_none=True) for pi in actor.pricing_infos] - new_pricing_infos = [*existing_pricing_infos, {'pricingModel': 'FREE'}] + new_pricing_infos = [ + *existing_pricing_infos, + { + 'pricingModel': 'FREE', + 'apifyMarginPercentage': 0.0, + 'createdAt': '2024-01-01T00:00:00.000Z', + 'startedAt': '2024-01-01T00:00:00.000Z', + }, + ] actor_client.update(pricing_infos=new_pricing_infos) actor_client.delete() diff --git a/tests/e2e/test_actor_call_timeouts.py b/tests/e2e/test_actor_call_timeouts.py index ddada096..190090ae 100644 --- a/tests/e2e/test_actor_call_timeouts.py +++ b/tests/e2e/test_actor_call_timeouts.py @@ -1,7 +1,6 @@ from __future__ import annotations import asyncio -from datetime import UTC from typing import TYPE_CHECKING from apify import Actor @@ -20,7 +19,7 @@ async def test_actor_start_inherit_timeout( Timeout should be the remaining time of the first Actor run calculated at the moment of the other Actor start.""" async def main() -> None: - from datetime import datetime, timedelta + from datetime import UTC, datetime, timedelta async with Actor: actor_input = (await Actor.get_input()) or {} @@ -70,7 +69,7 @@ async def test_actor_call_inherit_timeout( Timeout should be the remaining time of the first Actor run calculated at the moment of the other Actor call.""" async def main() -> None: - from datetime import datetime, timedelta + from datetime import UTC, datetime, timedelta async with Actor: actor_input = (await Actor.get_input()) or {} diff --git a/tests/e2e/test_actor_charge.py b/tests/e2e/test_actor_charge.py index 43ecf14e..9b0ecea1 100644 --- a/tests/e2e/test_actor_charge.py +++ b/tests/e2e/test_actor_charge.py @@ -34,6 +34,9 @@ async def main() -> None: pricing_infos=[ { 'pricingModel': 'PAY_PER_EVENT', + 'apifyMarginPercentage': 0.0, + 'createdAt': '2024-01-01T00:00:00.000Z', + 'startedAt': '2024-01-01T00:00:00.000Z', 'pricingPerEvent': { 'actorChargeEvents': { 'push-item': { @@ -84,6 +87,9 @@ async def main() -> None: pricing_infos=[ { 'pricingModel': 'PAY_PER_EVENT', + 'apifyMarginPercentage': 0.0, + 'createdAt': '2024-01-01T00:00:00.000Z', + 'startedAt': '2024-01-01T00:00:00.000Z', 'pricingPerEvent': { 'actorChargeEvents': { 'foobar': {