Skip to content
Draft
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/_check_code.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ jobs:
name: Lint check
uses: apify/workflows/.github/workflows/python_lint_check.yaml@main
with:
python_versions: '["3.10", "3.11", "3.12", "3.13", "3.14"]'
python_versions: '["3.11", "3.12", "3.13", "3.14"]'

type_check:
name: Type check
uses: apify/workflows/.github/workflows/python_type_check.yaml@main
with:
python_versions: '["3.10", "3.11", "3.12", "3.13", "3.14"]'
python_versions: '["3.11", "3.12", "3.13", "3.14"]'
6 changes: 3 additions & 3 deletions .github/workflows/_tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
uses: apify/workflows/.github/workflows/python_unit_tests.yaml@main
secrets: inherit
with:
python_versions: '["3.10", "3.11", "3.12", "3.13", "3.14"]'
python_versions: '["3.11", "3.12", "3.13", "3.14"]'
operating_systems: '["ubuntu-latest", "windows-latest"]'
python_version_for_codecov: "3.14"
operating_system_for_codecov: ubuntu-latest
Expand All @@ -34,7 +34,7 @@ jobs:
strategy:
matrix:
os: ["ubuntu-latest"]
python-version: ["3.10", "3.14"]
python-version: ["3.11", "3.14"]

runs-on: ${{ matrix.os }}

Expand Down Expand Up @@ -91,7 +91,7 @@ jobs:
strategy:
matrix:
os: ["ubuntu-latest"]
python-version: ["3.10", "3.14"]
python-version: ["3.11", "3.14"]

runs-on: ${{ matrix.os }}

Expand Down
4 changes: 2 additions & 2 deletions docs/02_concepts/code/07_webhook.py
Original file line number Diff line number Diff line change
@@ -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',
)

Expand Down
4 changes: 2 additions & 2 deletions docs/02_concepts/code/07_webhook_preventing.py
Original file line number Diff line number Diff line change
@@ -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',
)

Expand Down
12 changes: 5 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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 @ git+https://github.com/apify/apify-client-python.git@master",
"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",
Expand Down Expand Up @@ -188,7 +186,7 @@ builtins-ignorelist = ["id"]

[tool.ruff.lint.isort]
known-local-folder = ["apify"]
known-first-party = ["apify_client", "apify_shared", "crawlee"]
known-first-party = ["apify_client", "crawlee"]

[tool.ruff.lint.pylint]
max-branches = 18
Expand All @@ -200,7 +198,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"]
Expand Down
2 changes: 1 addition & 1 deletion src/apify/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from importlib import metadata

