Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/_tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
operating_systems: '["ubuntu-latest", "windows-latest"]'
python_version_for_codecov: "3.14"
operating_system_for_codecov: ubuntu-latest
tests_concurrency: "1"
tests_concurrency: "16"

integration_tests:
name: Integration tests
Expand All @@ -31,4 +31,4 @@ jobs:
operating_systems: '["ubuntu-latest"]'
python_version_for_codecov: "3.14"
operating_system_for_codecov: ubuntu-latest
tests_concurrency: "1"
tests_concurrency: "16"
2 changes: 1 addition & 1 deletion docs/01_overview/code/01_usage_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ async def main() -> None:
return

# Fetch results from the Actor run's default dataset.
dataset_client = apify_client.dataset(call_result['defaultDatasetId'])
dataset_client = apify_client.dataset(call_result.default_dataset_id)
list_items_result = await dataset_client.list_items()
print(f'Dataset: {list_items_result}')
2 changes: 1 addition & 1 deletion docs/01_overview/code/01_usage_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ def main() -> None:
return

# Fetch results from the Actor run's default dataset.
dataset_client = apify_client.dataset(call_result['defaultDatasetId'])
dataset_client = apify_client.dataset(call_result.default_dataset_id)
list_items_result = dataset_client.list_items()
print(f'Dataset: {list_items_result}')
2 changes: 1 addition & 1 deletion docs/02_concepts/code/01_async_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ async def main() -> None:

# Start the Actor and get the run ID
run_result = await actor_client.start()
run_client = apify_client.run(run_result['id'])
run_client = apify_client.run(run_result.id)
log_client = run_client.log()

# Stream the logs
Expand Down
6 changes: 4 additions & 2 deletions docs/02_concepts/code/05_retries_async.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from datetime import timedelta

from apify_client import ApifyClientAsync

TOKEN = 'MY-APIFY-TOKEN'
Expand All @@ -7,6 +9,6 @@ async def main() -> None:
apify_client = ApifyClientAsync(
token=TOKEN,
max_retries=8,
min_delay_between_retries_millis=500, # 0.5s
timeout_secs=360, # 6 mins
min_delay_between_retries=timedelta(milliseconds=500),
timeout=timedelta(seconds=360),
)
6 changes: 4 additions & 2 deletions docs/02_concepts/code/05_retries_sync.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from datetime import timedelta

from apify_client import ApifyClient

TOKEN = 'MY-APIFY-TOKEN'
Expand All @@ -7,6 +9,6 @@ async def main() -> None:
apify_client = ApifyClient(
token=TOKEN,
max_retries=8,
min_delay_between_retries_millis=500, # 0.5s
timeout_secs=360, # 6 mins
min_delay_between_retries=timedelta(milliseconds=500),
timeout=timedelta(seconds=360),
)
5 changes: 4 additions & 1 deletion docs/03_examples/code/01_input_async.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
from datetime import timedelta

from apify_client import ApifyClientAsync

Expand All @@ -16,7 +17,9 @@ async def main() -> None:

# Run the Actor and wait for it to finish up to 60 seconds.
# Input is not persisted for next runs.
run_result = await actor_client.call(run_input=input_data, timeout_secs=60)
run_result = await actor_client.call(
run_input=input_data, timeout=timedelta(seconds=60)
)


if __name__ == '__main__':
Expand Down
4 changes: 3 additions & 1 deletion docs/03_examples/code/01_input_sync.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from datetime import timedelta

from apify_client import ApifyClient

TOKEN = 'MY-APIFY-TOKEN'
Expand All @@ -14,7 +16,7 @@ def main() -> None:

# Run the Actor and wait for it to finish up to 60 seconds.
# Input is not persisted for next runs.
run_result = actor_client.call(run_input=input_data, timeout_secs=60)
run_result = actor_client.call(run_input=input_data, timeout=timedelta(seconds=60))


if __name__ == '__main__':
Expand Down
12 changes: 6 additions & 6 deletions docs/03_examples/code/02_tasks_async.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import asyncio

