Skip to content

Commit 923eb15

Browse files
committed
Refactoring p1
1 parent 39bcafd commit 923eb15

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+2185
-1039
lines changed

src/apify_client/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from importlib import metadata
22

3-
from ._client import ApifyClient, ApifyClientAsync
3+
from ._apify_client import ApifyClient, ApifyClientAsync
44

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

Lines changed: 32 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

3-
from apify_client._http_client import HTTPClient, HTTPClientAsync
3+
from apify_client._client_config import ClientConfig
4+
from apify_client._http_client import HttpClient, HttpClientAsync
45
from apify_client._resource_clients import (
56
ActorClient,
67
ActorClientAsync,
@@ -49,61 +50,12 @@
4950
WebhookDispatchCollectionClient,
5051
WebhookDispatchCollectionClientAsync,
5152
)
52-
from apify_client._types import Statistics
53+
from apify_client._statistics import Statistics
5354

54-
DEFAULT_API_URL = 'https://api.apify.com'
55-
DEFAULT_TIMEOUT = 360
56-
API_VERSION = 'v2'
5755

58-
59-
class _BaseApifyClient:
60-
http_client: HTTPClient | HTTPClientAsync
61-
62-
def __init__(
63-
self,
64-
token: str | None = None,
65-
*,
66-
api_url: str | None = None,
67-
api_public_url: str | None = None,
68-
max_retries: int | None = 8,
69-
min_delay_between_retries_millis: int | None = 500,
70-
timeout_secs: int | None = DEFAULT_TIMEOUT,
71-
) -> None:
72-
"""Initialize a new instance.
73-
74-
Args:
75-
token: The Apify API token.
76-
api_url: The URL of the Apify API server to which to connect. Defaults to https://api.apify.com. It can
77-
be an internal URL that is not globally accessible, in such case `api_public_url` should be set as well.
78-
api_public_url: The globally accessible URL of the Apify API server. It should be set only if the `api_url`
79-
is an internal URL that is not globally accessible.
80-
max_retries: How many times to retry a failed request at most.
81-
min_delay_between_retries_millis: How long will the client wait between retrying requests
82-
(increases exponentially from this value).
83-
timeout_secs: The socket timeout of the HTTP requests sent to the Apify API.
84-
"""
85-
self.token = token
86-
api_url = (api_url or DEFAULT_API_URL).rstrip('/')
87-
self.base_url = f'{api_url}/{API_VERSION}'
88-
api_public_url = (api_public_url or DEFAULT_API_URL).rstrip('/')
89-
self.public_base_url = f'{api_public_url}/{API_VERSION}'
90-
self.max_retries = max_retries or 8
91-
self.min_delay_between_retries_millis = min_delay_between_retries_millis or 500
92-
self.timeout_secs = timeout_secs or DEFAULT_TIMEOUT
93-
94-
def _options(self) -> dict:
95-
return {
96-
'root_client': self,
97-
'base_url': self.base_url,
98-
'http_client': self.http_client,
99-
}
100-
101-
102-
class ApifyClient(_BaseApifyClient):
56+
class ApifyClient:
10357
"""The Apify API client."""
10458

