feat!: Introduce fully typed clients#604
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #604 +/- ##
===========================================
+ Coverage 76.18% 96.63% +20.44%
===========================================
Files 42 45 +3
Lines 2482 4274 +1792
===========================================
+ Hits 1891 4130 +2239
+ Misses 591 144 -447
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:
|
There was a problem hiding this comment.
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.
a745fa3 to
10fb739
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 101 out of 103 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (2)
src/apify_client/_logging.py:1
- In async streaming, checking
self._streaming_task.done()before raising RuntimeError prevents detecting when a task exists but has already completed. If a completed task exists and start() is called again, it will create a new task, potentially leading to resource leaks if the old task wasn't properly cleaned up. The condition should either always reject ifself._streaming_taskexists, or should setself._streaming_task = Noneafter completion.
from __future__ import annotations
src/apify_client/_logging.py:1
- Same issue as the streaming task - checking
self._logging_task.done()before raising RuntimeError means a completed logging task won't prevent start() from being called again, potentially creating resource leaks. The condition should either always reject ifself._logging_taskexists, or should setself._logging_task = Noneafter completion.
from __future__ import annotations
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Everything has been addressed. Feel free to add more feedback or reopen any conversations if you feel something wasn't properly resolved. |
Pijukatel
left a comment
There was a problem hiding this comment.
What is the release plan? Wait for the validator, merge it, and release a new major version?
janbuchar
left a comment
There was a problem hiding this comment.
Could this get an upgrading guide, too?
There was a problem hiding this comment.
Could this be split into multiple files? I know it's autogenerated, but still, there's a good chance someone will want to view it.
There was a problem hiding this comment.
Could this be split into multiple files?
I would say it can be either one module with all the classes or a subpackage with one file per class.
Would you rather prefer a _models subpackage with ~100 modules? I am not really sure. The current state is not that bad.
Merge this. Resolve all other issues for https://github.com/apify/apify-client-python/milestone/560 milestone. Make sure the SDK is adapted. Release the new client. Release the new SDK. |
I'll add it all at once at the end, once all issues from https://github.com/apify/apify-client-python/milestone/560 are resolved. So it can receive a proper review, and it is not released before the actual client release. |
f4c476b to
865ce87
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 102 out of 104 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
tests/unit/test_actor_start_params.py:1
- The
finishedAttimestamp (2019-12-12) is after thestartedAttimestamp (2019-11-30), but the Actor run has status RUNNING which is inconsistent. A running Actor should not have afinishedAtvalue. Either remove thefinishedAtfield or change the status to a terminal state like SUCCEEDED.
from __future__ import annotations
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Fix generated models (nullable finished_at, Literal[1] for actorSpecification, correct DATA_TRANSFER_EXTERNAL_GBYTES alias) and use proper response wrapper models in user client. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bind webhooks to a specific already-completed run (actor_run_id + is_ad_hoc) instead of to the actor (actor_id). A finished run won't emit new events, so the webhooks will never fire when other tests run the same actor. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The API returns null for finishedAt when a dispatch hasn't completed yet. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
I'm merging it. Other PRs will follow, so there will still be plenty of room for your future potential comments. |
## Summary Clean up the unreleased changelog section for the v3.0.0 release: - Rename header `2.5.1` → `3.0.0` and add the upgrade-guide link. - Drop the test-only fix entry (#729) — not user-facing. - Drop the `[**breaking**]` marker on the pluggable HTTP client entry (#641) — additive at the public API surface; default impit behavior unchanged. - Move "Drop support for Python 3.10" (#636) to ⚙️ Miscellaneous Tasks, matching the precedent of prior version-drop entries. - Move "Introduce fully typed clients" (#604) to 🚀 Features as the v3 headliner — closes the long-standing #21 / #481 type-annotation requests. - Order breaking changes first within each section.
### Description - This PR updates the SDK to work with `apify-python-client` v3, which introduces fully typed API clients generated from OpenAPI specifications. - See apify/apify-client-python#604 for more details. ### Issues - Closes: #736 - Closes: #770 - Closes: #697 - Closes: #853 ### Testing - The existing SDK tests pass with `apify-python-client` v3.
…nts (#954) The `status` and `origin` filters passed to `ActorClient.last_run(...)` and `TaskClient.last_run(...)` were silently dropped by the chained storage clients. For example, `actor.last_run(status='SUCCEEDED').dataset().list_items()` queried the most recent run's dataset regardless of status. This is a regression vs 1.x, where `_sub_resource_init_options` forwarded the parent's params to every child client; the typed-client refactor (#604) lost that. The fix passes the run client's default params to its four child-client factories (`dataset()`, `key_value_store()`, `request_queue()`, `log()`) in both `RunClient` and `RunClientAsync`, so the filters ride along on the `runs/last` storage endpoints again, matching 1.x and the JS client. Adds a parametrized regression test covering all four chained clients, sync and async. All 8 cases fail before the fix and pass after.
* feat: Add request body compression with optional brotli (apify#927) ## Description - https://github.com/apify/apify-core/pull/28971 added support for brotli compression to BE. - Pros: higher compression, cons: more CPU intensive. - The brotli dependencies are defined as optional, one has to explicitly enable it and choose one. - JS client doesn't compress requests that are too small, this Python client compresses just everything. No change done, I only noticed and stating it. ## Issues Closes: apify#942 ## Related PRs - https://github.com/apify/apify-core/pull/28971 - apify/apify-docs#2750 - apify/apify-client-js#962 - apify#927 - apify/apify-sdk-python#1031 --------- Co-authored-by: Vlada Dusek <v.dusek96@gmail.com> * chore(release): Update changelog and package version [skip ci] * chore: Automatic docs theme update [skip ci] * test: Deflake test_schedule_list and test_task_list by polling eventually consistent listings (apify#951) `test_schedule_list` and `test_task_list` failed in CI ([schedule run](https://github.com/apify/apify-client-python/actions/runs/29497290807/job/87617167353), [task run](https://github.com/apify/apify-client-python/actions/runs/29498108478/job/87619855853)) because they assert read-your-write on listing endpoints: they list resources immediately after creating them, and under load the listing can serve a view that hasn't yet caught up with the creates, so the fresh IDs are sometimes missing. The creates themselves succeeded in both cases, so these are eventual-consistency flakes, not client bugs. The fix wraps each list read in the existing `poll_until_condition` helper (30 s ceiling), waiting until the created IDs appear in the listing. The original assertions still run on the final page, so a real regression still fails. Follows the same deflaking pattern as apify#824, apify#831, apify#844, and apify#868. * docs: fix input guide example to bound the wait with wait_duration (apify#955) The "Pass input to an Actor" guide example claimed that `timeout=timedelta(seconds=60)` makes `call()` wait up to 60 seconds for the run to finish. It doesn't. On `call()`, `timeout` is the per-request HTTP timeout forwarded to `start()`, while the wait cap is `wait_duration` (default `None`, i.e. wait indefinitely). A user copying the example would block indefinitely on a long or never-finishing run. The sync and async examples now use `wait_duration=timedelta(seconds=60)` and the comment is reworded accordingly. The stale copies under `website/versioned_docs/` are left untouched on purpose. The versioning workflow deletes and re-snapshots `version-3.0` from `docs/` on every 3.x release, so the fix propagates there automatically. * chore: Automatic docs theme update [skip ci] * fix: offload async request body compression to a worker thread (apify#950) The async `ImpitHttpClientAsync.call()` serialized and compressed request bodies inline on the event loop. Compression is CPU-bound (and brotli is becoming the default via the Apify SDK), so it blocked the loop for the whole duration of the compression, stalling every other concurrent task (response reads, the platform events websocket, other in-flight requests). Request preparation is now offloaded to a worker thread via `asyncio.to_thread` whenever there is a body to compress. Bodyless requests (the common polling, listing, and get path) stay inline to avoid a needless thread-dispatch hop. The synchronous client is unchanged, as it has no event loop to block. * chore(release): Update changelog and package version [skip ci] * fix: Propagate last_run status/origin filters to chained storage clients (apify#954) The `status` and `origin` filters passed to `ActorClient.last_run(...)` and `TaskClient.last_run(...)` were silently dropped by the chained storage clients. For example, `actor.last_run(status='SUCCEEDED').dataset().list_items()` queried the most recent run's dataset regardless of status. This is a regression vs 1.x, where `_sub_resource_init_options` forwarded the parent's params to every child client; the typed-client refactor (apify#604) lost that. The fix passes the run client's default params to its four child-client factories (`dataset()`, `key_value_store()`, `request_queue()`, `log()`) in both `RunClient` and `RunClientAsync`, so the filters ride along on the `runs/last` storage endpoints again, matching 1.x and the JS client. Adds a parametrized regression test covering all four chained clients, sync and async. All 8 cases fail before the fix and pass after. * chore(release): Update changelog and package version [skip ci] * chore: Automatic docs theme update [skip ci] * fix: Propagate API token to custom HTTP clients (apify#956) `ApifyClient.with_custom_http_client(token=...)` (and the async twin) stored the token on the `ApifyClient` instance but never passed it to the injected HTTP client, so no request carried an `Authorization` header and every call failed with 401. The documented custom HTTP client example inherited the bug. - `with_custom_http_client` now sets `Authorization: Bearer <token>` on the injected client's default headers, unless the client already has an auth header configured (checked case-insensitively). - `HttpClientBase._prepare_request_call` now merges the client's default headers under the per-request headers, so any custom client using the helper (including a pre-built `ImpitHttpClient` passed as the custom client) actually sends them. For the default client the wire behavior is unchanged, since impit request-level headers replace the identical client-level ones. - The HTTPX guide examples now merge `self._headers` before delegating, and the `HttpClient` ABC docstring states that implementations must send the default headers with every request. Regression tests cover the token reaching the wire (custom sync/async clients and a pre-built tokenless `ImpitHttpClient`) and the no-clobber semantics for client-configured auth headers. * chore(release): Update changelog and package version [skip ci] * fix: Make batch_add_requests split batches by serialized payload size (apify#953) The 9 MB payload guard in `batch_add_requests` was inert: `constrained_batches` was called without `get_len`, so the default `len()` measured each request dict's key count (~4) instead of its serialized size. Batches were therefore split only by the 25-request count limit, and large requests shipped as one oversized POST that the API rejects with 413, failing the whole call. The guard now measures each request as its UTF-8 JSON byte length, using the same serialization flags as the HTTP client's request body path. It also passes `strict=False`, which preserves the previous contract for an individually oversized request: it's sent in its own batch and left for the API to judge, instead of raising a client-side `ValueError`. Both the size-based splitting and the oversized-singleton path are covered by new sync/async regression tests. *✍️ Drafted by Claude Code* * chore(release): Update changelog and package version [skip ci] * docs: Fix documentation mismatches with actual client behavior (apify#958) Fixes 12 documentation issues found by an engineering audit, where docs, examples, or docstrings contradict what the client actually does: - The custom HTTP client guide example (`HttpxClient`) now raises `ApifyApiError` for error responses instead of returning them raw, and the guide documents this part of the `call` contract (resource clients rely on it, e.g. to translate a 404 into a `None` return value). - The streaming concepts page no longer claims all three streaming methods yield a raw `impit.Response` — it now describes the actual yielded value per method (`stream_record` yields a `dict` with the response under `value`, `stream` and `stream_record` may yield `None`). - The pagination concepts page no longer lists `ListOfRequests` among page models exposing `total`/`offset`/`count`; it now explains its cursor-based pagination via `next_cursor`. - The logging formatter example no longer references `%(status_code)s` (absent on most records, causing logging errors) and no longer attaches a duplicate handler; the page notes which properties are present on every record. - The upgrading-to-v3 guide cross-links now include the site baseUrl (`/api/client/python/...`), fixing 6 links that 404ed on the published site. - The conda instruction for the brotli extra installs `brotli-python` (the Python bindings) instead of `brotli` (the C library). - `RunClient.resurrect` docstrings cite the real `SUCCEEDED` status instead of the nonexistent `FINISHED`. - `wait_for_finish` docstrings in `RunClient` and `BuildClient` spell the terminal status as `TIMED-OUT` (the real literal) instead of `TIMED_OUT`. - The quick-start page refers to the `Run` model's `default_dataset_id` attribute instead of the v2-era run dictionary with `defaultDatasetId`. - The README dataset example passes `fields` as `list[str]` per the signature instead of a comma-separated string. - The README quick-start examples handle the `Run | None` return of `call()` instead of accessing attributes on a possible `None`. - The timeouts concepts page states that `no_timeout` is capped at 24 hours by the default client instead of claiming it disables the timeout entirely. *✍️ Drafted by Claude Code* * chore(release): Update changelog and package version [skip ci] * docs: Version docs for v3.1.0 [skip ci] * chore: Automatic docs theme update [skip ci] * chore: Automatic docs theme update [skip ci] * fix: Add missing cannot-monetize-without-payout-billing-info error code (apify#960) - Updates the auto-generated Pydantic models and TypedDicts based on the proposed OpenAPI specification changes. - Based on apify-docs PR [#2785](apify/apify-docs#2785). * chore(release): Update changelog and package version [skip ci] * chore(deps): update pnpm to v11.15.1 (apify#967) * chore(deps): update dependency oxfmt to ^0.59.0 (apify#966) * chore(deps): lock file maintenance (apify#968) * fix: Normalize query params in dataset create_items_public_url (apify#963) `DatasetClient.create_items_public_url` (and its async twin) passed `_build_params` output straight to `urlencode`, bypassing the `_parse_params` normalization that the real HTTP request path applies. As a result, boolean and list query params ended up as Python reprs in the signed URL — e.g. `create_items_public_url(clean=True, fields=['title', 'url'])` produced `clean=True&fields=%5B%27title%27%2C+%27url%27%5D` instead of `clean=true&fields=title,url`. Consumers of the shared URL then got unclean/unfiltered items or an API error. The fix routes the params through `self._http_client._parse_params(...)` before `urlencode`, so the public URL matches exactly what an actual API request would send (bool→`true`/`false`, list→comma-joined, `None` dropped). Added regression tests for both the sync and async clients. *✍️ Drafted by Claude Code* * chore(release): Update changelog and package version [skip ci] * chore(deps): update actions/setup-node action to v7 (apify#969) * chore(deps): update actions/setup-python action to v7 (apify#970) * docs: Remove conda special instructions for brotli (apify#959) It is now included by default: https://github.com/conda-forge/apify-client-feedstock/pull/4/changes * docs: Update description of `request_url` in `Webhooks` (apify#974) - Updates the auto-generated Pydantic models and TypedDicts based on the proposed OpenAPI specification changes. - Based on apify-docs PR [#2794](apify/apify-docs#2794). --------- Co-authored-by: Michal Turek <michal.turek@apify.com> Co-authored-by: Vlada Dusek <v.dusek96@gmail.com> Co-authored-by: Apify Service Account <64261774+apify-service-account@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Josef Procházka <josef.prochazka@apify.com>
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 - refactor!: Adapt to apify-client v3 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