from apify_client import ApifyClientAsync
from apify_client.clients.resource_clients import TaskClientAsync
from apify_client._models import Run, Task
from apify_client._resource_clients import TaskClientAsync

TOKEN = 'MY-APIFY-TOKEN'
HASHTAGS = ['zebra', 'lion', 'hippo']


async def run_apify_task(client: TaskClientAsync) -> dict:
result = await client.call()
return result or {}
async def run_apify_task(client: TaskClientAsync) -> Run | None:
return await client.call()


async def main() -> None:
apify_client = ApifyClientAsync(token=TOKEN)

# Create Apify tasks
apify_tasks = list[dict]()
apify_tasks = list[Task]()
apify_tasks_client = apify_client.tasks()

for hashtag in HASHTAGS:
Expand All @@ -34,7 +34,7 @@ async def main() -> None:
apify_task_clients = list[TaskClientAsync]()

for apify_task in apify_tasks:
task_id = apify_task['id']
task_id = apify_task.id
apify_task_client = apify_client.task(task_id)
apify_task_clients.append(apify_task_client)

Expand Down
17 changes: 9 additions & 8 deletions docs/03_examples/code/02_tasks_sync.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
from apify_client import ApifyClient
from apify_client.clients.resource_clients import TaskClient
from apify_client._models import Run, Task
from apify_client._resource_clients import TaskClient

TOKEN = 'MY-APIFY-TOKEN'
HASHTAGS = ['zebra', 'lion', 'hippo']


def run_apify_task(client: TaskClient) -> dict:
result = client.call()
return result or {}
def run_apify_task(client: TaskClient) -> Run | None:
return client.call()


def main() -> None:
apify_client = ApifyClient(token=TOKEN)

# Create Apify tasks
apify_tasks = list[dict]()
apify_tasks = list[Task]()
apify_tasks_client = apify_client.tasks()

for hashtag in HASHTAGS:
Expand All @@ -32,18 +32,19 @@ def main() -> None:
apify_task_clients = list[TaskClient]()

for apify_task in apify_tasks:
task_id = apify_task['id']
task_id = apify_task.id
apify_task_client = apify_client.task(task_id)
apify_task_clients.append(apify_task_client)

print('Task clients created:', apify_task_clients)

# Execute Apify tasks
task_run_results = list[dict]()
task_run_results = list[Run]()

for client in apify_task_clients:
result = run_apify_task(client)
task_run_results.append(result)
if result is not None:
task_run_results.append(result)

print('Task results:', task_run_results)

Expand Down
4 changes: 2 additions & 2 deletions docs/03_examples/code/03_retrieve_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ async def main() -> None:

for dataset_item in actor_datasets.items:
# Dataset items can be handled here. Dataset items can be paginated
dataset_client = apify_client.dataset(dataset_item['id'])
dataset_client = apify_client.dataset(dataset_item.id)
dataset_items = await dataset_client.list_items(limit=1000)

# Items can be pushed to single dataset
merging_dataset_client = apify_client.dataset(merging_dataset['id'])
merging_dataset_client = apify_client.dataset(merging_dataset.id)
await merging_dataset_client.push_items(dataset_items.items)

# ...
Expand Down
4 changes: 2 additions & 2 deletions docs/03_examples/code/03_retrieve_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ def main() -> None:

for dataset_item in actor_datasets.items:
# Dataset items can be handled here. Dataset items can be paginated
dataset_client = apify_client.dataset(dataset_item['id'])
dataset_client = apify_client.dataset(dataset_item.id)
dataset_items = dataset_client.list_items(limit=1000)

# Items can be pushed to single dataset
merging_dataset_client = apify_client.dataset(merging_dataset['id'])
merging_dataset_client = apify_client.dataset(merging_dataset.id)
merging_dataset_client.push_items(dataset_items.items)

# ...
Expand Down
49 changes: 38 additions & 11 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ classifiers = [
]
keywords = ["apify", "api", "client", "automation", "crawling", "scraping"]
dependencies = [
"apify-shared>=2.1.0,<3.0.0",
"colorama>=0.4.0",
"impit>=0.9.2",
"more_itertools>=10.0.0",
"pydantic[email]>=2.11.0",
]

