diff --git a/.github/workflows/_checks.yaml b/.github/workflows/_checks.yaml index d9b05457..bfeb08f7 100644 --- a/.github/workflows/_checks.yaml +++ b/.github/workflows/_checks.yaml @@ -45,13 +45,13 @@ 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"]' unit_tests: name: Unit tests @@ -59,7 +59,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 @@ -80,7 +80,7 @@ jobs: strategy: matrix: os: ["ubuntu-latest"] - python-version: ["3.10", "3.14"] + python-version: ["3.11", "3.14"] runs-on: ${{ matrix.os }} @@ -140,7 +140,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/docs/02_concepts/code/07_webhook.py b/docs/02_concepts/code/07_webhook.py index 3dd48b13..0fd15abe 100644 --- a/docs/02_concepts/code/07_webhook.py +++ b/docs/02_concepts/code/07_webhook.py @@ -1,13 +1,13 @@ import asyncio -from apify import Actor, Webhook, WebhookEventType +from apify import Actor, Webhook 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..0b05fa5b 100644 --- a/docs/02_concepts/code/07_webhook_preventing.py +++ b/docs/02_concepts/code/07_webhook_preventing.py @@ -1,18 +1,19 @@ import asyncio -from apify import Actor, Webhook, WebhookEventType +from apify import Actor, Webhook async def main() -> None: async with Actor: - # Create a webhook that will be triggered when the Actor run fails. + # Create a webhook with an idempotency key to prevent duplicates on retries. webhook = Webhook( - event_types=[WebhookEventType.ACTOR_RUN_FAILED], + event_types=['ACTOR.RUN.FAILED'], request_url='https://example.com/run-failed', + idempotency_key=Actor.configuration.actor_run_id, ) # Add the webhook to the Actor. - await Actor.add_webhook(webhook, idempotency_key=Actor.configuration.actor_run_id) + await Actor.add_webhook(webhook) # Raise an error to simulate a failed run. raise RuntimeError('I am an error and I know it!') diff --git a/docs/04_upgrading/upgrading_to_v4.md b/docs/04_upgrading/upgrading_to_v4.md new file mode 100644 index 00000000..9d47ec9a --- /dev/null +++ b/docs/04_upgrading/upgrading_to_v4.md @@ -0,0 +1,81 @@ +--- +id: upgrading-to-v4 +title: Upgrading to v4 +description: Breaking changes and migration guide from Apify SDK v3.x to v4.0. +--- + +This page summarizes the breaking changes between Apify Python SDK v3.x and v4.0. + +## Python 3.11+ required + +Support for Python 3.10 has been dropped. Apify Python SDK v4.x requires Python 3.11 or later. + +## `apify-client` v3 required + +The SDK is now built on `apify-client` v3 and no longer depends on `apify-shared`. Three changes are user-visible: + +- `Actor.start`, `Actor.call`, `Actor.call_task`, and `Actor.metamorph` return `apify_client._models.Run` instead of the SDK-side `ActorRun`. The shape is equivalent — only the import path changes. +- `apify.WebhookEventType` is now a `Literal[...]` instead of a `StrEnum`. Use plain string values (`'ACTOR.RUN.FAILED'`) instead of enum members. +- `apify_shared.consts.ActorEventTypes` (a `StrEnum`) is replaced by `apify.ActorEventTypes`, now a `Literal['systemInfo', 'persistState', 'migrating', 'aborting']`. For runtime values, use `apify.Event` (re-exported from Crawlee) instead of enum members. + +**Before (v3.x):** + +```python +from apify import Actor +from apify_shared.consts import ActorEventTypes + +Actor.on(ActorEventTypes.SYSTEM_INFO, callback) +``` + +**Now (v4.0):** + +```python +from apify import Actor, Event + +Actor.on(Event.SYSTEM_INFO, callback) +``` + +## `Webhook` API simplified + +The `Webhook` model has been slimmed down to only the fields a user sets when defining a webhook. Server-populated response fields (`id`, `created_at`, `modified_at`, `user_id`, `is_ad_hoc`, `condition`, `last_dispatch`, `stats`) and the unused `WebhookCondition` helper class have been removed. `Webhook` is now a plain `@dataclass` instead of a Pydantic `BaseModel` — construct it with snake_case kwargs; `.model_dump()` / `.model_validate()` are gone. + +The retry and idempotency kwargs that used to live on `Actor.add_webhook` have moved onto the `Webhook` instance itself. + +**Before (v3.x):** + +```python +from apify import Actor, Webhook + +await Actor.add_webhook( + Webhook(event_types=['ACTOR.RUN.FAILED'], request_url='https://example.com'), + ignore_ssl_errors=False, + do_not_retry=False, + idempotency_key='my-key', +) +``` + +**Now (v4.0):** + +```python +from apify import Actor, Webhook + +await Actor.add_webhook( + Webhook( + event_types=['ACTOR.RUN.FAILED'], + request_url='https://example.com', + ignore_ssl_errors=False, + do_not_retry=False, + idempotency_key='my-key', + ) +) +``` + +The `idempotency_key` kwarg form on `Actor.add_webhook` still works for one more release but emits a `DeprecationWarning` and will be removed in v5.0. The `ignore_ssl_errors` and `do_not_retry` kwargs have been removed outright — set them on the `Webhook` instance. + +`apify.WebhookCondition` is no longer exported; the SDK now binds the webhook to the current Actor run internally. + +The `webhooks` argument on `Actor.start`, `Actor.call`, and `Actor.call_task` still accepts `list[Webhook]` and the fields used at the call site (`event_types`, `request_url`, `payload_template`, `headers_template`) are unchanged. + +## `Actor.new_client` — `timeout` scales all tiers + +`apify-client` v3 split its single timeout into four tiers (short / medium / long / max). `Actor.new_client(timeout=...)` still takes a single `timedelta`; the SDK uses it as the medium-tier baseline and scales the other tiers proportionally (short = `timeout / 6`, long = `timeout * 12`, max = `timeout * 24`). The public signature is unchanged — no migration needed. diff --git a/pyproject.toml b/pyproject.toml index 5b90c41b..29230918 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,14 +9,13 @@ 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", "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", @@ -35,15 +34,14 @@ keywords = [ "scraping", ] dependencies = [ - "apify-client>=2.3.0,<3.0.0", - "apify-shared>=2.0.0,<3.0.0", + "apify-client>=3.0.0,<4.0.0", "crawlee>=1.0.4,<2.0.0", "cachetools>=5.5.0", "cryptography>=42.0.0", "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", @@ -198,7 +196,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 @@ -210,7 +208,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/__init__.py b/src/apify/__init__.py index f6495d55..4984e565 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._literals import WebhookEventType from crawlee import Request from crawlee.events import ( Event, @@ -14,13 +14,15 @@ from apify._actor import Actor from apify._configuration import Configuration -from apify._models import Webhook from apify._proxy_configuration import ProxyConfiguration, ProxyInfo +from apify._webhook import Webhook +from apify.events._types import ActorEventTypes __version__ = metadata.version('apify') __all__ = [ 'Actor', + 'ActorEventTypes', 'Configuration', 'Event', 'EventAbortingData', diff --git a/src/apify/_actor.py b/src/apify/_actor.py index d0ca4979..61366bd3 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 @@ -13,7 +13,14 @@ from pydantic import AliasChoices from apify_client import ApifyClientAsync -from apify_shared.consts import ActorEnvVars, ActorExitCodes, ApifyEnvVars +from apify_client._consts import ( + DEFAULT_MAX_RETRIES, + DEFAULT_MIN_DELAY_BETWEEN_RETRIES, + DEFAULT_TIMEOUT_LONG, + DEFAULT_TIMEOUT_MAX, + DEFAULT_TIMEOUT_MEDIUM, + DEFAULT_TIMEOUT_SHORT, +) from crawlee import service_locator from crawlee.errors import ServiceConflictError from crawlee.events import ( @@ -28,11 +35,11 @@ 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, EXIT_CODE_ERROR_USER_FUNCTION_THREW, ActorEnvVars, 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._webhook import to_client_representations from apify.events import ApifyEventManager, EventManager, LocalEventManager from apify.log import _configure_logging, logger from apify.storage_clients import ApifyStorageClient, SmartApifyStorageClient @@ -44,14 +51,14 @@ from collections.abc import Callable, MutableMapping from decimal import Decimal from types import TracebackType + from typing import Self - from typing_extensions import Self - - from apify_shared.consts import ActorPermissionLevel + from apify_client._literals import ActorPermissionLevel + from apify_client._models import Run from crawlee._types import JsonSerializable from crawlee.proxy_configuration import _NewUrlFunction - from apify._models import Webhook + from apify._webhook import Webhook MainReturnType = TypeVar('MainReturnType') @@ -236,7 +243,7 @@ async def __aexit__( # In IPython, we don't run `sys.exit()` during Actor exits, # so the exception traceback will be printed on its own self.log.exception('Actor failed with an exception', exc_info=exc_value) - self.exit_code = ActorExitCodes.ERROR_USER_FUNCTION_THREW.value + self.exit_code = EXIT_CODE_ERROR_USER_FUNCTION_THREW self._is_exiting = True self.log.info('Exiting Actor', extra={'exit_code': self.exit_code}) @@ -506,18 +513,22 @@ def new_client( max_retries: How many times to retry a failed request at most. min_delay_between_retries: How long will the client wait between retrying requests (increases exponentially from this value). - timeout: The socket timeout of the HTTP requests sent to the Apify API. + timeout: Baseline HTTP timeout for medium-duration API operations. The underlying client uses + separate timeout tiers for short/medium/long/max-duration calls; passing a value here scales + all four tiers proportionally (short = `timeout / 6`, long = `timeout * 12`, + max = `timeout * 24`). """ - 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) + token=token or self.configuration.token, + api_url=api_url or self.configuration.api_base_url, + max_retries=max_retries if max_retries is not None else DEFAULT_MAX_RETRIES, + min_delay_between_retries=min_delay_between_retries if min_delay_between_retries is not None - else None, - timeout_secs=int(timeout.total_seconds()) if timeout else None, + else DEFAULT_MIN_DELAY_BETWEEN_RETRIES, + timeout_short=timeout / 6 if timeout is not None else DEFAULT_TIMEOUT_SHORT, + timeout_medium=timeout if timeout is not None else DEFAULT_TIMEOUT_MEDIUM, + timeout_long=timeout * 12 if timeout is not None else DEFAULT_TIMEOUT_LONG, + timeout_max=timeout * 24 if timeout is not None else DEFAULT_TIMEOUT_MAX, ) @_ensure_context @@ -872,7 +883,7 @@ async def start( force_permission_level: ActorPermissionLevel | None = 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. @@ -905,13 +916,6 @@ async def start( """ client = self.new_client(token=token) if token else self.apify_client - if webhooks: - serialized_webhooks = [ - hook.model_dump(by_alias=True, exclude_unset=True, exclude_defaults=True) for hook in webhooks - ] - else: - serialized_webhooks = None - if timeout in {'inherit', 'RemainingTime'}: if timeout == 'RemainingTime': warnings.warn( @@ -929,20 +933,24 @@ 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, max_total_charge_usd=max_total_charge_usd, restart_on_error=restart_on_error, memory_mbytes=memory_mbytes, - timeout_secs=int(actor_start_timeout.total_seconds()) if actor_start_timeout is not None else None, + run_timeout=actor_start_timeout, force_permission_level=force_permission_level, wait_for_finish=wait_for_finish, - webhooks=serialized_webhooks, + webhooks=to_client_representations(webhooks), ) - return ActorRun.model_validate(api_result) + if run is None: + raise RuntimeError(f'Failed to start Actor with ID "{actor_id}".') + + return run @_ensure_context async def abort( @@ -952,7 +960,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. @@ -969,13 +977,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) + + run = await run_client.abort(gracefully=gracefully) - api_result = await client.run(run_id).abort(gracefully=gracefully) + if run is None: + raise RuntimeError(f'Failed to abort Actor run with ID "{run_id}".') - return ActorRun.model_validate(api_result) + return run @_ensure_context async def call( @@ -994,7 +1006,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. @@ -1030,13 +1042,6 @@ async def call( """ client = self.new_client(token=token) if token else self.apify_client - if webhooks: - serialized_webhooks = [ - hook.model_dump(by_alias=True, exclude_unset=True, exclude_defaults=True) for hook in webhooks - ] - else: - serialized_webhooks = None - if timeout in {'inherit', 'RemainingTime'}: if timeout == 'RemainingTime': warnings.warn( @@ -1055,21 +1060,25 @@ 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, max_total_charge_usd=max_total_charge_usd, restart_on_error=restart_on_error, memory_mbytes=memory_mbytes, - timeout_secs=int(actor_call_timeout.total_seconds()) if actor_call_timeout is not None else None, + run_timeout=actor_call_timeout, force_permission_level=force_permission_level, - webhooks=serialized_webhooks, - wait_secs=int(wait.total_seconds()) if wait is not None else None, + webhooks=to_client_representations(webhooks), + 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 run @_ensure_context async def call_task( @@ -1084,7 +1093,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. @@ -1117,13 +1126,6 @@ async def call_task( """ client = self.new_client(token=token) if token else self.apify_client - if webhooks: - serialized_webhooks = [ - hook.model_dump(by_alias=True, exclude_unset=True, exclude_defaults=True) for hook in webhooks - ] - else: - serialized_webhooks = None - if timeout == 'inherit': task_call_timeout = self._get_remaining_time() elif timeout is None: @@ -1133,17 +1135,21 @@ 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, restart_on_error=restart_on_error, memory_mbytes=memory_mbytes, - timeout_secs=int(task_call_timeout.total_seconds()) if task_call_timeout is not None else None, - webhooks=serialized_webhooks, - wait_secs=int(wait.total_seconds()) if wait is not None else None, + run_timeout=task_call_timeout, + webhooks=to_client_representations(webhooks), + 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 run @_ensure_context async def metamorph( @@ -1241,7 +1247,7 @@ async def reboot( ), timeout=event_listeners_timeout.total_seconds() if event_listeners_timeout else None, ) - except asyncio.TimeoutError: + except TimeoutError: self.log.warning('Pre-reboot event listeners did not finish within timeout; proceeding with reboot') results = [] @@ -1258,14 +1264,7 @@ async def reboot( await asyncio.sleep(custom_after_sleep.total_seconds()) @_ensure_context - async def add_webhook( - self, - webhook: Webhook, - *, - ignore_ssl_errors: bool | None = None, - do_not_retry: bool | None = None, - idempotency_key: str | None = None, - ) -> None: + async def add_webhook(self, webhook: Webhook, *, idempotency_key: str | None = None) -> None: """Create an ad-hoc webhook for the current Actor run. This webhook lets you receive a notification when the Actor run finished or failed. @@ -1276,15 +1275,21 @@ async def add_webhook( For more information about Apify Actor webhooks, please see the [documentation](https://docs.apify.com/webhooks). Args: - webhook: The webhook to be added - ignore_ssl_errors: Whether the webhook should ignore SSL errors returned by request_url - do_not_retry: Whether the webhook should retry sending the payload to request_url upon failure. - idempotency_key: A unique identifier of a webhook. You can use it to ensure that you won't create - the same webhook multiple times. + webhook: The webhook to be added. It is automatically bound to the current Actor run. + idempotency_key: Deprecated. Pass `idempotency_key` on the `Webhook` instance instead. + Will be removed in version 5.0.0. Returns: The created webhook. """ + if idempotency_key is not None: + warnings.warn( + 'Passing `idempotency_key` to `Actor.add_webhook()` is deprecated and will be removed in version ' + '5.0.0. Set it on the `Webhook` instance instead.', + DeprecationWarning, + stacklevel=2, + ) + if not self.is_at_home(): self.log.error('Actor.add_webhook() is only supported when running on the Apify platform.') return @@ -1298,9 +1303,11 @@ async def add_webhook( event_types=webhook.event_types, request_url=webhook.request_url, payload_template=webhook.payload_template, - ignore_ssl_errors=ignore_ssl_errors, - do_not_retry=do_not_retry, - idempotency_key=idempotency_key, + headers_template=webhook.headers_template, + ignore_ssl_errors=webhook.ignore_ssl_errors, + do_not_retry=webhook.do_not_retry, + idempotency_key=idempotency_key if idempotency_key is not None else webhook.idempotency_key, + is_ad_hoc=True, ) @_ensure_context @@ -1309,7 +1316,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: @@ -1328,11 +1335,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 run @_ensure_context async def create_proxy_configuration( @@ -1442,7 +1456,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 0bf43626..b5a78c73 100644 --- a/src/apify/_charging.py +++ b/src/apify/_charging.py @@ -3,20 +3,25 @@ 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 +from typing import TYPE_CHECKING, Annotated, Literal, Protocol, TypedDict -from pydantic import TypeAdapter +from pydantic import BaseModel, ConfigDict, Field -from apify._models import ( - ActorRun, - FlatPricePerMonthActorPricingInfo, - FreeActorPricingInfo, - PayPerEventActorPricingInfo, - PricePerDatasetItemActorPricingInfo, - PricingModel, +from apify_client._models import ( + FlatPricePerMonthActorPricingInfo as _ClientFlatPricePerMonth, ) +from apify_client._models import ( + FreeActorPricingInfo as _ClientFree, +) +from apify_client._models import ( + PayPerEventActorPricingInfo as _ClientPayPerEvent, +) +from apify_client._models import ( + PricePerDatasetItemActorPricingInfo as _ClientPricePerDatasetItem, +) + from apify._utils import ReentrantLock, docs_group, ensure_context from apify.log import logger from apify.storages import Dataset @@ -28,7 +33,8 @@ from apify._configuration import Configuration -run_validator = TypeAdapter[ActorRun | None](ActorRun | None) +PricingModel = Literal['PAY_PER_EVENT', 'PRICE_PER_DATASET_ITEM', 'FLAT_PRICE_PER_MONTH', 'FREE'] +"""Pricing model for an Actor.""" DEFAULT_DATASET_ITEM_EVENT = 'apify-default-dataset-item' @@ -39,6 +45,76 @@ _ensure_context = ensure_context('active') +# --- SDK-side Actor pricing-info models --------------------------------------------------------------- +# +# The Apify platform serializes Actor pricing info into the `APIFY_ACTOR_PRICING_INFO` env var, but it +# omits several metadata fields that `apify-client` v3 treats as required (`apifyMarginPercentage`, +# `createdAt`, `startedAt`, per-event `eventDescription`). The SDK keeps its own minimal copy with +# those fields optional so `Configuration` validates the env var without injecting fake defaults. + + +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 + + +@docs_group('Charging') +class FreeActorPricingInfo(CommonActorPricingInfo): + pricing_model: Annotated[Literal['FREE'], Field(alias='pricingModel')] + + +@docs_group('Charging') +class FlatPricePerMonthActorPricingInfo(CommonActorPricingInfo): + pricing_model: Annotated[Literal['FLAT_PRICE_PER_MONTH'], Field(alias='pricingModel')] + trial_minutes: Annotated[int | None, Field(alias='trialMinutes')] = None + price_per_unit_usd: Annotated[float | None, Field(alias='pricePerUnitUsd')] = None + + +@docs_group('Charging') +class PricePerDatasetItemActorPricingInfo(CommonActorPricingInfo): + pricing_model: Annotated[Literal['PRICE_PER_DATASET_ITEM'], Field(alias='pricingModel')] + unit_name: Annotated[str | None, Field(alias='unitName')] = None + price_per_unit_usd: Annotated[float | None, Field(alias='pricePerUnitUsd')] = None + + +@docs_group('Charging') +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 + + +@docs_group('Charging') +class PricingPerEvent(BaseModel): + model_config = ConfigDict(populate_by_name=True, extra='allow') + + actor_charge_events: Annotated[dict[str, ActorChargeEvent] | None, Field(alias='actorChargeEvents')] = None + + +@docs_group('Charging') +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 + + +ActorPricingInfoModel = ( + FreeActorPricingInfo + | FlatPricePerMonthActorPricingInfo + | PricePerDatasetItemActorPricingInfo + | PayPerEventActorPricingInfo +) +"""Discriminated union of Actor pricing-info models, keyed by `pricing_model`.""" + + @docs_group('Charging') class ChargingManager(Protocol): """Provides fine-grained access to pay-per-event functionality. @@ -308,7 +384,7 @@ async def charge(self, event_name: str, count: int = 1) -> ChargeResult: # the platform handles them automatically based on dataset writes. pass elif event_name in self._pricing_info: - await self._client.run(self._actor_run_id).charge(event_name, charged_count) + await self._client.run(self._actor_run_id).charge(event_name, count=charged_count) else: logger.warning(f"Attempting to charge for an unknown event '{event_name}'") @@ -320,7 +396,7 @@ async def charge(self, event_name: str, count: int = 1) -> ChargeResult: 'event_title': pricing_info.title, 'event_price_usd': float(round(pricing_info.price, 3)), 'charged_count': charged_count, - 'timestamp': datetime.now(timezone.utc).isoformat(), + 'timestamp': datetime.now(UTC).isoformat(), } ) @@ -427,13 +503,14 @@ 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 return _FetchedPricingInfoDict( - pricing_info=run.pricing_info, + pricing_info=_from_client_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'), ) @@ -469,12 +546,26 @@ class PricingInfoItem: class _FetchedPricingInfoDict(TypedDict): - pricing_info: ( - FreeActorPricingInfo - | FlatPricePerMonthActorPricingInfo - | PricePerDatasetItemActorPricingInfo - | PayPerEventActorPricingInfo - | None - ) + pricing_info: ActorPricingInfoModel | None charged_event_counts: dict[str, int] max_total_charge_usd: Decimal + + +def _from_client_pricing_info( + pricing_info: _ClientFree | _ClientFlatPricePerMonth | _ClientPricePerDatasetItem | _ClientPayPerEvent | None, +) -> ActorPricingInfoModel | None: + """Project an `apify-client` pricing-info model (from `Run.pricingInfo`) into the SDK's pricing model. + + The SDK keeps its own minimal pydantic copies so the platform's `APIFY_ACTOR_PRICING_INFO` env var + (which omits some metadata fields apify-client treats as required) deserializes cleanly. The API + response uses apify-client's models, so we convert here on the boundary. + """ + if pricing_info is None: + return None + if isinstance(pricing_info, _ClientPayPerEvent): + return PayPerEventActorPricingInfo.model_validate(pricing_info.model_dump(by_alias=True)) + if isinstance(pricing_info, _ClientFlatPricePerMonth): + return FlatPricePerMonthActorPricingInfo.model_validate(pricing_info.model_dump(by_alias=True)) + if isinstance(pricing_info, _ClientPricePerDatasetItem): + return PricePerDatasetItemActorPricingInfo.model_validate(pricing_info.model_dump(by_alias=True)) + return FreeActorPricingInfo.model_validate(pricing_info.model_dump(by_alias=True)) diff --git a/src/apify/_configuration.py b/src/apify/_configuration.py index b88f9ae5..d84bd2a2 100644 --- a/src/apify/_configuration.py +++ b/src/apify/_configuration.py @@ -5,17 +5,17 @@ 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 from crawlee._utils.urls import validate_http_url from crawlee.configuration import Configuration as CrawleeConfiguration -from apify._models import ( +from apify._charging import ( FlatPricePerMonthActorPricingInfo, FreeActorPricingInfo, PayPerEventActorPricingInfo, @@ -71,6 +71,21 @@ def _load_storage_keys(data: None | str | ActorStorages) -> ActorStorages | None } +def _parse_actor_pricing_info(data: Any) -> Any: + """Parse the raw `APIFY_ACTOR_PRICING_INFO` env var value into a pydantic-friendly form. + + Deserializes a JSON string when needed. Treats `None`, an empty string, and an empty/ + discriminator-less JSON object (`{}` - the value the platform sets for Actors without a configured + pricing model) as "no pricing info" so the union validator doesn't fail on a missing discriminator. + """ + if data is None or data == '': + return None + pricing_info = json.loads(data) if isinstance(data, str) else data + if isinstance(pricing_info, dict) and not pricing_info.get('pricingModel'): + return None + return pricing_info + + @docs_group('Configuration') class Configuration(CrawleeConfiguration): """A class for specifying the configuration of an Actor. @@ -471,7 +486,7 @@ class Configuration(CrawleeConfiguration): description='JSON string with prising info of the actor', discriminator='pricing_model', ), - BeforeValidator(lambda data: json.loads(data) if isinstance(data, str) else data or None), + BeforeValidator(_parse_actor_pricing_info), ] = None charged_event_counts: Annotated[ diff --git a/src/apify/_consts.py b/src/apify/_consts.py index 9bef9cdb..84040460 100644 --- a/src/apify/_consts.py +++ b/src/apify/_consts.py @@ -2,12 +2,86 @@ import re from datetime import timedelta +from enum import StrEnum 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})$' ) +"""Regex matching encrypted input values with base64-encoded components.""" + +EXIT_CODE_SUCCESS = 0 +"""Exit code indicating that the Actor finished successfully.""" + +EXIT_CODE_ERROR_USER_FUNCTION_THREW = 91 +"""Exit code indicating that the Actor's main function raised an exception.""" + + +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' diff --git a/src/apify/_models.py b/src/apify/_models.py deleted file mode 100644 index 357417ec..00000000 --- a/src/apify/_models.py +++ /dev/null @@ -1,266 +0,0 @@ -from __future__ import annotations - -from datetime import datetime -from typing import Annotated, Literal - -from pydantic import BaseModel, BeforeValidator, ConfigDict, Field - -from apify_shared.consts import ActorJobStatus, MetaOrigin, WebhookEventType -from crawlee._utils.urls import validate_http_url - -from apify._utils import docs_group - -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): - model_config = ConfigDict(populate_by_name=True, extra='allow') - - event_types: Annotated[ - list[WebhookEventType], - Field(alias='eventTypes', description='Event types that should trigger the webhook'), - ] - request_url: Annotated[ - str, - Field(alias='requestUrl', description='URL that the webhook should call'), - 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 - 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 - condition: Annotated[WebhookCondition | None, Field(alias='condition')] = None - ignore_ssl_errors: Annotated[bool | None, Field(alias='ignoreSslErrors')] = None - do_not_retry: Annotated[bool | None, Field(alias='doNotRetry')] = None - payload_template: Annotated[ - str | None, - Field(alias='payloadTemplate', description='Template for the payload sent by the webhook'), - ] = None - headers_template: Annotated[str | None, Field(alias='headersTemplate')] = None - 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[MetaOrigin, 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): - model_config = ConfigDict(populate_by_name=True, extra='allow') - - id: Annotated[str, Field(alias='id')] - act_id: Annotated[str, Field(alias='actId')] - user_id: Annotated[str, Field(alias='userId')] - actor_task_id: Annotated[str | None, Field(alias='actorTaskId')] = None - started_at: Annotated[datetime, Field(alias='startedAt')] - finished_at: Annotated[datetime | None, Field(alias='finishedAt')] = None - status: Annotated[ActorJobStatus, Field(alias='status')] - status_message: Annotated[str | None, Field(alias='statusMessage')] = None - is_status_message_terminal: Annotated[bool | None, Field(alias='isStatusMessageTerminal')] = None - meta: Annotated[ActorRunMeta, Field(alias='meta')] - stats: Annotated[ActorRunStats, Field(alias='stats')] - options: Annotated[ActorRunOptions, Field(alias='options')] - build_id: Annotated[str, Field(alias='buildId')] - exit_code: Annotated[int | None, Field(alias='exitCode')] = None - general_access: Annotated[str | None, Field(alias='generalAccess')] = None - default_key_value_store_id: Annotated[str, Field(alias='defaultKeyValueStoreId')] - default_dataset_id: Annotated[str, Field(alias='defaultDatasetId')] - default_request_queue_id: Annotated[str, Field(alias='defaultRequestQueueId')] - build_number: Annotated[str | None, Field(alias='buildNumber')] = None - container_url: Annotated[str | None, Field(alias='containerUrl')] = None - is_container_server_ready: Annotated[bool | None, Field(alias='isContainerServerReady')] = None - git_branch_name: Annotated[str | None, Field(alias='gitBranchName')] = None - usage: Annotated[ActorRunUsage | None, Field(alias='usage')] = None - usage_total_usd: Annotated[float | None, Field(alias='usageTotalUsd')] = None - usage_usd: Annotated[ActorRunUsageUsd | None, Field(alias='usageUsd')] = None - pricing_info: Annotated[ - FreeActorPricingInfo - | FlatPricePerMonthActorPricingInfo - | PricePerDatasetItemActorPricingInfo - | PayPerEventActorPricingInfo - | None, - Field(alias='pricingInfo', discriminator='pricing_model'), - ] = None - charged_event_counts: Annotated[ - dict[str, int] | None, - Field(alias='chargedEventCounts'), - ] = None - metamorphs: Annotated[list[Metamorph] | None, Field(alias='metamorphs')] = None diff --git a/src/apify/_proxy_configuration.py b/src/apify/_proxy_configuration.py index 10cf277f..453f2331 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 @@ -286,9 +286,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/_webhook.py b/src/apify/_webhook.py new file mode 100644 index 00000000..30e84787 --- /dev/null +++ b/src/apify/_webhook.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from apify_client._models import WebhookRepresentation + +from apify._utils import docs_group + +if TYPE_CHECKING: + from apify_client._literals import WebhookEventType + + +@docs_group('Actor') +@dataclass +class Webhook: + """An Apify webhook definition used by the Actor SDK. + + The same instance can be passed as an ad-hoc webhook to `Actor.start()` / `Actor.call()` or as a persistent + webhook to `Actor.add_webhook()` (the `condition.actor_run_id` is set automatically to the current run). + """ + + event_types: list[WebhookEventType] + """Events that trigger the webhook.""" + + request_url: str + """URL the webhook sends its payload to.""" + + payload_template: str | None = None + """Template for the JSON payload sent by the webhook.""" + + headers_template: str | None = None + """Template for the HTTP headers sent by the webhook.""" + + idempotency_key: str | None = None + """Key that prevents creating duplicate webhooks.""" + + ignore_ssl_errors: bool | None = None + """Whether to ignore SSL errors when sending the request.""" + + do_not_retry: bool | None = None + """Whether to skip retrying the request on failure.""" + + description: str | None = None + """Human-readable description of the webhook.""" + + should_interpolate_strings: bool | None = None + """Whether to interpolate variables in string fields of the payload.""" + + +def to_client_representations(webhooks: list[Webhook] | None) -> list[WebhookRepresentation] | None: + """Project SDK webhooks to the minimal ad-hoc representation accepted by the client's `start()` / `call()`.""" + if not webhooks: + return None + return [ + WebhookRepresentation( + event_types=w.event_types, + request_url=w.request_url, + payload_template=w.payload_template, + headers_template=w.headers_template, + ) + for w in webhooks + ] diff --git a/src/apify/events/__init__.py b/src/apify/events/__init__.py index b4d9ea6c..780743b9 100644 --- a/src/apify/events/__init__.py +++ b/src/apify/events/__init__.py @@ -1,5 +1,6 @@ from crawlee.events import Event, EventManager, LocalEventManager from ._apify_event_manager import ApifyEventManager +from ._types import ActorEventTypes -__all__ = ['ApifyEventManager', 'Event', 'EventManager', 'LocalEventManager'] +__all__ = ['ActorEventTypes', 'ApifyEventManager', 'Event', 'EventManager', 'LocalEventManager'] diff --git a/src/apify/events/_apify_event_manager.py b/src/apify/events/_apify_event_manager.py index 0b927ebc..21707875 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/events/_types.py b/src/apify/events/_types.py index f6ff3ee6..d702fa32 100644 --- a/src/apify/events/_types.py +++ b/src/apify/events/_types.py @@ -16,6 +16,14 @@ from apify._utils import docs_group +ActorEventTypes = Literal['systemInfo', 'persistState', 'migrating', 'aborting'] +"""Event types emitted by the Apify platform during an Actor run. + +This is the Apify-specific subset of [`Event`][crawlee.events.Event] — for the full set +(including framework-level events like `SESSION_RETIRED` or `BROWSER_LAUNCHED`) use +[`Event`][crawlee.events.Event] from `apify`. +""" + @docs_group('Event data') class SystemInfoEventData(BaseModel): diff --git a/src/apify/storage_clients/_apify/_alias_resolving.py b/src/apify/storage_clients/_apify/_alias_resolving.py index abca78fe..50234a1a 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 @@ -14,7 +15,7 @@ from collections.abc import Callable from types import TracebackType - from apify_client.clients import ( + from apify_client._resource_clients import ( DatasetClientAsync, DatasetCollectionClientAsync, KeyValueStoreClientAsync, @@ -107,8 +108,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: @@ -257,8 +258,7 @@ 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), ) 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..75e95c60 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,5 @@ 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), ) diff --git a/src/apify/storage_clients/_apify/_dataset_client.py b/src/apify/storage_clients/_apify/_dataset_client.py index fc809646..d8c27792 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, Mapping, Sequence - 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..49c9c4f6 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,12 @@ 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..822c916a 100644 --- a/src/apify/storage_clients/_apify/_models.py +++ b/src/apify/storage_clients/_apify/_models.py @@ -1,15 +1,19 @@ 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 +from apify_client._models import RequestQueueStats from crawlee.storage_clients.models import KeyValueStoreMetadata, RequestQueueMetadata 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): @@ -22,15 +26,6 @@ class ApifyKeyValueStoreMetadata(KeyValueStoreMetadata): """The secret key used for signing URLs for secure access to key-value store records.""" -@docs_group('Storage data') -class ProlongRequestLockResponse(BaseModel): - """Response to prolong request lock calls.""" - - model_config = ConfigDict(populate_by_name=True, extra='allow') - - lock_expires_at: Annotated[datetime, Field(alias='lockExpiresAt')] - - @docs_group('Storage data') class RequestQueueHead(BaseModel): """Model for request queue head. @@ -59,33 +54,21 @@ 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. -class KeyValueStoreKeyInfo(BaseModel): - """Model for a key-value store key info. + Args: + client_locked_head: `LockedRequestQueueHead` instance from Apify API client. - Only internal structure. - """ - - model_config = ConfigDict(populate_by_name=True, extra='allow') + 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') - key: Annotated[str, Field(alias='key')] - size: Annotated[int, Field(alias='size')] - - -class KeyValueStoreListKeysPage(BaseModel): - """Model for listing keys in the key-value store. - - Only internal structure. - """ - - model_config = ConfigDict(populate_by_name=True, extra='allow') - - count: Annotated[int, Field(alias='count')] - limit: Annotated[int, Field(alias='limit')] - is_truncated: Annotated[bool, Field(alias='isTruncated')] - items: Annotated[list[KeyValueStoreKeyInfo], Field(alias='items', default_factory=list)] - exclusive_start_key: Annotated[str | None, Field(alias='exclusiveStartKey', default=None)] - next_exclusive_start_key: Annotated[str | None, Field(alias='nextExclusiveStartKey', default=None)] + # Validate and construct RequestQueueHead from the serialized dict + return cls.model_validate(head_dict) class CachedRequest(BaseModel): @@ -107,25 +90,6 @@ class CachedRequest(BaseModel): """The expiration time of the lock on the request.""" -class RequestQueueStats(BaseModel): - model_config = ConfigDict(populate_by_name=True, extra='allow') - - delete_count: Annotated[int, Field(alias='deleteCount', default=0)] - """"The number of request queue deletes.""" - - head_item_read_count: Annotated[int, Field(alias='headItemReadCount', default=0)] - """The number of request queue head reads.""" - - read_count: Annotated[int, Field(alias='readCount', default=0)] - """The number of request queue reads.""" - - storage_bytes: Annotated[int, Field(alias='storageBytes', default=0)] - """Storage size in bytes.""" - - write_count: Annotated[int, Field(alias='writeCount', default=0)] - """The number of request queue writes.""" - - class ApifyRequestQueueMetadata(RequestQueueMetadata): stats: Annotated[RequestQueueStats, Field(alias='stats', default_factory=RequestQueueStats)] """Additional statistics about the request queue.""" 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 8257ec15..496ec4f1 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 @@ -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) @@ -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.get(request_id): cached_request.was_already_handled = request.was_already_handled @@ -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 9bf9e58e..a42f7252 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 @@ -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( @@ -212,7 +211,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 @@ -295,16 +294,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. @@ -340,7 +339,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: @@ -377,7 +376,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: @@ -390,9 +389,12 @@ 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_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/_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/src/apify/storage_clients/_file_system/_dataset_client.py b/src/apify/storage_clients/_file_system/_dataset_client.py index 057bcc9f..b5ab1a43 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 bdcc9883..6a29342f 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -6,6 +6,7 @@ import subprocess import sys import textwrap +from datetime import UTC, datetime, timedelta from pathlib import Path from typing import TYPE_CHECKING, Any, Protocol @@ -13,19 +14,20 @@ from filelock import FileLock from apify_client import ApifyClient, ApifyClientAsync -from apify_shared.consts import ActorJobStatus, ActorPermissionLevel, ActorSourceType, ApifyEnvVars from crawlee import service_locator import apify._actor from ._utils import generate_unique_resource_name -from apify._models import ActorRun +from apify._consts import ApifyEnvVars from apify.storage_clients._apify._alias_resolving import AliasResolver if TYPE_CHECKING: from collections.abc import Awaitable, Callable, Coroutine, Iterator, Mapping from decimal import Decimal - from apify_client.clients.resource_clients import ActorClientAsync + from apify_client._literals import ActorPermissionLevel + from apify_client._models import Run + 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': 'SOURCE_FILES', '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 == '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,31 @@ 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] + # The API requires the new record to start strictly after all existing records. + latest_started_at = max(pi.started_at for pi in actor.pricing_infos) + new_started_at = max(latest_started_at, datetime.now(tz=UTC)) + timedelta(seconds=1) + new_pricing_infos = [ + *existing_pricing_infos, + { + 'pricingModel': 'FREE', + 'apifyMarginPercentage': 0.0, + 'createdAt': new_started_at.isoformat(), + 'startedAt': new_started_at.isoformat(), + }, + ] + actor_client.update(pricing_infos=new_pricing_infos) actor_client.delete() @@ -355,7 +379,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: @@ -382,19 +406,20 @@ 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, 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 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..7408efc3 100644 --- a/tests/e2e/test_actor_api_helpers.py +++ b/tests/e2e/test_actor_api_helpers.py @@ -2,14 +2,13 @@ import asyncio import json +from datetime import timedelta from typing import TYPE_CHECKING -from apify_shared.consts import ActorPermissionLevel from crawlee._utils.crypto import crypto_random_object_id from ._utils import generate_unique_resource_name from apify import Actor -from apify._models import ActorRun if TYPE_CHECKING: from apify_client import ApifyClientAsync @@ -62,7 +61,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() @@ -129,12 +128,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 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( @@ -144,7 +146,7 @@ async def main_outer() -> None: assert run_result_outer.status == '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 +174,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 == '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( @@ -189,7 +195,7 @@ async def main_outer() -> None: assert run_result_outer.status == '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 +223,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 == '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 +245,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']}, - force_permission_level=ActorPermissionLevel.FULL_PERMISSIONS, + run_input={'test_value': test_value, 'inner_task_id': task.id}, + force_permission_level='FULL_PERMISSIONS', ) assert run_result_outer.status == '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( @@ -272,20 +282,24 @@ async def main_outer() -> None: inner_actor = await make_actor(label='abort-inner', main_func=main_inner) 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'] + run_result_inner = await inner_actor.start(force_permission_level='FULL_PERMISSIONS') + inner_run_id = run_result_inner.id run_result_outer = await run_actor( outer_actor, run_input={'inner_run_id': inner_run_id}, - force_permission_level=ActorPermissionLevel.FULL_PERMISSIONS, + force_permission_level='FULL_PERMISSIONS', ) assert run_result_outer.status == 'SUCCEEDED' - 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_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.') + + inner_actor_last_run = inner_actor_run assert inner_actor_last_run.status == 'ABORTED' @@ -300,7 +314,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 @@ -331,7 +345,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( @@ -395,7 +412,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 = '' @@ -425,7 +442,7 @@ def do_POST(self) -> None: await Actor.set_value('WEBHOOK_BODY', webhook_body) async def main_client() -> None: - from apify import Webhook, WebhookEventType + from apify import Webhook async with Actor: actor_input = await Actor.get_input() or {} @@ -433,7 +450,7 @@ async def main_client() -> None: await Actor.add_webhook( Webhook( - event_types=[WebhookEventType.ACTOR_RUN_SUCCEEDED], + event_types=['ACTOR.RUN.SUCCEEDED'], request_url=server_actor_container_url, ) ) @@ -444,7 +461,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: @@ -458,8 +475,13 @@ async def main_client() -> None: assert ac_run_result.status == 'SUCCEEDED' - 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_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 = sa_run_client_run assert sa_run_result.status == 'SUCCEEDED' diff --git a/tests/e2e/test_actor_call_timeouts.py b/tests/e2e/test_actor_call_timeouts.py index abd754ce..368c685a 100644 --- a/tests/e2e/test_actor_call_timeouts.py +++ b/tests/e2e/test_actor_call_timeouts.py @@ -19,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, timezone + from datetime import UTC, datetime, timedelta async with Actor: actor_input = (await Actor.get_input()) or {} @@ -42,7 +42,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 +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, timezone + from datetime import UTC, datetime, timedelta async with Actor: actor_input = (await Actor.get_input()) or {} @@ -94,7 +94,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 f8b1f393..04a6388a 100644 --- a/tests/e2e/test_actor_charge.py +++ b/tests/e2e/test_actor_charge.py @@ -6,16 +6,13 @@ import pytest_asyncio -from apify_shared.consts import ActorJobStatus - from apify import Actor -from apify._models import ActorRun if TYPE_CHECKING: 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 @@ -35,6 +32,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': { @@ -56,7 +56,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') @@ -85,6 +85,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': { @@ -95,13 +98,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 +132,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 = updated_run try: - assert run.status == ActorJobStatus.SUCCEEDED + assert run.status == 'SUCCEEDED' assert run.charged_event_counts == {'foobar': 4} break except AssertionError: @@ -146,17 +153,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 = updated_run try: - assert run.status == ActorJobStatus.SUCCEEDED - assert run.charged_event_counts == {'foobar': 2} + assert run_result.status == 'SUCCEEDED' + assert run_result.charged_event_counts == {'foobar': 2} break except AssertionError: if is_last_attempt: @@ -177,10 +188,11 @@ async def test_actor_push_data_charges_both_events( for is_last_attempt, _ in retry_counter(120): 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 + assert run.status == 'SUCCEEDED' assert run.charged_event_counts == { 'push-item': 5, 'apify-default-dataset-item': 5, @@ -208,10 +220,11 @@ async def test_actor_push_data_combined_budget_limit( for is_last_attempt, _ in retry_counter(120): 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 + assert run.status == 'SUCCEEDED' assert run.charged_event_counts == { 'push-item': 2, 'apify-default-dataset-item': 2, diff --git a/tests/e2e/test_actor_events.py b/tests/e2e/test_actor_events.py index ce2bf399..d15561ae 100644 --- a/tests/e2e/test_actor_events.py +++ b/tests/e2e/test_actor_events.py @@ -3,8 +3,6 @@ import asyncio from typing import TYPE_CHECKING -from apify_shared.consts import ActorEventTypes - from apify import Actor if TYPE_CHECKING: @@ -22,29 +20,30 @@ 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 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,11 +62,43 @@ async def log_event(data: Any) -> None: assert run_result.status == '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 + + +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 crawlee.events._types import Event + + from apify._consts import ApifyEnvVars + + 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 == 'SUCCEEDED' diff --git a/tests/e2e/test_actor_lifecycle.py b/tests/e2e/test_actor_lifecycle.py index 983b8ca3..10dc6925 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._active 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._active is False + + actor = await make_actor(label='actor-init', main_func=main) + run_result = await run_actor(actor) + + assert run_result.status == '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._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) + + assert run_result.status == 'SUCCEEDED' + + async def test_actor_exit_with_different_exit_codes( make_actor: MakeActorFunction, run_actor: RunActorFunction, @@ -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: diff --git a/tests/e2e/test_actor_request_queue.py b/tests/e2e/test_actor_request_queue.py index 81f75919..5bdb4647 100644 --- a/tests/e2e/test_actor_request_queue.py +++ b/tests/e2e/test_actor_request_queue.py @@ -1,9 +1,9 @@ from __future__ import annotations +from datetime import timedelta from typing import TYPE_CHECKING from apify import Actor -from apify._models import ActorRun if TYPE_CHECKING: from apify_client import ApifyClientAsync @@ -32,14 +32,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 = 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..363b84e7 100644 --- a/tests/e2e/test_actor_scrapy.py +++ b/tests/e2e/test_actor_scrapy.py @@ -3,8 +3,6 @@ from pathlib import Path from typing import TYPE_CHECKING -from apify_shared.consts import ActorPermissionLevel - if TYPE_CHECKING: from .conftest import MakeActorFunction, RunActorFunction @@ -37,7 +35,7 @@ async def test_actor_scrapy_title_spider( 'allowedDomains': ['crawlee.dev'], 'proxyConfiguration': {'useApifyProxy': True}, }, - force_permission_level=ActorPermissionLevel.FULL_PERMISSIONS, + force_permission_level='FULL_PERMISSIONS', ) assert run_result.status == 'SUCCEEDED' diff --git a/tests/e2e/test_crawlee/conftest.py b/tests/e2e/test_crawlee/conftest.py index 9965e5cc..0f2344de 100644 --- a/tests/e2e/test_crawlee/conftest.py +++ b/tests/e2e/test_crawlee/conftest.py @@ -5,9 +5,8 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from apify_client.clients.resource_clients import ActorClientAsync - - from apify._models import ActorRun + from apify_client._models import Run + from apify_client._resource_clients import ActorClientAsync _PYTHON_VERSION = f'{sys.version_info[0]}.{sys.version_info[1]}' @@ -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_fixtures.py b/tests/e2e/test_fixtures.py index 865effa9..ec2a02bf 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 @@ -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)) @@ -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/e2e/test_scrapy/conftest.py b/tests/e2e/test_scrapy/conftest.py index e19f6c36..93a09d79 100644 --- a/tests/e2e/test_scrapy/conftest.py +++ b/tests/e2e/test_scrapy/conftest.py @@ -4,9 +4,8 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from apify_client.clients.resource_clients import ActorClientAsync - - from apify._models import ActorRun + from apify_client._models import Run + from apify_client._resource_clients import ActorClientAsync _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/integration/conftest.py b/tests/integration/conftest.py index 30aa077d..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 @@ -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..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 @@ -117,7 +116,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..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 @@ -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 ab8967d2..8a2b0dc6 100644 --- a/tests/integration/test_request_queue.py +++ b/tests/integration/test_request_queue.py @@ -2,18 +2,19 @@ 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 import pytest -from apify_shared.consts import ApifyEnvVars +from apify_client._models import BatchAddResult, RequestDraft 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 @@ -887,7 +888,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( @@ -906,7 +907,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( @@ -979,7 +980,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) @@ -1005,13 +1006,13 @@ async def _get_rq_metadata() -> ApifyRequestQueueMetadata: metadata = await poll_until_condition( _get_rq_metadata, - lambda m: m.stats.read_count - stats_before.read_count >= len(requests), + lambda m: (m.stats.read_count or 0) - (stats_before.read_count or 0) >= len(requests), ) stats_after = metadata.stats Actor.log.info(stats_after) # Cache was actually initialized, readCount increased - assert (stats_after.read_count - stats_before.read_count) == len(requests) + assert (stats_after.read_count or 0) - (stats_before.read_count or 0) == len(requests) # Deduplication happened locally, writeCount should be the same assert stats_after.write_count == stats_before.write_count @@ -1107,11 +1108,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( @@ -1149,22 +1150,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. @@ -1176,15 +1186,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( @@ -1273,7 +1284,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') @@ -1282,16 +1293,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( @@ -1306,7 +1318,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) @@ -1314,16 +1326,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( @@ -1340,7 +1353,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))) @@ -1352,16 +1365,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_charge.py b/tests/unit/actor/test_actor_charge.py index 4e452e78..14d75284 100644 --- a/tests/unit/actor/test_actor_charge.py +++ b/tests/unit/actor/test_actor_charge.py @@ -6,8 +6,7 @@ from unittest.mock import AsyncMock, Mock, patch from apify import Actor, Configuration -from apify._charging import ChargingManagerImplementation, PricingInfoItem -from apify._models import PayPerEventActorPricingInfo +from apify._charging import ChargingManagerImplementation, PayPerEventActorPricingInfo, PricingInfoItem class MockedChargingSetup(NamedTuple): @@ -32,7 +31,7 @@ async def setup_mocked_charging( setup.charging_mgr._pricing_info['event'] = PricingInfoItem(Decimal('1.0'), 'Event') result = await Actor.charge('event', count=1) - setup.mock_charge.assert_called_once_with('event', 1) + setup.mock_charge.assert_called_once_with('event', count=1) """ # Mock the ApifyClientAsync mock_client = Mock() @@ -76,7 +75,7 @@ async def test_actor_charge_push_data_with_no_remaining_budget() -> None: result1 = await Actor.charge('some-event', count=1) # Costs $1, leaving $0.5 # Verify the first charge call was made correctly - setup.mock_charge.assert_called_once_with('some-event', 1) + setup.mock_charge.assert_called_once_with('some-event', count=1) setup.mock_charge.reset_mock() assert result1.charged_count == 1 @@ -111,7 +110,7 @@ async def test_actor_charge_api_call_verification() -> None: # Call charge with count=1 - this SHOULD call the API result2 = await Actor.charge('test-event', count=1) - setup.mock_charge.assert_called_once_with('test-event', 1) + setup.mock_charge.assert_called_once_with('test-event', count=1) assert result2.charged_count == 1 @@ -124,15 +123,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 +239,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_actor_create_proxy_configuration.py b/tests/unit/actor/test_actor_create_proxy_configuration.py index 86622b71..9d409ee2 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 @@ -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_env_helpers.py b/tests/unit/actor/test_actor_env_helpers.py index 25e337bb..b539b5a2 100644 --- a/tests/unit/actor/test_actor_env_helpers.py +++ b/tests/unit/actor/test_actor_env_helpers.py @@ -8,18 +8,78 @@ from pydantic_core import TzInfo -from apify_shared.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 import Actor +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 diff --git a/tests/unit/actor/test_actor_helpers.py b/tests/unit/actor/test_actor_helpers.py index 8067e6be..6c034193 100644 --- a/tests/unit/actor/test_actor_helpers.py +++ b/tests/unit/actor/test_actor_helpers.py @@ -3,62 +3,66 @@ 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 from apify_client import ApifyClientAsync -from apify_shared.consts import ApifyEnvVars, WebhookEventType +from apify_client._models import Run 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 @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,10 @@ 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=['ACTOR.BUILD.ABORTED'], + request_url='https://example.com', + ) ) matching = [r for r in caplog.records if 'Actor.add_webhook()' in r.message] @@ -235,7 +242,7 @@ async def test_remote_method_with_webhooks( actor_method = getattr(Actor, actor_method_name) await actor_method( entity_id, - webhooks=[Webhook(event_types=[WebhookEventType.ACTOR_RUN_SUCCEEDED], request_url='https://example.com')], + webhooks=[Webhook(event_types=['ACTOR.RUN.SUCCEEDED'], request_url='https://example.com')], ) calls = apify_client_async_patcher.calls[client_resource][client_method] @@ -264,7 +271,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('run_timeout') == timedelta(seconds=120) async def test_call_actor_with_remaining_time_deprecation( @@ -328,7 +335,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 @@ -339,7 +346,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_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 03fdd00e..167f73a7 100644 --- a/tests/unit/actor/test_actor_lifecycle.py +++ b/tests/unit/actor/test_actor_lifecycle.py @@ -2,16 +2,22 @@ import asyncio import contextlib +import json import logging -from typing import TYPE_CHECKING -from unittest.mock import AsyncMock +from datetime import UTC, datetime +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 crawlee.events._types import Event, EventPersistStateData from apify import Actor +from apify._consts import EXIT_CODE_ERROR_USER_FUNCTION_THREW, ActorEnvVars, ApifyEnvVars if TYPE_CHECKING: from collections.abc import AsyncGenerator, Callable @@ -175,7 +181,7 @@ async def test_unhandled_exception_sets_error_exit_code() -> None: async with actor: raise RuntimeError('Test error') - assert actor.exit_code == ActorExitCodes.ERROR_USER_FUNCTION_THREW.value + assert actor.exit_code == EXIT_CODE_ERROR_USER_FUNCTION_THREW async def test_actor_stops_periodic_events_after_exit(monkeypatch: pytest.MonkeyPatch) -> None: @@ -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(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/actor/test_charging_manager.py b/tests/unit/actor/test_charging_manager.py index 6fb84b2c..422de20b 100644 --- a/tests/unit/actor/test_charging_manager.py +++ b/tests/unit/actor/test_charging_manager.py @@ -6,12 +6,8 @@ import pytest -from apify._charging import ChargingManagerImplementation +from apify._charging import ActorChargeEvent, ChargingManagerImplementation, PayPerEventActorPricingInfo from apify._configuration import Configuration -from apify._models import ( - ActorChargeEvent, - PayPerEventActorPricingInfo, -) def _make_config(**kwargs: Any) -> Configuration: @@ -49,12 +45,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..0fd686dd 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', + } + } }, } ) @@ -331,6 +340,39 @@ def test_actor_pricing_info_from_json_env_var(monkeypatch: pytest.MonkeyPatch) - assert config.actor_pricing_info.pricing_model == 'PAY_PER_EVENT' +def test_actor_pricing_info_env_var_tolerates_platform_omissions(monkeypatch: pytest.MonkeyPatch) -> None: + """The platform env var may omit fields that apify-client models require; they should be injected with defaults.""" + + pricing_json = json.dumps( + { + 'pricingModel': 'PAY_PER_EVENT', + 'pricingPerEvent': { + 'actorChargeEvents': { + 'search': { + 'eventPriceUsd': '0.01', + 'eventTitle': 'Search event', + } + } + }, + } + ) + monkeypatch.setenv('APIFY_ACTOR_PRICING_INFO', pricing_json) + config = ApifyConfiguration() + assert config.actor_pricing_info is not None + assert config.actor_pricing_info.pricing_model == 'PAY_PER_EVENT' + + +@pytest.mark.parametrize('env_value', ['', '{}']) +def test_actor_pricing_info_env_var_empty_becomes_none(monkeypatch: pytest.MonkeyPatch, env_value: str) -> None: + """Platform sends `APIFY_ACTOR_PRICING_INFO={}` for Actors without a pricing model. + + Without a `pricingModel` discriminator, the pydantic union cannot resolve — treat it as no pricing info. + """ + monkeypatch.setenv('APIFY_ACTOR_PRICING_INFO', env_value) + config = ApifyConfiguration() + assert config.actor_pricing_info is None + + def test_actor_storage_json_env_var(monkeypatch: pytest.MonkeyPatch) -> None: """Test that actor_storages_json is parsed from JSON env var.""" datasets = {'default': 'default_dataset_id', 'custom': 'custom_dataset_id'} diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 3cd2e32c..c289a3dd 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: @@ -62,6 +62,7 @@ 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 # Set the environment variable for the local storage directory to the temporary path. diff --git a/tests/unit/events/test_apify_event_manager.py b/tests/unit/events/test_apify_event_manager.py index eb8dd375..2b8ff412 100644 --- a/tests/unit/events/test_apify_event_manager.py +++ b/tests/unit/events/test_apify_event_manager.py @@ -14,10 +14,10 @@ import websockets.asyncio.server import websockets.exceptions -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/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/tests/unit/storage_clients/test_apify_request_queue_client.py b/tests/unit/storage_clients/test_apify_request_queue_client.py index 6117afd6..ae110adf 100644 --- a/tests/unit/storage_clients/test_apify_request_queue_client.py +++ b/tests/unit/storage_clients/test_apify_request_queue_client.py @@ -1,10 +1,11 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime from unittest.mock import AsyncMock import pytest +from apify_client._models import RequestQueueHead from crawlee.storage_clients.models import RequestQueueMetadata from apify.storage_clients._apify._request_queue_single_client import ApifyRequestQueueSingleClient @@ -16,7 +17,7 @@ def _make_single_client( ) -> tuple[ApifyRequestQueueSingleClient, AsyncMock]: if api_client is None: api_client = AsyncMock() - now = datetime.now(tz=timezone.utc) + now = datetime.now(tz=UTC) metadata = RequestQueueMetadata( id='test-rq-id', name='test-rq', @@ -78,7 +79,14 @@ def test_unique_key_to_request_id_matches_known_values(unique_key: str, expected ) async def test_list_head_limit(in_progress_count: int, expected_limit: int) -> None: client, api_client = _make_single_client() - api_client.list_head = AsyncMock(return_value={'items': [], 'hadMultipleClients': False}) + api_client.list_head = AsyncMock( + return_value=RequestQueueHead( + limit=expected_limit, + queue_modified_at=datetime.now(tz=UTC), + had_multiple_clients=False, + items=[], + ) + ) client._requests_in_progress = {f'req_{i}' for i in range(in_progress_count)} await client._list_head() 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 e0f65f78..d11ec1da 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: @@ -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/uv.lock b/uv.lock index 8b28fc8b..05840d17 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.10" +requires-python = ">=3.11" [options] exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. @@ -26,7 +26,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'" }, ] @@ -41,14 +40,13 @@ version = "3.4.1" source = { editable = "." } dependencies = [ { name = "apify-client" }, - { name = "apify-shared" }, { name = "cachetools" }, { name = "crawlee" }, { name = "cryptography" }, { name = "impit" }, { name = "lazy-object-proxy" }, { name = "more-itertools" }, - { name = "pydantic" }, + { name = "pydantic", extra = ["email"] }, { name = "typing-extensions" }, { name = "websockets" }, { name = "yarl" }, @@ -85,15 +83,14 @@ dev = [ [package.metadata] requires-dist = [ - { name = "apify-client", specifier = ">=2.3.0,<3.0.0" }, - { name = "apify-shared", specifier = ">=2.0.0,<3.0.0" }, + { name = "apify-client", specifier = ">=3.0.0,<4.0.0" }, { name = "cachetools", specifier = ">=5.5.0" }, { name = "crawlee", specifier = ">=1.0.4,<2.0.0" }, { name = "cryptography", specifier = ">=42.0.0" }, { 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" }, @@ -127,26 +124,17 @@ dev = [ [[package]] name = "apify-client" -version = "2.5.0" +version = "3.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "apify-shared" }, { name = "colorama" }, { name = "impit" }, { name = "more-itertools" }, + { name = "pydantic", extra = ["email"] }, ] -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" }, -] - -[[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" } +sdist = { url = "https://files.pythonhosted.org/packages/64/08/5d6c2fa64fd93b837325368aedaa23f57897030eff31a150873f3aa697e3/apify_client-3.0.2.tar.gz", hash = "sha256:e34dc9c8ac951154a0382adfce785d9658639c373425b43ac54df510d076ac0d", size = 122200, upload-time = "2026-05-26T07:23:43.691Z" } 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" }, + { url = "https://files.pythonhosted.org/packages/d2/32/ac2ab06f8713f9a1313912815f57431d495cf04861a9f9a2436310dcf38e/apify_client-3.0.2-py3-none-any.whl", hash = "sha256:d78e188eb98e8357a21b57d9b31b139410b517eecc2d125c80f054cdc604296f", size = 141197, upload-time = "2026-05-26T07:23:42.039Z" }, ] [[package]] @@ -176,15 +164,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.5.0" @@ -196,16 +175,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/22/58/0a9d9b1195c159d206000c541c3e05897e339be754f0e4d8b29445ab536e/black-26.5.0.tar.gz", hash = "sha256:5cbe4cc4037ffca34cdb0a6a9a046f104b262d0bd63c30fd4a88c7adc2049b1d", size = 677762, upload-time = "2026-05-16T17:57:12.54Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/2a/f0bdee0d043b9e860fc1ae35596aa6d663d334b195d87019532afe97f29f/black-26.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:402454bfdd7a940be00455e87309438a24b328b7ba7d80b7207e8a87b32ffc29", size = 1983871, upload-time = "2026-05-16T18:00:43.834Z" }, - { url = "https://files.pythonhosted.org/packages/fe/a9/3dbf82806bc3b884ccb116a0f3b34f94ee2e0e6d5477d7abd215b1704907/black-26.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4863b2a2c382661a018bf2213f2b957fa34511df131259ffaa8d54859620ac31", size = 1806039, upload-time = "2026-05-16T18:00:45.681Z" }, - { url = "https://files.pythonhosted.org/packages/57/10/150f2b66a08f840b89824dc5750363ee834e73e6b1b31050cfe4e76e13f3/black-26.5.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:490b623006a75c0ea59c1ecf91cc76ecb9d66df1482c3a53f4f7de95a7c85e10", size = 1856443, upload-time = "2026-05-16T18:00:47.89Z" }, - { url = "https://files.pythonhosted.org/packages/c4/71/d1f562c52c7a55060783e82b07b47c7eb09384f3f2759f868028a8a8aba7/black-26.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:6f53deb3d1108a523212da5c79e5c0cd76abcc548948f2d8415e62929c81a569", size = 1474602, upload-time = "2026-05-16T18:00:49.678Z" }, - { url = "https://files.pythonhosted.org/packages/1e/35/a0e0a1e57bd72099fc72b52e96fbfdc52af273254526e6783bcf136ae207/black-26.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:a62f9d069ac27de20c6fa3dbf60d7c951141c4025bb9755274802d05b1aa418b", size = 1273042, upload-time = "2026-05-16T18:00:51.949Z" }, { url = "https://files.pythonhosted.org/packages/6b/71/17d04d49a406640f531f6d12e0f15858e0d337b7dbd4a5a05476cd04b229/black-26.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:862945b2a08193cdff9f632f51bdadbb11e6852da1d31c306a3508449dc81b84", size = 1965325, upload-time = "2026-05-16T18:00:53.755Z" }, { url = "https://files.pythonhosted.org/packages/1a/a6/0739015dbd9df669529657bf6bef1185679a0eb8ba93bb6e160561f57652/black-26.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:03102aa97c279e5f62e1e1ab828cfe8aa72c3af4cf86f9448e5537b2519cbfea", size = 1786840, upload-time = "2026-05-16T18:00:55.55Z" }, { url = "https://files.pythonhosted.org/packages/09/23/6cd101b4bc2234708120450d8ac54f6580d6ae52f6dce1098e040e6f259c/black-26.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:990ee0e1d96dd8ca623f19dd3f339c138bdc02f74e4fea01cc64aee38944ea2b", size = 1840560, upload-time = "2026-05-16T18:00:57.103Z" }, @@ -235,10 +207,8 @@ version = "1.5.0" 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/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796, upload-time = "2026-04-30T03:18:25.17Z" } wheels = [ @@ -272,18 +242,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" }, @@ -360,22 +318,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" }, @@ -495,20 +437,6 @@ version = "7.14.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/23/7f/d0720730a397a999ffc0fd3f5bebef347338e3a47b727da66fbb228e2ff2/coverage-7.14.0.tar.gz", hash = "sha256:057a6af2f160a85384cde4ab36f0d2777bae1057bae255f95413cdd382aa5c74", size = 919489, upload-time = "2026-05-10T18:02:31.397Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/9d/7c83ef51c3eb495f10010094e661833588b7709946da634c8b66520b97c7/coverage-7.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84c32d90bf4537f0e7b4dec9aaa9a938fb8205136b9d2ecf4d7629d5262dc075", size = 219668, upload-time = "2026-05-10T17:59:23.106Z" }, - { url = "https://files.pythonhosted.org/packages/24/34/898546aefbd28f0af131201d0dc852c9e976f817bd7d5bfb8dc4e02863bb/coverage-7.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7c843572c605ab51cfdb5c6b5f2586e2a8467c0d28eca4bdef4ec70c5fecbd82", size = 220192, upload-time = "2026-05-10T17:59:26.095Z" }, - { url = "https://files.pythonhosted.org/packages/df/4a/b457c88aca72b0df13a98167ebd5d947135ccd9881ea88ce6a570e13aa9b/coverage-7.14.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0c451757d3fa2603354fdc789b5e58a0e327a117c370a40e3476ba4eabab228c", size = 246932, upload-time = "2026-05-10T17:59:27.806Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d9/92600e89486fd074c50f0117422b2c9592c3e144e2f25bd5ac0bc62bc7a0/coverage-7.14.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3fd43f0616e765ab78d069cf8358def7363957a45cee446d65c502dcfeea7893", size = 248762, upload-time = "2026-05-10T17:59:29.479Z" }, - { url = "https://files.pythonhosted.org/packages/0d/e1/9ea1eb9c311da7f15853559dc1d9d82bef88ecd3e59fbeb51f16bc2ffa91/coverage-7.14.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:731e535b1498b27d13594a0527a79b0510867b0ad891532be41cb883f2128e20", size = 250625, upload-time = "2026-05-10T17:59:31.33Z" }, - { url = "https://files.pythonhosted.org/packages/a5/03/57afca1b8106f8549a5329139315041fe166d6099bd9381346b9430dfbd1/coverage-7.14.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c7492f2d493b976941c7ca050f273cbda2f43c381124f7586a3e3c16d1804fec", size = 252539, upload-time = "2026-05-10T17:59:32.692Z" }, - { url = "https://files.pythonhosted.org/packages/57/5e/2e9fc63c9928119c1dbae02222be51407d3e7ebac5811ebbda4af3557795/coverage-7.14.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc38367eaa2abb1b766ac333142bce7655335a73537f5c8b75aaa89c2b987757", size = 247636, upload-time = "2026-05-10T17:59:34.599Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e2/0b7898cda21041cc67546e19b80ba66cbbb47cbece52a76a5904de6a3aaf/coverage-7.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0a951308cde22cf77f953955a754d04dccb57fe3bb8e345d685778ed9fc1632a", size = 248666, upload-time = "2026-05-10T17:59:36.232Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/d33662a2fdaef23229c15921f39c84ec38441f3069ba26e134ed402c833b/coverage-7.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fab3877e4ebb06bd9d4d4d00ee53309ee5478e66873c66a382272e3ee33eb7ea", size = 246670, upload-time = "2026-05-10T17:59:38.029Z" }, - { url = "https://files.pythonhosted.org/packages/99/b2/533942c3bfbf6770b5c32d7f2ff029fe013dba31f3fe8b45cabbb250365e/coverage-7.14.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b812eb847b19876ebf33fb6c4f11819af05ab6050b0bfa1bc53412ae81779adb", size = 250484, upload-time = "2026-05-10T17:59:39.974Z" }, - { url = "https://files.pythonhosted.org/packages/d8/00/15acbad83a96de13c73831486c7627bfed73dfaec53b04e4a6315edf3fd8/coverage-7.14.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d9c8ef6ed820c433de075657d72dda1f89a2984955e58b8a75feb3f184250218", size = 246942, upload-time = "2026-05-10T17:59:41.659Z" }, - { url = "https://files.pythonhosted.org/packages/70/db/cef0228de493f2c740c760a9057a61d00c6849480073b70a75b87c7d4bab/coverage-7.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d128b1bba9361fbaaf6a19e179e6cfd6a9103ce0c0555876f72780acc93efd85", size = 247544, upload-time = "2026-05-10T17:59:43.471Z" }, - { url = "https://files.pythonhosted.org/packages/77/a0/d9ef8e148f3025c2ae8401d77cda1502b6d2a4d8102603a8af31460aedb6/coverage-7.14.0-cp310-cp310-win32.whl", hash = "sha256:65f267ca1370726ec2c1aa38bbe4df9a71a740f22878d2d4bf59d71a4cd8d323", size = 222285, upload-time = "2026-05-10T17:59:44.908Z" }, - { url = "https://files.pythonhosted.org/packages/85/c0/30c454c7d3cf47b2805d4e06f12443f5eece8a5d030d3b0350e7b74ecb49/coverage-7.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:b34ece8065914f938ed7f2c5872bb865336977a52919149846eac3744327267a", size = 223215, upload-time = "2026-05-10T17:59:46.779Z" }, { url = "https://files.pythonhosted.org/packages/fc/e4/649c8d4f7f1709b6dbfc474358aa1bba02f67bcd52e2fec291a5014006cd/coverage-7.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a78e2a9d9c5e3b8d4ab9b9d28c985ea66fced0a7d7c2aec1f216e03a2011480", size = 219795, upload-time = "2026-05-10T17:59:48.198Z" }, { url = "https://files.pythonhosted.org/packages/7f/8d/46692d24b3f395d4cbf17bfcc57136b4f2f9c0c0df864b0bddfc1d71a014/coverage-7.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1816c505187592dcd1c5a5f226601a549f70365fbd00930ac88b0c225b76bb4", size = 220299, upload-time = "2026-05-10T17:59:49.683Z" }, { url = "https://files.pythonhosted.org/packages/12/c2/a40f5cb295bbcbb697a76947a56081c494c61950366294ee426ffe261099/coverage-7.14.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d8e1762f0e9cbc26ec315471e7b47855218e833cd5a032d706fbf43845d878c7", size = 250721, upload-time = "2026-05-10T17:59:51.494Z" }, @@ -642,7 +570,6 @@ version = "48.0.0" 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/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } wheels = [ @@ -775,6 +702,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" @@ -822,15 +758,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]] @@ -901,13 +838,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" }, @@ -974,13 +904,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" }, @@ -1023,25 +946,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 = [ @@ -1107,12 +1017,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" }, @@ -1152,22 +1056,6 @@ version = "6.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/28/30/9abc9e34c657c33834eaf6cd02124c61bdf5944d802aa48e69be8da3585d/lxml-6.1.0.tar.gz", hash = "sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13", size = 4197006, upload-time = "2026-04-18T04:32:51.613Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/6e/ee8fc0e01202eb3dd2b9e1ea4f0910d72425d35c66187c63931d7a3ea73f/lxml-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:41dcc4c7b10484257cbd6c37b83ddb26df2b0e5aff5ac00d095689015af868ec", size = 8540733, upload-time = "2026-04-18T04:27:33.185Z" }, - { url = "https://files.pythonhosted.org/packages/54/e8/325fe9b942824c773dffe1baf0c35b046a763851fdff4393af4450bceeb7/lxml-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a31286dbb5e74c8e9a5344465b77ab4c5bd511a253b355b5ca2fae7e579fafec", size = 4602805, upload-time = "2026-04-18T04:27:36.097Z" }, - { url = "https://files.pythonhosted.org/packages/2d/81/221aa3ea4a40370bb0358fa454cbe7e5a837e522f7630c24dfef3f9a73b0/lxml-6.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1bc4cc83fb7f66ffb16f74d6dd0162e144333fc36ebcce32246f80c8735b2551", size = 5002652, upload-time = "2026-04-18T04:27:30.603Z" }, - { url = "https://files.pythonhosted.org/packages/c6/e1/fdbfb9019542f1875c093576df7f37adc2983c8ba7ecf17e5f14490bc107/lxml-6.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:20cf4d0651987c906a2f5cba4e3a8d6ba4bfdf973cfe2a96c0d6053888ea2ecd", size = 5155332, upload-time = "2026-04-18T04:27:33.507Z" }, - { url = "https://files.pythonhosted.org/packages/56/b1/4087c782fff397cd03abf9c551069be59bb04a7e548c50fb7b9c4cdaca28/lxml-6.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb34ea45a82dd637c2c97ae1bbb920850c1e59bcae79ce1c15af531d83e7215", size = 5057226, upload-time = "2026-04-18T04:27:37.567Z" }, - { url = "https://files.pythonhosted.org/packages/5d/66/516c79dec8417f3a972327330254c0b5fac93d5c3ecfd8a5b43650a5a4d9/lxml-6.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1d9b99e5b2597e4f5aed2484fef835256fa1b68a19e4265c97628ef4bf8bcf4", size = 5287588, upload-time = "2026-04-18T04:27:41.4Z" }, - { url = "https://files.pythonhosted.org/packages/94/1d/e578f4cbeb42b9df9f29b0d44a45a7cdfa3a5ae300dd59ec68e3602d29bb/lxml-6.1.0-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:d43aa26dcda363f21e79afa0668f5029ed7394b3bb8c92a6927a3d34e8b610ea", size = 5412438, upload-time = "2026-04-18T04:27:45.589Z" }, - { url = "https://files.pythonhosted.org/packages/47/5b/2aa68307d6d15959e84d4882f9c04f2da63127eac463e1594166f681ef77/lxml-6.1.0-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:6262b87f9e5c1e5fe501d6c153247289af42eb44ad7660b9b3de17baaf92d6f6", size = 4770997, upload-time = "2026-04-18T04:27:49.853Z" }, - { url = "https://files.pythonhosted.org/packages/ae/c9/3e51fc1228310a836b4eb32595ae00154ab12197fca944676a3ab3b163ea/lxml-6.1.0-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1392c569c032f78a11a25d1de1c43fff13294c793b39e19d84fade3045cbbc3", size = 5359678, upload-time = "2026-04-18T04:31:56.184Z" }, - { url = "https://files.pythonhosted.org/packages/b5/91/ab8bc834f977fbbd310e697b120787c153db026f9151e02a88d2645d4e5b/lxml-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:045e387d1f4f42a418380930fa3f45c73c9b392faf67e495e58902e68e8f44a7", size = 5107890, upload-time = "2026-04-18T04:32:00.387Z" }, - { url = "https://files.pythonhosted.org/packages/bb/10/8a143cfa3ac99cb5b0523ff6d0429a9c9dddf25ffeae09caa3866c7964d9/lxml-6.1.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:9f93d5b8b07f73e8c77e3c6556a3db269918390c804b5e5fcdd4858232cc8f16", size = 4803977, upload-time = "2026-04-18T04:32:05.099Z" }, - { url = "https://files.pythonhosted.org/packages/45/fd/ee02faf52fa39c2fe32f824628958b9aa86dff21343dc3161f0e3c6ccd15/lxml-6.1.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:de550d129f18d8ab819651ffe4f38b1b713c7e116707de3c0c6400d0ef34fbc1", size = 5350277, upload-time = "2026-04-18T04:32:09.176Z" }, - { url = "https://files.pythonhosted.org/packages/85/8c/b3481364b8554b5d36d540189a87fc71e94b0b01c24f8f152bd662dd2e45/lxml-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c08da09dc003c9e8c70e06b53a11db6fb3b250c21c4236b03c7d7b443c318e7a", size = 5309717, upload-time = "2026-04-18T04:32:13.303Z" }, - { url = "https://files.pythonhosted.org/packages/74/e8/a6b21927077a9127afa17473b6576b322616f34ac50ee4f577e763b75ec0/lxml-6.1.0-cp310-cp310-win32.whl", hash = "sha256:37448bf9c7d7adfc5254763901e2bbd6bb876228dfc1fc7f66e58c06368a7544", size = 3598491, upload-time = "2026-04-18T04:27:24.288Z" }, - { url = "https://files.pythonhosted.org/packages/ea/82/14dea800d041274d96c07d49ff9191f011d1427450850de19bf541e2cc12/lxml-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:2593a0a6621545b9095b71ad74ed4226eba438a7d9fc3712a99bdb15508cf93a", size = 4020906, upload-time = "2026-04-18T04:27:27.53Z" }, - { url = "https://files.pythonhosted.org/packages/f2/ba/d3539aaf4d9d21456b9a7b902816623227d05d63e7c5aafd8834c4b9bed6/lxml-6.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:e80807d72f96b96ad5588cb85c75616e4f2795a7737d4630784c51497beb7776", size = 3667787, upload-time = "2026-04-18T04:27:29.407Z" }, { url = "https://files.pythonhosted.org/packages/5e/5d/3bccad330292946f97962df9d5f2d3ae129cce6e212732a781e856b91e07/lxml-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cec05be8c876f92a5aa07b01d60bbb4d11cfbdd654cad0561c0d7b5c043a61b9", size = 8526232, upload-time = "2026-04-18T04:27:40.389Z" }, { url = "https://files.pythonhosted.org/packages/a7/51/adc8826570a112f83bb4ddb3a2ab510bbc2ccd62c1b9fe1f34fae2d90b57/lxml-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9c03e048b6ce8e77b09c734e931584894ecd58d08296804ca2d0b184c933ce50", size = 4595448, upload-time = "2026-04-18T04:27:44.208Z" }, { url = "https://files.pythonhosted.org/packages/54/84/5a9ec07cbe1d2334a6465f863b949a520d2699a755738986dcd3b6b89e3f/lxml-6.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:942454ff253da14218f972b23dc72fa4edf6c943f37edd19cd697618b626fac5", size = 4923771, upload-time = "2026-04-18T04:32:17.402Z" }, @@ -1270,17 +1158,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" }, @@ -1362,29 +1239,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" }, @@ -1613,7 +1469,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/3b/f5/d501fcb67e450fd3fae9db06050420c0c6043758cfa8c30ba40278211265/poethepoet-0.46.0.tar.gz", hash = "sha256:daf8469031879ef59ef0b34fdba83574d65e41eb9186e20cd0f7c89ce479b030", size = 117276, upload-time = "2026-05-15T15:52:02.548Z" } wheels = [ @@ -1642,23 +1497,6 @@ version = "0.5.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/56/030b7b4719d53085722893e0009dffb9236aa10bca1b12121bdc5626ef16/propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b", size = 93417, upload-time = "2026-05-08T20:59:15.597Z" }, - { url = "https://files.pythonhosted.org/packages/1a/55/1140a8e067b8ec093a18a4ae7bb0045d9db65da38a08618ddc5e2f1994aa/propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c", size = 53847, upload-time = "2026-05-08T20:59:17.096Z" }, - { url = "https://files.pythonhosted.org/packages/20/42/0e7443c90310498561addf346e7d57fe3c6ba1914e1ba938b5464c7bbfd2/propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb", size = 53512, upload-time = "2026-05-08T20:59:18.64Z" }, - { url = "https://files.pythonhosted.org/packages/b7/db/cf51a71bab2009517d1a7f0ee07657e3bd446c4d69f67e6966cf17bcf956/propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e", size = 58068, upload-time = "2026-05-08T20:59:20.683Z" }, - { url = "https://files.pythonhosted.org/packages/b7/43/39b6bdee9699fa1e1641c519feeb64a67e2a9f93bb465c70776b37a7333f/propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e", size = 61020, upload-time = "2026-05-08T20:59:22.112Z" }, - { url = "https://files.pythonhosted.org/packages/26/0b/843726fbb0a29a8c5684fdb25971823638399f31e52e9d1f06a02dc9aa6b/propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b", size = 62732, upload-time = "2026-05-08T20:59:23.805Z" }, - { url = "https://files.pythonhosted.org/packages/39/6e/899fed76dc1942b8a64193a4f059d7f1a2c7ef65085e8a9366ed8ec0d199/propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d", size = 60140, upload-time = "2026-05-08T20:59:25.389Z" }, - { url = "https://files.pythonhosted.org/packages/ab/09/3da4be9b5b879219ad234aa535b3dd4a080ed1ad48d3a73ca07a9e798f22/propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d", size = 60400, upload-time = "2026-05-08T20:59:27.238Z" }, - { url = "https://files.pythonhosted.org/packages/60/2f/09b72b874a9aa0044faf52a69807a6ed618e267ceaa9ec4a63195fa5b504/propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0", size = 58155, upload-time = "2026-05-08T20:59:28.48Z" }, - { url = "https://files.pythonhosted.org/packages/8a/37/97489848c54c95578045473954f10956d619ce6a09e7ac137b71cdcb698b/propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b", size = 57037, upload-time = "2026-05-08T20:59:30.146Z" }, - { url = "https://files.pythonhosted.org/packages/22/db/6c695285ccfc49012743ee9c98212b8c5dd0aed7b63cfd816d4a0f7a1601/propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf", size = 61103, upload-time = "2026-05-08T20:59:31.626Z" }, - { url = "https://files.pythonhosted.org/packages/98/a9/1e500401ca593b0bdb6bf75a70bc2d723835fd53360edff6af70692c7546/propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf", size = 60394, upload-time = "2026-05-08T20:59:32.829Z" }, - { url = "https://files.pythonhosted.org/packages/1f/87/f638b6e375eae0f30a1a2325d8b34fd85fdc785bb9960cf805f3bf1ec69a/propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e", size = 63084, upload-time = "2026-05-08T20:59:35.964Z" }, - { url = "https://files.pythonhosted.org/packages/f6/18/884573f5d97b6d9eba68de759a82c901b7e39d7904d30f7b8d58d42d2a12/propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274", size = 60999, upload-time = "2026-05-08T20:59:38.481Z" }, - { url = "https://files.pythonhosted.org/packages/8f/1a/c3915eb059ceec9e758a56e4cfd955292bc0f201be2176a46b76d94b303a/propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe", size = 39036, upload-time = "2026-05-08T20:59:40.323Z" }, - { url = "https://files.pythonhosted.org/packages/5b/02/1dfd5607501a602d19c1c449d2d193b7d1c611f9246b4059026a1189a80e/propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d", size = 42190, upload-time = "2026-05-08T20:59:42.232Z" }, - { url = "https://files.pythonhosted.org/packages/57/93/f71588ad08b3e6f4b555b5ef215808a3c02b042d0151ad82fa6f15be677a/propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5", size = 38545, upload-time = "2026-05-08T20:59:44.087Z" }, { url = "https://files.pythonhosted.org/packages/e7/f1/8a8cc1c2c7e7934ab77e0163414f736fadbc0f5e8dd9673b952355ac175b/propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78", size = 90744, upload-time = "2026-05-08T20:59:45.799Z" }, { url = "https://files.pythonhosted.org/packages/c2/f4/651b1225e976bd1a2ba5cfba0c29d096581c2636b437e3a9a7ab6276270a/propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959", size = 52033, upload-time = "2026-05-08T20:59:47.408Z" }, { url = "https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7", size = 52754, upload-time = "2026-05-08T20:59:49.202Z" }, @@ -1846,6 +1684,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + [[package]] name = "pydantic-core" version = "2.46.4" @@ -1855,20 +1698,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, - { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, - { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, - { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, - { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, - { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, - { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, - { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, - { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, - { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, @@ -2065,12 +1894,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 = [ @@ -2082,7 +1909,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'" }, ] @@ -2170,11 +1996,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" }, @@ -2209,15 +2030,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" }, @@ -2560,7 +2372,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/f6/b1/8e7077a8641086aea449e1b5752a570f1b5906c64e0a33cd6d93b63a066b/uvicorn-0.47.0.tar.gz", hash = "sha256:7c9a0ea1a9414106bbab7324609c162d8fa0cdcdcb703060987269d77c7bb533", size = 90582, upload-time = "2026-05-14T18:16:54.455Z" } wheels = [ @@ -2584,12 +2395,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" }, @@ -2631,7 +2436,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/15/ba/1f6e8c957e4932be060dcdc482d339c12e0216351478add3645cdaa53c05/virtualenv-21.3.3.tar.gz", hash = "sha256:f5bda277e553b1c2b3c1a8debfc30496e1288cc93ce6b7b71b3280047e317328", size = 7613784, upload-time = "2026-05-13T18:01:30.19Z" } wheels = [ @@ -2653,9 +2457,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" }, @@ -2665,8 +2466,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" }, @@ -2688,18 +2487,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" }, @@ -2772,10 +2559,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" }, @@ -2788,15 +2571,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" }, @@ -2868,17 +2642,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" }, @@ -2954,7 +2717,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 = [ @@ -2972,24 +2734,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" }, @@ -3101,27 +2845,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.4" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/9f/65/34a6e6e4dfa260c4c55ee02bb2fc53625e126ff0181485286cf0c9d453d6/zope_interface-8.4.tar.gz", hash = "sha256:9dbee7925a23aa6349738892c911019d4095a96cff487b743482073ecbc174a8", size = 257736, upload-time = "2026-04-25T07:22:10.439Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/fb/cc696345b27909fe918a17f2b4deacee9bc8fd715ee04eb88c82677f1154/zope_interface-8.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:415de524326ddd61a78f0816f65942fa1aa35dced19e72579ad30dd106ce523e", size = 210401, upload-time = "2026-04-25T07:27:40.6Z" }, - { url = "https://files.pythonhosted.org/packages/17/be/7fcce2121df992061093a8060130d0152ddf5bb32942ed5dad09a6a0baac/zope_interface-8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fa0a26d5767087170b3da9ff503221d535ea266bf61b522d0afa2590fd05db0a", size = 210760, upload-time = "2026-04-25T07:27:42.943Z" }, - { url = "https://files.pythonhosted.org/packages/73/e9/4e3c564f9bd857abccd0839c22283ef7f85f3d09170c045a7cca42bb4988/zope_interface-8.4-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:8544081e32b515bbaf1c6339eef06b23ed470cf4876ff2f575803f82a744cc43", size = 254081, upload-time = "2026-04-25T07:27:45.392Z" }, - { url = "https://files.pythonhosted.org/packages/5d/e2/390d27ae877dc364980ab4d333a07a9766a7c9b69535fe095bd18d06dc7c/zope_interface-8.4-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:892b4b5350e58d6914858f58eb85d39fe9b992640ac6ece695f46c978046554d", size = 258944, upload-time = "2026-04-25T07:27:47.439Z" }, - { url = "https://files.pythonhosted.org/packages/60/d2/993428933830d364d7d710db3f850ee5f3a631a023e598316588da58c406/zope_interface-8.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d683267a6243526869cb69677dcfc663416d5f87904c1576ddec6e420687d51", size = 259556, upload-time = "2026-04-25T07:27:49.685Z" }, - { url = "https://files.pythonhosted.org/packages/29/82/0e4562eb9e7be8e82facbf1b70a777e9107e903448a81dc9c1246d152ab0/zope_interface-8.4-cp310-cp310-win_amd64.whl", hash = "sha256:f00fd65343d2a241a2b17688a12f5e815aa704ed64f9ca375de5f9e0ae9c9bda", size = 214238, upload-time = "2026-04-25T07:27:51.376Z" }, { url = "https://files.pythonhosted.org/packages/79/11/bd982648e1e62d7c06a56017fd88d1beea2ebc8d7a5972cce137e774aff2/zope_interface-8.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8b733af6e89a2b0b8edf5ff7a37988fe4e1788806e84e72127b88c47858f0da6", size = 210908, upload-time = "2026-04-25T07:27:53.363Z" }, { url = "https://files.pythonhosted.org/packages/2d/4f/fa87d3bd69d22b93fa5b968597a3dd0a297e44aa87e4611b0ca74c4aeec1/zope_interface-8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:265bad2df2ec070f23ff863249a89b408b11908fd4207662781fd18e3c6fc912", size = 211235, upload-time = "2026-04-25T07:27:55.392Z" }, { url = "https://files.pythonhosted.org/packages/eb/74/67379f7df4400ee45299c5200f17ec6c493e8a120ff4e5e9d26b09e32956/zope_interface-8.4-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:e195e76767847afb5379ffd67690c17d3c6efdab58dc0e477cf81ac94d5a5a15", size = 259918, upload-time = "2026-04-25T07:27:57.705Z" }, 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)