-
Notifications
You must be signed in to change notification settings - Fork 15
refactor!: Introduce fully typed clients #604
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #604 +/- ##
===========================================
+ Coverage 76.02% 95.16% +19.13%
===========================================
Files 42 44 +2
Lines 2482 4588 +2106
===========================================
+ Hits 1887 4366 +2479
+ Misses 595 222 -373
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| keys_signature: dict[str, str] | ||
|
|
||
|
|
||
| def crypto_random_object_id(length: int = 17) -> str: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest moving test utils to a standalone file to avoid importing from conftest, so that on one(including IDE helpers and AI) is tempted to import fixtures from conftest
| max_retries=8, | ||
| min_delay_between_retries_millis=500, # 0.5s | ||
| timeout_secs=360, # 6 mins | ||
| min_delay_between_retries=timedelta(milliseconds=500), # 0.5s |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| min_delay_between_retries=timedelta(milliseconds=500), # 0.5s | |
| min_delay_between_retries=timedelta(milliseconds=500), |
| min_delay_between_retries_millis=500, # 0.5s | ||
| timeout_secs=360, # 6 mins | ||
| min_delay_between_retries=timedelta(milliseconds=500), # 0.5s | ||
| timeout=timedelta(seconds=360), # 6 mins |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| timeout=timedelta(seconds=360), # 6 mins | |
| timeout=timedelta(seconds=360), |
| max_retries=8, | ||
| min_delay_between_retries_millis=500, # 0.5s | ||
| timeout_secs=360, # 6 mins | ||
| min_delay_between_retries=timedelta(milliseconds=500), # 0.5s |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| min_delay_between_retries=timedelta(milliseconds=500), # 0.5s | |
| min_delay_between_retries=timedelta(milliseconds=500), |
| min_delay_between_retries_millis=500, # 0.5s | ||
| timeout_secs=360, # 6 mins | ||
| min_delay_between_retries=timedelta(milliseconds=500), # 0.5s | ||
| timeout=timedelta(seconds=360), # 6 mins |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| timeout=timedelta(seconds=360), # 6 mins | |
| timeout=timedelta(seconds=360), |
| Subclasses should call `super().__init__()` and create their specific impit client using the `_headers` attribute. | ||
| """ | ||
|
|
||
| def __init__( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this change was lost
#601
| url = url.replace(self._base_url, self._public_base_url, 1) | ||
|
|
||
| if params: | ||
| filtered = {k: v for k, v in params.items() if v is not None} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We filter params here and also in _build_params. Shouldn't one reuse the other and filter just in one place?
| Raises: | ||
| ApifyApiError: If API returns errors other than 404. | ||
| """ | ||
| started_at = datetime.now(timezone.utc) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can just track the endtime which would be something or None
deadline = datetime.now(timezone.utc) + wait_duration
while ((deadline is None) or deadline>datetime.now()):
| if is_terminal or is_timed_out: | ||
| should_repeat = False | ||
|
|
||
| if not should_repeat: | ||
| # Early return here so that we avoid the sleep below if not needed | ||
| return actor_job |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not doing this directly?
if is_terminal or is_timed_out:
# Early return here so that we avoid the sleep below if not needed
break
| @property | ||
| def resource_id(self) -> str | None: | ||
| """Get the resource ID.""" | ||
| return self._resource_id | ||
|
|
||
| @property | ||
| def _resource_url(self) -> str: | ||
| """Build the full resource URL from base URL, path, and optional ID.""" | ||
| url = f'{self._base_url}/{self._resource_path}' | ||
| if self._resource_id is not None: | ||
| url = f'{url}/{to_safe_id(self._resource_id)}' | ||
| return url | ||
|
|
||
| @cached_property | ||
| def _base_client_kwargs(self) -> dict[str, Any]: | ||
| """Base kwargs for creating nested/child clients. | ||
|
|
||
| Returns dict with base_url, public_base_url, http_client, and client_registry. Caller adds | ||
| resource_path, resource_id, and params as needed. | ||
| """ | ||
| return { | ||
| 'base_url': self._resource_url, | ||
| 'public_base_url': self._public_base_url, | ||
| 'http_client': self._http_client, | ||
| 'client_registry': self._client_registry, | ||
| } | ||
|
|
||
| def _build_url( | ||
| self, | ||
| path: str | None = None, | ||
| *, | ||
| public: bool = False, | ||
| params: dict | None = None, | ||
| ) -> str: | ||
| """Build complete URL for API request. | ||
|
|
||
| Args: | ||
| path: Optional path segment to append (e.g., 'runs', 'items'). | ||
| public: Whether to use public CDN URL instead of API URL. | ||
| params: Optional query parameters to append. | ||
|
|
||
| Returns: | ||
| Complete URL with optional path and query string. | ||
| """ | ||
| url = f'{self._resource_url}/{path}' if path else self._resource_url | ||
|
|
||
| if public: | ||
| if not url.startswith(self._base_url): | ||
| raise ValueError(f'URL {url} does not start with base URL {self._base_url}') | ||
| url = url.replace(self._base_url, self._public_base_url, 1) | ||
|
|
||
| if params: | ||
| filtered = {k: v for k, v in params.items() if v is not None} | ||
| if filtered: | ||
| separator = '&' if '?' in url else '?' | ||
| url += separator + urlencode(filtered) | ||
|
|
||
| return url | ||
|
|
||
| def _build_params(self, **kwargs: Any) -> dict: | ||
| """Merge default params with method params, filtering out None values. | ||
|
|
||
| Args: | ||
| **kwargs: Method-specific parameters to merge. | ||
|
|
||
| Returns: | ||
| Merged parameters with None values removed. | ||
| """ | ||
| merged = {**self._default_params, **kwargs} | ||
| return {k: v for k, v in merged.items() if v is not None} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are the same for sync and async. They should share same implementation
| response_as_dict = response_to_dict(response) | ||
| return ListOfActorsResponse.model_validate(response_as_dict).data | ||
|
|
||
| def iterate( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'd better do this in a standalone change. This is complex on its own.
#556
That change suggests that each list method (sync or async) will be (sync/async)iterable on its own, so there is no need for an explicit iterate method.
# Same as before, potentially not all items
list_of_actors_response = ActorCollectionClient().list(...)
# New functionality, lazily fetched in chunks, can iterate over all items, doing multiple API calls if needed
individual_actors = [item for item in ActorCollectionClient().list(...)]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Same for all other list like responses)
| try: | ||
| response = self._http_client.call( | ||
| url=self._build_url(), | ||
| method='GET', | ||
| params=self._build_params(), | ||
| ) | ||
| result = response_to_dict(response) | ||
| return EnvVarResponse.model_validate(result).data | ||
| except ApifyApiError as exc: | ||
| catch_not_found_or_throw(exc) | ||
| return None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we keep the _get, _update and _delete methods on the ResourceClient(Async)?
class ResourceClient(...
def _get(self, timeout_secs: int | None = None) -> dict | None:
response = self._http_client.call(
url=self._build_url(),
method='GET',
params=self._build_params(),
)
return response_to_dict(response)
except ApifyApiError as exc:
catch_not_found_or_throw(exc)
return None
We could reuse most of it and just wrap it in something like this:
ActorEnvVarClient(...
def get(self) -> EnvVar | None:
return EnvVarResponse.model_validate(self._get()).data
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Same for all other clients)
| cleaned = filter_none_values(actor_env_var_representation) | ||
|
|
||
| return self._update(filter_out_none_values_recursively(actor_env_var_representation)) | ||
| response = self._http_client.call( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to https://github.com/apify/apify-client-python/pull/604/changes#r2783198077
Why not keep generic methods on the base class and just wrap them with the correct model in the children?
_list, _create, _get_or_create
| api_url = DEFAULT_API_URL if api_url is None else api_url | ||
| api_public_url = DEFAULT_API_URL if api_public_url is None else api_public_url | ||
|
|
||
| def _check_custom_headers(self, headers: dict) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another part of https://github.com/apify/apify-client-python/pull/604/changes#r2782803552 lost
| ) | ||
| """HTTP client used to communicate with the Apify API.""" | ||
|
|
||
| self._client_registry = ClientRegistry( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool. I can imagine this allowing custom hacks way more easily :-)
Maybe useful for testing as well.
| catch_not_found_or_throw(error_500) | ||
|
|
||
|
|
||
| def test_filter_none_values() -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please redo these tests with pytest.param. One assertion per simple test like this
@pytest.mark.parametrize(
('in', 'out'),
[
pytest.param({'a': 1, 'b': None, 'c': 3}, {'a': 1, 'c': 3}, id='Simple case'),
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment for other similar tests. Please parametrize where suitable
| self, | ||
| headers: dict[str, str] | None = None, | ||
| params: dict[str, Any] | None = None, | ||
| data: Any = None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we narrow down the type of data to str | bytes | bytearray | None?
Based on the method code, this seems valid.
| result = response_to_dict(response) | ||
| return ActorResponse.model_validate(result).data |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason why we don't use ActorResponse.model_validate_json(response.json()).data?
Similarly, in all places where response_to_dict is used
| default_run_options: Annotated[DefaultRunOptions | None, Field(alias='defaultRunOptions')] = None | ||
|
|
||
|
|
||
| class ActorPermissionLevel(str, Enum): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the rest of the code we are shifting from Enums to string Literals. Have you experimented with it here?
|
|
||
| # OpenAPI definition should be a dict with standard OpenAPI fields | ||
| # Note: May be None if the actor doesn't have an OpenAPI definition | ||
| if openapi_def is not None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We use specific actor that has it, right? so it should rather be:
assert openapi_def is not None
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Major refactor that replaces dict-based API responses with generated, fully typed Pydantic models across the client, plus a new impit-based HTTP layer.
Changes:
- Introduces new typed resource clients and response wrappers around generated Pydantic models.
- Replaces legacy HTTP client with new
SyncHttpClient/AsyncHttpClientand updates tests/docs accordingly. - Adds extensive new unit/integration coverage and model generation tooling configuration.
Reviewed changes
Copilot reviewed 100 out of 106 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/test_client_timeouts.py | Updates timeout tests to use new http clients + timedelta. |
| tests/unit/test_client_request_queue.py | Updates request-queue tests for typed responses and explicit API URLs. |
| tests/unit/test_client_headers.py | Removes legacy header tests (likely superseded by new HTTP layer). |
| tests/unit/test_client_errors.py | Switches error tests to new http clients and improves assertion naming. |
| tests/unit/test_actor_start_params.py | Adds unit tests ensuring timeout query param is used with typed client. |
| tests/unit/conftest.py | Removes patch_basic_url fixture; keeps httpserver fixture. |
| tests/unit/README.md | Documents unit-test isolation requirements. |
| tests/integration/test_webhook_dispatch.py | Adds unified sync/async integration coverage for webhook dispatch endpoints. |
| tests/integration/test_webhook.py | Adds unified sync/async integration coverage for webhooks. |
| tests/integration/test_user.py | Adds unified sync/async integration coverage for user endpoints (typed). |
| tests/integration/test_store.py | Reworks store tests into unified sync/async style with typed models. |
| tests/integration/test_schedule.py | Adds unified sync/async integration coverage for schedules. |
| tests/integration/test_run_collection.py | Removes legacy run-collection integration tests (dict-based). |
| tests/integration/test_log.py | Adds unified sync/async integration coverage for logs. |
| tests/integration/test_build.py | Adds unified sync/async integration coverage for builds. |
| tests/integration/test_basic.py | Removes legacy basic integration tests (dict-based). |
| tests/integration/test_apify_client.py | Adds unified basic integration test using typed models. |
| tests/integration/test_actor_version.py | Adds unified sync/async integration coverage for actor versions. |
| tests/integration/test_actor_env_var.py | Adds unified sync/async integration coverage for actor env vars. |
| tests/integration/test_actor.py | Adds unified sync/async integration coverage for actors. |
| tests/integration/integration_test_utils.py | Removes old integration utilities (now likely in conftest). |
| tests/integration/README.md | Documents integration tests’ requirement for real API tokens. |
| src/apify_client/errors.py | Improves error docstrings and hardens JSON error extraction. |
| src/apify_client/clients/resource_clients/run_collection.py | Removes legacy client implementation (dict-based). |
| src/apify_client/clients/resource_clients/build.py | Removes legacy build client implementation (dict-based). |
| src/apify_client/clients/resource_clients/actor_env_var_collection.py | Removes legacy env var collection client (dict-based). |
| src/apify_client/clients/base/resource_collection_client.py | Removes legacy base list/create abstraction (dict-based). |
| src/apify_client/clients/base/resource_client.py | Removes legacy base resource client abstraction (dict-based). |
| src/apify_client/clients/base/base_client.py | Removes legacy base client (replaced by new resource client base). |
| src/apify_client/clients/base/actor_job_base_client.py | Removes legacy polling/abort base (replaced elsewhere). |
| src/apify_client/clients/base/init.py | Removes legacy base exports. |
| src/apify_client/clients/init.py | Removes legacy large re-export module. |
| src/apify_client/_types.py | Removes old JSON/ListPage types (moved/replaced). |
| src/apify_client/_statistics.py | Renames Statistics -> ClientStatistics. |
| src/apify_client/_resource_clients/webhook_dispatch_collection.py | Converts to typed models + new base resource client. |
| src/apify_client/_resource_clients/webhook_dispatch.py | Converts get() to typed models and explicit 404 handling. |
| src/apify_client/_resource_clients/webhook_collection.py | Converts list/create to typed models and new representation helpers. |
| src/apify_client/_resource_clients/webhook.py | Converts get/update/delete/test/dispatches to typed models and new base. |
| src/apify_client/_resource_clients/user.py | Converts user endpoints to typed models and new base. |
| src/apify_client/_resource_clients/store_collection.py | Converts store listing to typed models and new base. |
| src/apify_client/_resource_clients/schedule_collection.py | Converts schedule list/create to typed models and new base. |
| src/apify_client/_resource_clients/schedule.py | Converts schedule get/update/delete/log to typed models and new base. |
| src/apify_client/_resource_clients/run_collection.py | Adds new typed run collection client with pagination iteration. |
| src/apify_client/_resource_clients/request_queue_collection.py | Converts list/get_or_create to typed models and new base. |
| src/apify_client/_resource_clients/log.py | Updates to new base client, typed run usage, and task lifecycle handling. |
| src/apify_client/_resource_clients/key_value_store_collection.py | Converts list/get_or_create to typed models and new base. |
| src/apify_client/_resource_clients/dataset_collection.py | Converts list/get_or_create to typed models and new base. |
| src/apify_client/_resource_clients/build_collection.py | Converts list to typed models and new base. |
| src/apify_client/_resource_clients/build.py | Adds new typed build client with log + wait_for_finish. |
| src/apify_client/_resource_clients/actor_version_collection.py | Converts list/create to typed models and new base. |
| src/apify_client/_resource_clients/actor_env_var_collection.py | Adds new typed actor env var collection client. |
| src/apify_client/_resource_clients/actor_env_var.py | Converts env var get/update/delete to typed models and new base. |
| src/apify_client/_resource_clients/init.py | Expands exports for log streaming/status watchers; updates resource exports. |
| src/apify_client/_logging.py | Refactors logging helpers, adds redirect logger utilities, updates context injection. |
| src/apify_client/_internal_models.py | Adds internal minimal Pydantic models for polling/validation (non-generated). |
| src/apify_client/_http_clients/_sync.py | Adds new synchronous impit-based HTTP client with retries/backoff. |
| src/apify_client/_http_clients/_base.py | Adds shared HTTP base: headers, param conversion, gzip JSON, timeout scaling. |
| src/apify_client/_http_clients/_async.py | Adds new async impit-based HTTP client with retries/backoff. |
| src/apify_client/_http_clients/init.py | Adds package entrypoint for new HTTP clients. |
| src/apify_client/_http_client.py | Removes legacy HTTP client implementation. |
| src/apify_client/_consts.py | Adds shared constants (timeouts, retries, terminal statuses, API URL). |
| src/apify_client/_client_registry.py | Adds sync/async registries for DI and avoiding circular imports. |
| src/apify_client/init.py | Switches public import to new _apify_client module. |
| scripts/utils.py | Improves docstring conversion and typing in scripts. |
| scripts/fix_async_docstrings.py | Skips _http_clients and handles missing sync class/methods. |
| scripts/check_async_docstrings.py | Skips _http_clients and handles missing sync classes. |
| pyproject.toml | Updates dependencies, adds datamodel-codegen config, fixes ruff ignore for generated models, bumps test concurrency, adds generate-models task. |
| docs/03_examples/code/03_retrieve_sync.py | Updates examples to attribute access for typed models. |
| docs/03_examples/code/03_retrieve_async.py | Updates examples to attribute access for typed models. |
| docs/03_examples/code/02_tasks_sync.py | Updates examples to typed Task/Run + attribute access. |
| docs/03_examples/code/02_tasks_async.py | Updates examples to typed Task/Run + attribute access. |
| docs/03_examples/code/01_input_sync.py | Updates timeout usage to timedelta. |
| docs/03_examples/code/01_input_async.py | Updates timeout usage to timedelta. |
| docs/02_concepts/code/05_retries_sync.py | Updates retry/timeout params to timedelta. |
| docs/02_concepts/code/05_retries_async.py | Updates retry/timeout params to timedelta. |
| docs/02_concepts/code/01_async_support.py | Updates example to attribute access for typed run id. |
| docs/01_overview/code/01_usage_sync.py | Updates example to attribute access for typed dataset id. |
| docs/01_overview/code/01_usage_async.py | Updates example to attribute access for typed dataset id. |
| .github/workflows/_tests.yaml | Increases CI test concurrency default to 16. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| from ._async import AsyncHttpClient | ||
| from ._sync import SyncHttpClient | ||
|
|
||
| __all__ = [ | ||
| 'HttpClient', | ||
| 'HttpClientAsync', | ||
| ] |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
__all__ exports names that are not defined in this module (HttpClient, HttpClientAsync). This breaks star-imports and is misleading for public API. Export the actual symbols (SyncHttpClient, AsyncHttpClient) and/or add explicit aliases if HttpClient / HttpClientAsync are intended public names.
| for attempt in range(1, max_retries + 1): | ||
| try: | ||
| return func(stop_retrying, attempt) | ||
| except Exception: | ||
| if not swallow: | ||
| raise | ||
|
|
||
| random_sleep_factor = random.uniform(1, 1 + random_factor) | ||
| backoff_base_secs = to_seconds(backoff_base) | ||
| backoff_exp_factor = backoff_factor ** (attempt - 1) | ||
|
|
||
| sleep_time_secs = random_sleep_factor * backoff_base_secs * backoff_exp_factor | ||
| time.sleep(sleep_time_secs) | ||
|
|
||
| return func(stop_retrying, max_retries + 1) |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_retry_with_exp_backoff() performs an extra attempt after the retry loop (max_retries + 1), so the total number of attempts becomes max_retries + 1, contradicting the max_retries contract/docstring. Remove the final call and instead re-raise the last exception (or track it) after the loop if all attempts fail.
| for attempt in range(1, max_retries + 1): | ||
| try: | ||
| return await func(stop_retrying, attempt) | ||
| except Exception: | ||
| if not swallow: | ||
| raise | ||
|
|
||
| random_sleep_factor = random.uniform(1, 1 + random_factor) | ||
| backoff_base_secs = to_seconds(backoff_base) | ||
| backoff_exp_factor = backoff_factor ** (attempt - 1) | ||
|
|
||
| sleep_time_secs = random_sleep_factor * backoff_base_secs * backoff_exp_factor | ||
| await asyncio.sleep(sleep_time_secs) | ||
|
|
||
| return await func(stop_retrying, max_retries + 1) |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same issue as sync version: this does an extra attempt after exhausting max_retries, making total attempts max_retries + 1. Remove the final attempt and re-raise the last failure once the loop completes.
| Formatted log message with colored logger name prefix. | ||
| """ | ||
| formatted_logger_name = f'{Fore.CYAN}[{record.name}]{Style.RESET_ALL}' | ||
| return f'{formatted_logger_name} -> {record.msg}' |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using record.msg drops %-style formatting and {}-style formatting that relies on record.args, so redirected logs can lose important details. Use record.getMessage() (or delegate to logging.Formatter.format(...)) so arguments are interpolated correctly.
| return f'{formatted_logger_name} -> {record.msg}' | |
| message = record.getMessage() | |
| return f'{formatted_logger_name} -> {message}' |
| for handler in to_logger.handlers: | ||
| to_logger.removeHandler(handler) | ||
| for log_filter in to_logger.filters: |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This mutates to_logger.handlers / to_logger.filters while iterating them, which can skip elements depending on list behavior. Iterate over a copy (e.g., for handler in list(to_logger.handlers): ...) or clear them via a while-loop to ensure all handlers/filters are removed.
| for handler in to_logger.handlers: | |
| to_logger.removeHandler(handler) | |
| for log_filter in to_logger.filters: | |
| for handler in list(to_logger.handlers): | |
| to_logger.removeHandler(handler) | |
| for log_filter in list(to_logger.filters): |
| # Try to parse as UserPrivateInfo first (has more fields), fall back to UserPublicInfo | ||
| try: | ||
| return PrivateUserDataResponse.model_validate(result).data | ||
| except Exception: | ||
| return PublicUserDataResponse.model_validate(result).data |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Catching Exception here is too broad and can hide unrelated bugs (e.g., unexpected runtime errors). Catch the specific Pydantic validation exception (typically pydantic.ValidationError) when falling back from private to public parsing.
| 'LogClient', | ||
| 'LogClientAsync', | ||
| 'LogClientAsync', |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
__all__ contains duplicate entries (LogClient, LogClientAsync). This is noisy and can confuse tooling/readers. Remove duplicates so each symbol appears exactly once.
| 'LogClient', | |
| 'LogClientAsync', | |
| 'LogClientAsync', | |
| 'LogClientAsync', |
| - production or test API tokens | ||
| - network access at all | ||
|
|
||
| Any test that calls the Apify API belong to integration tests. |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Grammatical fix: change 'belong' to 'belongs' (subject-verb agreement).
| Any test that calls the Apify API belong to integration tests. | |
| Any test that calls the Apify API belongs to integration tests. |
| api_url = httpserver.url_for('/').removesuffix('/') | ||
| client = ApifyClient(token='test_token', api_url=api_url) | ||
|
|
||
| # Call start with timeout_secs |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment says timeout_secs, but the call uses the new timeout parameter (timedelta). Update the comment to avoid confusion (same applies to the analogous async test comment).
| # Call start with timeout_secs | |
| # Call start with timeout (timedelta) parameter |
| async def test_batch_processed_partially_sync(httpserver: HTTPServer) -> None: | ||
| client = ApifyClient(token='placeholder_token') | ||
| server_url = httpserver.url_for('/').removesuffix('/') | ||
| client = ApifyClient( | ||
| token='placeholder_token', | ||
| api_url=server_url, | ||
| api_public_url=server_url, | ||
| ) |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test is declared async def but only exercises the sync client and contains no await. Consider making it a regular def test (or switch to ApifyClientAsync) so the test type matches the behavior and avoids unnecessary async test overhead.
Summary
This is a major refactoring that introduces fully typed Pydantic models throughout the client library. The models are generated from the OpenAPI specifications. All API responses now return typed objects instead of raw dictionaries.
This follows up on apify/apify-docs#2182.
Issues
Packages
Pydantic.apify-shared.Key changes
pyproject.tomlto generate Pydantic models based on the OpenAPI specs.Actor,Task,Run, etc.).apify/apify-sdk-pythonfor details - chore: Adapt to apify-client v3 [WIP] apify-sdk-python#719.Architecture
ClientRegistryto be able to achieve that (because of resource clients-siblings imports).Breaking changes
result['key']) to attribute-style (result.key).Test plan
Next steps