[project.urls]
Expand All @@ -47,22 +47,23 @@ dev = [
# See https://github.com/apify/apify-client-python/pull/582/ for more details.
# We explicitly constrain black>=24.3.0 to override the transitive dependency.
"black>=24.3.0",
"datamodel-code-generator[http,ruff]<1.0.0",
"dycw-pytest-only<3.0.0",
"griffe",
"poethepoet<1.0.0",
"pre-commit<5.0.0",
"pydoc-markdown<5.0.0",
"pytest-asyncio<2.0.0",
"pytest-cov<8.0.0",
"pytest-httpserver<2.0.0",
"pytest-timeout<3.0.0",
"pytest-xdist<4.0.0",
"pytest<9.0.0",
"pytest-httpserver<2.0.0",
"redbaron<1.0.0",
"ruff~=0.15.0",
"setuptools", # setuptools are used by pytest but not explicitly required
"ty~=0.0.0",
"types-colorama<0.5.0",
"ty~=0.0.0",
"werkzeug<4.0.0", # Werkzeug is used by pytest-httpserver
]

Expand Down Expand Up @@ -144,6 +145,12 @@ indent-style = "space"
"N999", # Invalid module name
"T201", # print found
]
"src/apify_client/_models.py" = [
"D", # Everything from the pydocstyle
"E501", # Line too long
"ERA001", # Commented-out code
"TC003", # Move standard library import into a type-checking block
]

[tool.ruff.lint.flake8-quotes]
docstring-quotes = "double"
Expand Down Expand Up @@ -171,10 +178,7 @@ python-version = "3.10"
include = ["src", "tests", "scripts", "docs", "website"]

[[tool.ty.overrides]]
include = [
"docs/**/*.py",
"website/**/*.py",
]
include = ["docs/**/*.py", "website/**/*.py"]

[tool.ty.overrides.rules]
unresolved-import = "ignore"
Expand All @@ -185,17 +189,37 @@ exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:", "assert_never()"]
[tool.ipdb]
context = 7

# https://koxudaxi.github.io/datamodel-code-generator/
[tool.datamodel-codegen]
url = "https://docs.apify.com/api/openapi.json"
input_file_type = "openapi"
output = "src/apify_client/_models.py"
target_python_version = "3.10"
output_model_type = "pydantic_v2.BaseModel"
use_schema_description = true
use_field_description = true
use_union_operator = true
capitalise_enum_members = true
collapse_root_models = true
set_default_enum_member = true
use_annotated = true
wrap_string_literal = true
snake_case_field = true
use_subclass_enum = true
extra_fields = "allow"
formatters = ["ruff-check", "ruff-format"]

# Run tasks with: uv run poe <task>
[tool.poe.tasks]
clean = "rm -rf .coverage .pytest_cache .ruff_cache .ty_cache build dist htmlcov"
install-sync = "uv sync --all-extras"
build = "uv build"
publish-to-pypi = "uv publish --token ${APIFY_PYPI_TOKEN_CRAWLEE}"
type-check = "uv run ty check"
unit-tests = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-auto} tests/unit"
unit-tests-cov = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-auto} --cov=src/apify_client --cov-report=xml:coverage-unit.xml tests/unit"
integration-tests = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-auto} tests/integration"
integration-tests-cov = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-auto} --cov=src/apify_client --cov-report=xml:coverage-integration.xml tests/integration"
unit-tests = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-16} tests/unit"
unit-tests-cov = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-16} --cov=src/apify_client --cov-report=xml:coverage-unit.xml tests/unit"
integration-tests = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-16} tests/integration"
integration-tests-cov = "uv run pytest --numprocesses=${TESTS_CONCURRENCY:-16} --cov=src/apify_client --cov-report=xml:coverage-integration.xml tests/integration"
check-async-docstrings = "uv run python scripts/check_async_docstrings.py"
fix-async-docstrings = "uv run python scripts/fix_async_docstrings.py"
check-code = ["lint", "type-check", "unit-tests", "check-async-docstrings"]
Expand All @@ -220,3 +244,6 @@ cwd = "website"
[tool.poe.tasks.run-docs]
shell = "./build_api_reference.sh && corepack enable && yarn && uv run yarn start"
cwd = "website"

