Skip to content

Commit e793417

Browse files
committed
Better representations for API calls
1 parent 45fa0c6 commit e793417

12 files changed

Lines changed: 410 additions & 270 deletions
Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
"""Helper functions for building API request representations.
2+
3+
This module provides utilities for constructing dictionary representations of API resources. These representations
4+
are used when creating or updating resources via the Apify API.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from typing import TYPE_CHECKING, Any
10+
11+
from typing_extensions import overload
12+
13+
from apify_client._utils import enum_to_value
14+
15+
if TYPE_CHECKING:
16+
from datetime import timedelta
17+
18+
from apify_client._consts import WebhookEventType
19+
from apify_client._models import ActorPermissionLevel, VersionSourceType
20+
21+
22+
@overload
23+
def to_seconds(td: timedelta) -> int: ...
24+
@overload
25+
def to_seconds(td: None) -> None: ...
26+
27+
28+
def to_seconds(td: timedelta | None) -> int | None:
29+
"""Convert timedelta to seconds.
30+
31+
Args:
32+
td: The timedelta to convert, or None.
33+
34+
Returns:
35+
The total seconds as an integer, or None if input is None.
36+
"""
37+
return int(td.total_seconds()) if td is not None else None
38+
39+
40+
def build_actor_standby_dict(
41+
*,
42+
is_enabled: bool | None = None,
43+
desired_requests_per_actor_run: int | None = None,
44+
max_requests_per_actor_run: int | None = None,
45+
idle_timeout: timedelta | None = None,
46+
build: str | None = None,
47+
memory_mbytes: int | None = None,
48+
) -> dict:
49+
"""Build actor standby configuration dictionary.
50+
51+
This is used by both Actor and Task representations.
52+
53+
Args:
54+
is_enabled: Whether the Actor Standby is enabled.
55+
desired_requests_per_actor_run: The desired number of concurrent HTTP requests.
56+
max_requests_per_actor_run: The maximum number of concurrent HTTP requests.
57+
idle_timeout: Idle timeout before the Actor run is shut down.
58+
build: The build tag or number to run in Standby mode.
59+
memory_mbytes: The memory in megabytes for Standby mode.
60+
61+
Returns:
62+
Dictionary with actor standby configuration.
63+
"""
64+
return {
65+
'isEnabled': is_enabled,
66+
'desiredRequestsPerActorRun': desired_requests_per_actor_run,
67+
'maxRequestsPerActorRun': max_requests_per_actor_run,
68+
'idleTimeoutSecs': to_seconds(idle_timeout),
69+
'build': build,
70+
'memoryMbytes': memory_mbytes,
71+
}
72+
73+
74+
def build_default_run_options_dict(
75+
*,
76+
build: str | None = None,
77+
max_items: int | None = None,
78+
memory_mbytes: int | None = None,
79+
timeout: timedelta | None = None,
80+
restart_on_error: bool | None = None,
81+
force_permission_level: ActorPermissionLevel | None = None,
82+
) -> dict:
83+
"""Build default run options dictionary for Actor.
84+
85+
Args:
86+
build: Tag or number of the build to run by default.
87+
max_items: Default limit of results returned by runs.
88+
memory_mbytes: Default memory allocated for runs, in megabytes.
89+
timeout: Default timeout for runs.
90+
restart_on_error: Whether to restart on non-zero exit code.
91+
force_permission_level: Permission level to force for runs.
92+
93+
Returns:
94+
Dictionary with default run options.
95+
"""
96+
return {
97+
'build': build,
98+
'maxItems': max_items,
99+
'memoryMbytes': memory_mbytes,
100+
'timeoutSecs': to_seconds(timeout),
101+
'restartOnError': restart_on_error,
102+
'forcePermissionLevel': force_permission_level,
103+
}
104+
105+
106+
def build_task_options_dict(
107+
*,
108+
build: str | None = None,
109+
max_items: int | None = None,
110+
memory_mbytes: int | None = None,
111+
timeout: timedelta | None = None,
112+
restart_on_error: bool | None = None,
113+
) -> dict:
114+
"""Build task options dictionary.
115+
116+
Args:
117+
build: Actor build to run.
118+
max_items: Maximum number of results returned by the run.
119+
memory_mbytes: Memory limit for the run, in megabytes.
120+
timeout: Timeout for the run.
121+
restart_on_error: Whether to restart on non-zero exit code.
122+
123+
Returns:
124+
Dictionary with task options.
125+
"""
126+
return {
127+
'build': build,
128+
'maxItems': max_items,
129+
'memoryMbytes': memory_mbytes,
130+
'timeoutSecs': to_seconds(timeout),
131+
'restartOnError': restart_on_error,
132+
}
133+
134+
135+
def build_example_run_input_dict(
136+
*,
137+
body: Any = None,
138+
content_type: str | None = None,
139+
) -> dict:
140+
"""Build example run input dictionary for Actor.
141+
142+
Args:
143+
body: Input to be prefilled as default input.
144+
content_type: The content type of the example run input.
145+
146+
Returns:
147+
Dictionary with example run input.
148+
"""
149+
return {
150+
'body': body,
151+
'contentType': content_type,
152+
}
153+
154+
155+
def build_webhook_condition_dict(
156+
*,
157+
actor_id: str | None = None,
158+
actor_task_id: str | None = None,
159+
actor_run_id: str | None = None,
160+
) -> dict:
161+
"""Build webhook condition dictionary.
162+
163+
Args:
164+
actor_id: The Actor ID to filter webhook events.
165+
actor_task_id: The Actor task ID to filter webhook events.
166+
actor_run_id: The Actor run ID to filter webhook events.
167+
168+
Returns:
169+
Dictionary with webhook condition.
170+
"""
171+
return {
172+
'actorRunId': actor_run_id,
173+
'actorTaskId': actor_task_id,
174+
'actorId': actor_id,
175+
}
176+
177+
178+
def get_actor_representation(
179+
*,
180+
name: str | None,
181+
title: str | None = None,
182+
description: str | None = None,
183+
seo_title: str | None = None,
184+
seo_description: str | None = None,
185+
versions: list[dict] | None = None,
186+
restart_on_error: bool | None = None,
187+
is_public: bool | None = None,
188+
is_deprecated: bool | None = None,
189+
is_anonymously_runnable: bool | None = None,
190+
categories: list[str] | None = None,
191+
default_run_build: str | None = None,
192+
default_run_max_items: int | None = None,
193+
default_run_memory_mbytes: int | None = None,
194+
default_run_timeout: timedelta | None = None,
195+
default_run_force_permission_level: ActorPermissionLevel | None = None,
196+
example_run_input_body: Any = None,
197+
example_run_input_content_type: str | None = None,
198+
actor_standby_is_enabled: bool | None = None,
199+
actor_standby_desired_requests_per_actor_run: int | None = None,
200+
actor_standby_max_requests_per_actor_run: int | None = None,
201+
actor_standby_idle_timeout: timedelta | None = None,
202+
actor_standby_build: str | None = None,
203+
actor_standby_memory_mbytes: int | None = None,
204+
pricing_infos: list[dict] | None = None,
205+
actor_permission_level: ActorPermissionLevel | None = None,
206+
tagged_builds: dict[str, None | dict[str, str]] | None = None,
207+
) -> dict:
208+
"""Get dictionary representation of the Actor."""
209+
actor_dict: dict[str, Any] = {
210+
'name': name,
211+
'title': title,
212+
'description': description,
213+
'seoTitle': seo_title,
214+
'seoDescription': seo_description,
215+
'versions': versions,
216+
'isPublic': is_public,
217+
'isDeprecated': is_deprecated,
218+
'isAnonymouslyRunnable': is_anonymously_runnable,
219+
'categories': categories,
220+
'pricingInfos': pricing_infos,
221+
'actorPermissionLevel': actor_permission_level,
222+
'defaultRunOptions': build_default_run_options_dict(
223+
build=default_run_build,
224+
max_items=default_run_max_items,
225+
memory_mbytes=default_run_memory_mbytes,
226+
timeout=default_run_timeout,
227+
restart_on_error=restart_on_error,
228+
force_permission_level=default_run_force_permission_level,
229+
),
230+
'actorStandby': build_actor_standby_dict(
231+
is_enabled=actor_standby_is_enabled,
232+
desired_requests_per_actor_run=actor_standby_desired_requests_per_actor_run,
233+
max_requests_per_actor_run=actor_standby_max_requests_per_actor_run,
234+
idle_timeout=actor_standby_idle_timeout,
235+
build=actor_standby_build,
236+
memory_mbytes=actor_standby_memory_mbytes,
237+
),
238+
'exampleRunInput': build_example_run_input_dict(
239+
body=example_run_input_body,
240+
content_type=example_run_input_content_type,
241+
),
242+
}
243+
244+
# Include taggedBuilds if provided
245+
if tagged_builds is not None:
246+
actor_dict['taggedBuilds'] = tagged_builds
247+
248+
return actor_dict
249+
250+
251+
def get_task_representation(
252+
actor_id: str | None = None,
253+
name: str | None = None,
254+
task_input: dict | None = None,
255+
build: str | None = None,
256+
max_items: int | None = None,
257+
memory_mbytes: int | None = None,
258+
timeout: timedelta | None = None,
259+
title: str | None = None,
260+
actor_standby_desired_requests_per_actor_run: int | None = None,
261+
actor_standby_max_requests_per_actor_run: int | None = None,
262+
actor_standby_idle_timeout: timedelta | None = None,
263+
actor_standby_build: str | None = None,
264+
actor_standby_memory_mbytes: int | None = None,
265+
*,
266+
restart_on_error: bool | None = None,
267+
) -> dict:
268+
"""Get the dictionary representation of a task."""
269+
return {
270+
'actId': actor_id,
271+
'name': name,
272+
'title': title,
273+
'input': task_input,
274+
'options': build_task_options_dict(
275+
build=build,
276+
max_items=max_items,
277+
memory_mbytes=memory_mbytes,
278+
timeout=timeout,
279+
restart_on_error=restart_on_error,
280+
),
281+
'actorStandby': build_actor_standby_dict(
282+
desired_requests_per_actor_run=actor_standby_desired_requests_per_actor_run,
283+
max_requests_per_actor_run=actor_standby_max_requests_per_actor_run,
284+
idle_timeout=actor_standby_idle_timeout,
285+
build=actor_standby_build,
286+
memory_mbytes=actor_standby_memory_mbytes,
287+
),
288+
}
289+
290+
291+
def get_actor_version_representation(
292+
*,
293+
version_number: str | None = None,
294+
build_tag: str | None = None,
295+
env_vars: list[dict] | None = None,
296+
apply_env_vars_to_build: bool | None = None,
297+
source_type: VersionSourceType | None = None,
298+
source_files: list[dict] | None = None,
299+
git_repo_url: str | None = None,
300+
tarball_url: str | None = None,
301+
github_gist_url: str | None = None,
302+
) -> dict:
303+
"""Get dictionary representation of an Actor version."""
304+
return {
305+
'versionNumber': version_number,
306+
'buildTag': build_tag,
307+
'envVars': env_vars,
308+
'applyEnvVarsToBuild': apply_env_vars_to_build,
309+
'sourceType': enum_to_value(source_type),
310+
'sourceFiles': source_files,
311+
'gitRepoUrl': git_repo_url,
312+
'tarballUrl': tarball_url,
313+
'gitHubGistUrl': github_gist_url,
314+
}
315+
316+
317+
def get_schedule_representation(
318+
cron_expression: str | None = None,
319+
name: str | None = None,
320+
actions: list[dict] | None = None,
321+
description: str | None = None,
322+
timezone: str | None = None,
323+
title: str | None = None,
324+
*,
325+
is_enabled: bool | None = None,
326+
is_exclusive: bool | None = None,
327+
) -> dict:
328+
"""Get dictionary representation of a schedule."""
329+
return {
330+
'cronExpression': cron_expression,
331+
'isEnabled': is_enabled,
332+
'isExclusive': is_exclusive,
333+
'name': name,
334+
'actions': actions,
335+
'description': description,
336+
'timezone': timezone,
337+
'title': title,
338+
}
339+
340+
341+
def get_webhook_representation(
342+
*,
343+
event_types: list[WebhookEventType] | None = None,
344+
request_url: str | None = None,
345+
payload_template: str | None = None,
346+
headers_template: str | None = None,
347+
actor_id: str | None = None,
348+
actor_task_id: str | None = None,
349+
actor_run_id: str | None = None,
350+
ignore_ssl_errors: bool | None = None,
351+
do_not_retry: bool | None = None,
352+
idempotency_key: str | None = None,
353+
is_ad_hoc: bool | None = None,
354+
) -> dict:
355+
"""Prepare webhook dictionary representation for clients."""
356+
webhook: dict[str, Any] = {
357+
'requestUrl': request_url,
358+
'payloadTemplate': payload_template,
359+
'headersTemplate': headers_template,
360+
'ignoreSslErrors': ignore_ssl_errors,
361+
'doNotRetry': do_not_retry,
362+
'idempotencyKey': idempotency_key,
363+
'isAdHoc': is_ad_hoc,
364+
'condition': build_webhook_condition_dict(
365+
actor_id=actor_id,
366+
actor_task_id=actor_task_id,
367+
actor_run_id=actor_run_id,
368+
),
369+
}
370+
371+
if actor_run_id is not None:
372+
webhook['isAdHoc'] = True
373+
374+
if event_types is not None:
375+
webhook['eventTypes'] = [enum_to_value(event_type) for event_type in event_types]
376+
377+
return webhook

src/apify_client/_resource_clients/_resource_client.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from apify_client._consts import DEFAULT_WAIT_FOR_FINISH, DEFAULT_WAIT_WHEN_JOB_NOT_EXIST, ActorJobStatus
1111
from apify_client._logging import WithLogDetailsClient
12+
from apify_client._representations import to_seconds
1213
from apify_client._utils import catch_not_found_or_throw, response_to_dict, to_safe_id
1314
from apify_client.errors import ApifyApiError, ApifyClientError
1415

@@ -155,10 +156,10 @@ def _wait_for_finish(
155156
should_repeat = True
156157
job: dict | None = None
157158
seconds_elapsed = 0.0
158-
wait_secs = wait_duration.total_seconds() if wait_duration is not None else None
159+
wait_secs = to_seconds(wait_duration)
159160

160161
while should_repeat:
161-
wait_for_finish = int(DEFAULT_WAIT_FOR_FINISH.total_seconds())
162+
wait_for_finish = to_seconds(DEFAULT_WAIT_FOR_FINISH)
162163
if wait_secs is not None:
163164
wait_for_finish = int(wait_secs - seconds_elapsed)
164165

0 commit comments

Comments
 (0)