105-
http_client: HTTPClient
106-
10759
def __init__(
10860
self,
10961
token: str | None = None,
@@ -112,7 +64,7 @@ def __init__(
11264
api_public_url: str | None = None,
11365
max_retries: int | None = 8,
11466
min_delay_between_retries_millis: int | None = 500,
115-
timeout_secs: int | None = DEFAULT_TIMEOUT,
67+
timeout_secs: int | None = 360,
11668
) -> None:
11769
"""Initialize a new instance.
11870
@@ -127,8 +79,8 @@ def __init__(
12779
(increases exponentially from this value).
12880
timeout_secs: The socket timeout of the HTTP requests sent to the Apify API.
12981
"""
130-
super().__init__(
131-
token,
82+
self._config = ClientConfig.from_user_params(
83+
token=token,
13284
api_url=api_url,
13385
api_public_url=api_public_url,
13486
max_retries=max_retries,
@@ -137,21 +89,26 @@ def __init__(
13789
)
13890

13991
self.stats = Statistics()
140-
self.http_client = HTTPClient(
141-
token=token,
142-
max_retries=self.max_retries,
143-
min_delay_between_retries_millis=self.min_delay_between_retries_millis,
144-
timeout_secs=self.timeout_secs,
145-
stats=self.stats,
146-
)
92+
self.http_client = HttpClient(config=self._config, stats=self.stats)
93+
94+
def _options(self) -> dict:
95+
return {
96+
'base_url': self._config.base_url,
97+
'public_base_url': self._config.public_base_url,
98+
'http_client': self.http_client,
99+
}
147100

148101
def actor(self, actor_id: str) -> ActorClient:
149102
"""Retrieve the sub-client for manipulating a single Actor.
150103
151104
Args:
152105
actor_id: ID of the Actor to be manipulated.
153106
"""
154-
return ActorClient(resource_id=actor_id, **self._options())
107+
return ActorClient(
108+
resource_id=actor_id,
109+
resource_path='acts',
110+
**self._options()
111+
)
155112

156113
def actors(self) -> ActorCollectionClient:
157114
"""Retrieve the sub-client for manipulating Actors."""
@@ -287,11 +244,9 @@ def store(self) -> StoreCollectionClient:
287244
return StoreCollectionClient(**self._options())
288245

289246

290-
class ApifyClientAsync(_BaseApifyClient):
247+
class ApifyClientAsync:
291248
"""The asynchronous version of the Apify API client."""
292249

293-
http_client: HTTPClientAsync
294-
295250
def __init__(
296251
self,
297252
token: str | None = None,
@@ -300,7 +255,7 @@ def __init__(
300255
api_public_url: str | None = None,
301256
max_retries: int | None = 8,
302257
min_delay_between_retries_millis: int | None = 500,
303-
timeout_secs: int | None = DEFAULT_TIMEOUT,
258+
timeout_secs: int | None = 360,
304259
) -> None:
305260
"""Initialize a new instance.
306261
@@ -315,8 +270,8 @@ def __init__(
315270
(increases exponentially from this value).
316271
timeout_secs: The socket timeout of the HTTP requests sent to the Apify API.
317272
"""
318-
super().__init__(
319-
token,
273+
self._config = ClientConfig.from_user_params(
274+
token=token,
320275
api_url=api_url,
321276
api_public_url=api_public_url,
322277
max_retries=max_retries,
@@ -325,13 +280,14 @@ def __init__(
325280
)
326281

327282
self.stats = Statistics()
328-
self.http_client = HTTPClientAsync(
329-
token=token,
330-
max_retries=self.max_retries,
331-
min_delay_between_retries_millis=self.min_delay_between_retries_millis,
332-
timeout_secs=self.timeout_secs,
333-
stats=self.stats,
334-
)
283+
self.http_client = HttpClientAsync(config=self._config, stats=self.stats)
284+
285+
def _options(self) -> dict:
286+
return {
287+
'base_url': self._config.base_url,
288+
'public_base_url': self._config.public_base_url,
289+
'http_client': self.http_client,
290+
}
335291

336292
def actor(self, actor_id: str) -> ActorClientAsync:
337293
"""Retrieve the sub-client for manipulating a single Actor.

src/apify_client/_client_config.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""Client configuration module.
2+
3+
This module provides the ClientConfig dataclass that encapsulates all
4+
configuration options for the Apify API client.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from dataclasses import dataclass
10+
11+
DEFAULT_API_URL = 'https://api.apify.com'
12+
DEFAULT_TIMEOUT = 360
13+
API_VERSION = 'v2'
14+
15+
16+
@dataclass(frozen=True)
17+
class ClientConfig:
18+
"""Immutable configuration for Apify HTTP client.
19+
20+
This dataclass holds all configuration options needed by the HTTP client
21+
to communicate with the Apify API. It is created once by ApifyClient
22+
and shared across all resource clients.
23+
24+
All fields are frozen (immutable) to prevent accidental modification
25+
after initialization.
26+
"""
27+
28+
base_url: str
29+
"""Base URL of the Apify API (e.g., 'https://api.apify.com/v2')."""
30+
31+
public_base_url: str
32+
"""Public base URL for CDN access (e.g., 'https://cdn.apify.com/v2')."""
33+
34+
token: str | None = None
35+
"""Apify API token for authentication."""
36+
37+
max_retries: int = 8
38+
"""Maximum number of retries for failed requests."""
39+
40+
min_delay_between_retries_millis: int = 500
41+
"""Minimum delay between retries in milliseconds (increases exponentially)."""
42+
43+
timeout_secs: int = 360
44+
"""Request timeout in seconds."""
45+
46+
@classmethod
47+
def from_user_params(
48+
cls,
49+
*,
50+
token: str | None = None,
51+
api_url: str | None = None,
52+
api_public_url: str | None = None,
53+
max_retries: int | None = 8,
54+
min_delay_between_retries_millis: int | None = 500,
55+
timeout_secs: int | None = 360,
56+
) -> ClientConfig:
57+
"""Create ClientConfig from user-provided parameters.
58+
59+
This factory method processes user input and creates a properly
60+
formatted ClientConfig instance with sensible defaults.
61+
62+
Args:
63+
token: Apify API token for authentication.
64+
api_url: Base API URL (default: https://api.apify.com).
65+
api_public_url: Public CDN URL (default: same as api_url).
66+
max_retries: Maximum number of retries for failed requests (default: 8).
67+
min_delay_between_retries_millis: Minimum delay between retries in ms (default: 500).
68+
timeout_secs: Request timeout in seconds (default: 360).
69+
70+
Returns:
71+
Immutable ClientConfig instance.
72+
"""
73+
api_url = (api_url or DEFAULT_API_URL).rstrip('/')
74+
api_public_url = (api_public_url or DEFAULT_API_URL).rstrip('/')
75+
76+
return cls(
77+
base_url=f'{api_url}/{API_VERSION}',
78+
public_base_url=f'{api_public_url}/{API_VERSION}',
79+
token=token,
80+
max_retries=max_retries or 8,
81+
min_delay_between_retries_millis=min_delay_between_retries_millis or 500,
82+
timeout_secs=timeout_secs or DEFAULT_TIMEOUT,
83+
)

src/apify_client/_consts.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"""Constants and enums used by the Apify client."""
2+
3+
from __future__ import annotations
4+
5+
from enum import Enum
6+
7+
8+
class ActorJobStatus(str, Enum):
9+
"""Available statuses for Actor jobs (runs or builds).
10+
11+
These statuses represent the lifecycle of an Actor execution,
12+
from initialization to completion or termination.
13+
"""
14+
15+
READY = 'READY'
16+
"""Actor job has been initialized but not yet started."""
17+
18+
RUNNING = 'RUNNING'
19+
"""Actor job is currently executing."""
20+
21+
SUCCEEDED = 'SUCCEEDED'
22+
"""Actor job completed successfully without errors."""
23+
24+
FAILED = 'FAILED'
25+
"""Actor job or build failed due to an error or exception."""
26+
27+
TIMING_OUT = 'TIMING-OUT'
28+
"""Actor job is currently in the process of timing out."""
29+
30+
TIMED_OUT = 'TIMED-OUT'
31+
"""Actor job was terminated due to timeout."""
32+
33+
ABORTING = 'ABORTING'
34+
"""Actor job is currently being aborted by user request."""
35+
36+
ABORTED = 'ABORTED'
37+
"""Actor job was successfully aborted by user request."""
38+
39+
@property
40+
def is_terminal(self: ActorJobStatus) -> bool:
41+
"""Whether this Actor job status is terminal."""
42+
return self in (
43+
ActorJobStatus.SUCCEEDED,
44+
ActorJobStatus.FAILED,
45+
ActorJobStatus.TIMED_OUT,
46+
ActorJobStatus.ABORTED,
47+
)
48+
49+
50+
class WebhookEventType(str, Enum):
51+
"""Event types that can trigger webhook notifications.
52+
53+
These events are sent to configured webhook URLs when specific
54+
Actor run or build lifecycle events occur, enabling integration
55+
with external systems and automated workflows.
56+
"""
57+
58+
ACTOR_RUN_CREATED = 'ACTOR.RUN.CREATED'
59+
"""Triggered when a new Actor run is created and initialized."""
60+
61+
ACTOR_RUN_SUCCEEDED = 'ACTOR.RUN.SUCCEEDED'
62+
"""Triggered when an Actor run completes successfully."""
63+
64+
ACTOR_RUN_FAILED = 'ACTOR.RUN.FAILED'
65+
"""Triggered when an Actor run fails due to an error."""
66+
67+
ACTOR_RUN_TIMED_OUT = 'ACTOR.RUN.TIMED_OUT'
68+
"""Triggered when an Actor run is terminated due to timeout."""
69+
70+
ACTOR_RUN_ABORTED = 'ACTOR.RUN.ABORTED'
71+
"""Triggered when an Actor run is manually aborted by user."""
72+
73+
ACTOR_RUN_RESURRECTED = 'ACTOR.RUN.RESURRECTED'
74+
"""Triggered when a previously failed Actor run is automatically resurrected."""
75+
76+
ACTOR_BUILD_CREATED = 'ACTOR.BUILD.CREATED'
77+
"""Triggered when a new Actor build process is initiated."""
78+
79+
ACTOR_BUILD_SUCCEEDED = 'ACTOR.BUILD.SUCCEEDED'
80+
"""Triggered when an Actor build completes successfully."""
81+
82+
ACTOR_BUILD_FAILED = 'ACTOR.BUILD.FAILED'
83+
"""Triggered when an Actor build fails due to compilation or setup errors."""
84+
85+
ACTOR_BUILD_TIMED_OUT = 'ACTOR.BUILD.TIMED_OUT'
86+
"""Triggered when an Actor build process exceeds the time limit."""
87+
88+
ACTOR_BUILD_ABORTED = 'ACTOR.BUILD.ABORTED'
89+
"""Triggered when an Actor build is manually cancelled by user."""
90+
91+
92+
class StorageGeneralAccess(str, Enum):
93+
"""Storage setting determining how others can access the storage.
94+
95+
This setting overrides the user setting of the storage owner.
96+
"""
97+
98+
FOLLOW_USER_SETTING = 'FOLLOW_USER_SETTING'
99+
"""Respect the user setting of the storage owner (default behavior)."""
100+
101+
RESTRICTED = 'RESTRICTED'
102+
"""Only signed-in users with explicit access can read this storage."""
103+
104+
ANYONE_WITH_ID_CAN_READ = 'ANYONE_WITH_ID_CAN_READ'
105+
"""Anyone with a link or the unique storage ID can read this storage."""
106+
107+
ANYONE_WITH_NAME_CAN_READ = 'ANYONE_WITH_NAME_CAN_READ'
108+
"""Anyone with a link, ID, or storage name can read this storage."""
109+
110+
111+
class RunGeneralAccess(str, Enum):
112+
"""Run setting determining how others can access the run.
113+
114+
This setting overrides the user setting of the run owner.
115+
"""
116+
117+
FOLLOW_USER_SETTING = 'FOLLOW_USER_SETTING'
118+
"""Respect the user setting of the storage owner (default behavior)."""
119+
120+
RESTRICTED = 'RESTRICTED'
121+
"""Only signed-in users with explicit access can read this run."""
122+
123+
ANYONE_WITH_ID_CAN_READ = 'ANYONE_WITH_ID_CAN_READ'
124+
"""Anyone with a link or the unique run ID can read this run."""

0 commit comments

Comments
 (0)