[tool.poe.tasks.generate-models]
shell = "uv run datamodel-codegen"
6 changes: 4 additions & 2 deletions scripts/fix_async_docstrings.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@

# Find all classes which end with "ClientAsync" (there should be at most 1 per file)
async_class = red.find('ClassNode', name=re.compile('.*ClientAsync$'))
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After removing the check for async_class existence (line 20-21 in the old code), the code now assumes async_class is never None. If no async class is found in a file, this will cause AttributeError when accessing async_class.name on line 22. The check should be restored, or a comment should explain why it's guaranteed to exist.

Suggested change
async_class = red.find('ClassNode', name=re.compile('.*ClientAsync$'))
async_class = red.find('ClassNode', name=re.compile('.*ClientAsync$'))
if async_class is None:
# No async client class in this file, nothing to fix
continue

Copilot uses AI. Check for mistakes.
if not async_class:
continue

# Find the corresponding sync classes (same name, but without -Async)
sync_class = red.find('ClassNode', name=async_class.name.replace('ClientAsync', 'Client'))
Expand All @@ -32,6 +30,10 @@
if len(async_method.decorators) and str(async_method.decorators[0].value) == 'ignore_docs':
continue

# Skip methods that don't exist in the sync class
if sync_method is None:
continue

# If the sync method has a docstring, copy it to the async method (with adjustments)
if isinstance(sync_method.value[0].value, str):
sync_docstring = sync_method.value[0].value
Expand Down
16 changes: 12 additions & 4 deletions scripts/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def get_current_package_version() -> str:
# It replaces the version number on the line with the format `version = "1.2.3"`
def set_current_package_version(version: str) -> None:
with open(PYPROJECT_TOML_FILE_PATH, 'r+', encoding='utf-8') as pyproject_toml_file:
updated_pyproject_toml_file_lines = []
updated_pyproject_toml_file_lines = list[str]()
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This creates a type object list[str], not an empty list instance. This should be updated_pyproject_toml_file_lines = [] or updated_pyproject_toml_file_lines: list[str] = []. The code will fail when trying to call .append() on this type object.

Suggested change
updated_pyproject_toml_file_lines = list[str]()
updated_pyproject_toml_file_lines: list[str] = []

Copilot uses AI. Check for mistakes.
version_string_found = False
for line in pyproject_toml_file:
line_processed = line
Expand All @@ -45,7 +45,15 @@ def set_current_package_version(version: str) -> None:
# Generate convert a docstring from a sync resource client method
# into a doctring for its async resource client analogue
def sync_to_async_docstring(docstring: str) -> str:
substitutions = [(r'Client', r'ClientAsync')]
substitutions = [
(r'Client', r'ClientAsync'),
(r'\bsynchronously\b', r'asynchronously'),
(r'\bSynchronously\b', r'Asynchronously'),
(r'\bsynchronous\b', r'asynchronous'),
(r'\bSynchronous\b', r'Asynchronous'),
(r'Retry a function', r'Retry an async function'),
(r'Function to retry', r'Async function to retry'),
]
res = docstring
for pattern, replacement in substitutions:
res = re.sub(pattern, replacement, res, flags=re.MULTILINE)
Expand All @@ -59,8 +67,8 @@ def get_published_package_versions() -> list:
package_data = json.load(urlopen(package_info_url)) # noqa: S310
published_versions = list(package_data['releases'].keys())
# If the URL returns 404, it means the package has no releases yet (which is okay in our case)
except HTTPError as e:
if e.code != 404:
except HTTPError as exc:
if exc.code != 404:
raise
published_versions = []
return published_versions
2 changes: 1 addition & 1 deletion src/apify_client/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from importlib import metadata

from .client import ApifyClient, ApifyClientAsync
from ._apify_client import ApifyClient, ApifyClientAsync

__version__ = metadata.version('apify-client')

Expand Down
Loading