from apify_shared.consts import WebhookEventType
from apify_client._models import WebhookEventType
from crawlee import Request
from crawlee.events import (
Event,
Expand Down
105 changes: 65 additions & 40 deletions src/apify/_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -13,7 +13,6 @@
from pydantic import AliasChoices

from apify_client import ApifyClientAsync
from apify_shared.consts import ActorEnvVars, ActorExitCodes, ApifyEnvVars
from crawlee import service_locator
from crawlee.errors import ServiceConflictError
from crawlee.events import (
Expand All @@ -28,9 +27,8 @@

from apify._charging import DEFAULT_DATASET_ITEM_EVENT, ChargeResult, ChargingManager, ChargingManagerImplementation
from apify._configuration import Configuration
from apify._consts import EVENT_LISTENERS_TIMEOUT
from apify._consts import EVENT_LISTENERS_TIMEOUT, ActorEnvVars, ActorExitCodes, ApifyEnvVars
from apify._crypto import decrypt_input_secrets, load_private_key
from apify._models import ActorRun
from apify._proxy_configuration import ProxyConfiguration
from apify._utils import docs_group, docs_name, ensure_context, get_system_info, is_running_in_ipython
from apify.events import ApifyEventManager, EventManager, LocalEventManager
Expand All @@ -43,9 +41,9 @@
import logging
from collections.abc import Callable
from types import TracebackType
from typing import Self

from typing_extensions import Self

from apify_client._models import Run
from crawlee._types import JsonSerializable
from crawlee.proxy_configuration import _NewUrlFunction

Expand Down Expand Up @@ -506,17 +504,21 @@ def new_client(
(increases exponentially from this value).
timeout: The socket timeout of the HTTP requests sent to the Apify API.
"""
token = token or self.configuration.token
api_url = api_url or self.configuration.api_base_url
return ApifyClientAsync(
token=token,
api_url=api_url,
max_retries=max_retries,
min_delay_between_retries_millis=int(min_delay_between_retries.total_seconds() * 1000)
if min_delay_between_retries is not None
else None,
timeout_secs=int(timeout.total_seconds()) if timeout else None,
)
kwargs: dict[str, Any] = {
'token': token or self.configuration.token,
'api_url': api_url or self.configuration.api_base_url,
}

if max_retries is not None:
kwargs['max_retries'] = max_retries

if min_delay_between_retries is not None:
kwargs['min_delay_between_retries_millis'] = int(min_delay_between_retries.total_seconds() * 1000)

if timeout is not None:
kwargs['timeout_secs'] = int(timeout.total_seconds())

return ApifyClientAsync(**kwargs)

@_ensure_context
async def open_dataset(
Expand Down Expand Up @@ -867,7 +869,7 @@ async def start(
timeout: timedelta | None | Literal['inherit', 'RemainingTime'] = None,
wait_for_finish: int | None = None,
webhooks: list[Webhook] | None = None,
) -> ActorRun:
) -> Run:
"""Run an Actor on the Apify platform.

Unlike `Actor.call`, this method just starts the run without waiting for finish.
Expand Down Expand Up @@ -919,17 +921,21 @@ async def start(
f'Invalid timeout {timeout!r}: expected `None`, `"inherit"`, `"RemainingTime"`, or a `timedelta`.'
)

api_result = await client.actor(actor_id).start(
actor_client = client.actor(actor_id)
run = await actor_client.start(
run_input=run_input,
content_type=content_type,
build=build,
memory_mbytes=memory_mbytes,
timeout_secs=int(actor_start_timeout.total_seconds()) if actor_start_timeout is not None else None,
timeout=actor_start_timeout if actor_start_timeout is not None else 'medium',
wait_for_finish=wait_for_finish,
webhooks=serialized_webhooks,
)

return ActorRun.model_validate(api_result)
if run is None:
raise RuntimeError(f'Failed to start Actor with ID "{actor_id}".')

return run

@_ensure_context
async def abort(
Expand All @@ -939,7 +945,7 @@ async def abort(
token: str | None = None,
status_message: str | None = None,
gracefully: bool | None = None,
) -> ActorRun:
) -> Run:
"""Abort given Actor run on the Apify platform using the current user account.

The user account is determined by the `APIFY_TOKEN` environment variable.
Expand All @@ -956,13 +962,17 @@ async def abort(
Info about the aborted Actor run.
"""
client = self.new_client(token=token) if token else self.apify_client
run_client = client.run(run_id)

if status_message:
await client.run(run_id).update(status_message=status_message)
await run_client.update(status_message=status_message)

api_result = await client.run(run_id).abort(gracefully=gracefully)
run = await run_client.abort(gracefully=gracefully)

return ActorRun.model_validate(api_result)
if run is None:
raise RuntimeError(f'Failed to abort Actor run with ID "{run_id}".')

return run

@_ensure_context
async def call(
Expand All @@ -978,7 +988,7 @@ async def call(
webhooks: list[Webhook] | None = None,
wait: timedelta | None = None,
logger: logging.Logger | None | Literal['default'] = 'default',
) -> ActorRun | None:
) -> Run | None:
"""Start an Actor on the Apify Platform and wait for it to finish before returning.

It waits indefinitely, unless the wait argument is provided.
Expand Down Expand Up @@ -1034,18 +1044,22 @@ async def call(
f'Invalid timeout {timeout!r}: expected `None`, `"inherit"`, `"RemainingTime"`, or a `timedelta`.'
)

api_result = await client.actor(actor_id).call(
actor_client = client.actor(actor_id)
run = await actor_client.call(
run_input=run_input,
content_type=content_type,
build=build,
memory_mbytes=memory_mbytes,
timeout_secs=int(actor_call_timeout.total_seconds()) if actor_call_timeout is not None else None,
timeout=actor_call_timeout if actor_call_timeout is not None else 'no_timeout',
webhooks=serialized_webhooks,
wait_secs=int(wait.total_seconds()) if wait is not None else None,
wait_duration=wait,
logger=logger,
)

return ActorRun.model_validate(api_result)
if run is None:
raise RuntimeError(f'Failed to call Actor with ID "{actor_id}".')

return run

@_ensure_context
async def call_task(
Expand All @@ -1059,7 +1073,7 @@ async def call_task(
webhooks: list[Webhook] | None = None,
wait: timedelta | None = None,
token: str | None = None,
) -> ActorRun | None:
) -> Run | None:
"""Start an Actor task on the Apify Platform and wait for it to finish before returning.

It waits indefinitely, unless the wait argument is provided.
Expand Down Expand Up @@ -1106,16 +1120,20 @@ async def call_task(
else:
raise ValueError(f'Invalid timeout {timeout!r}: expected `None`, `"inherit"`, or a `timedelta`.')

api_result = await client.task(task_id).call(
task_client = client.task(task_id)
run = await task_client.call(
task_input=task_input,
build=build,
memory_mbytes=memory_mbytes,
timeout_secs=int(task_call_timeout.total_seconds()) if task_call_timeout is not None else None,
timeout=task_call_timeout if task_call_timeout is not None else 'no_timeout',
webhooks=serialized_webhooks,
wait_secs=int(wait.total_seconds()) if wait is not None else None,
wait_duration=wait,
)

return ActorRun.model_validate(api_result)
if run is None:
raise RuntimeError(f'Failed to call Task with ID "{task_id}".')

return run

@_ensure_context
async def metamorph(
Expand Down Expand Up @@ -1274,7 +1292,7 @@ async def set_status_message(
status_message: str,
*,
is_terminal: bool | None = None,
) -> ActorRun | None:
) -> Run | None:
"""Set the status message for the current Actor run.

Args:
Expand All @@ -1293,11 +1311,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(
Expand Down Expand Up @@ -1402,7 +1427,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'
Expand Down
Loading
Loading