render_sdk 0.1.2__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- render_sdk/__init__.py +41 -4
- render_sdk/client/__init__.py +25 -0
- render_sdk/client/client.py +5 -0
- render_sdk/client/sse.py +5 -1
- render_sdk/client/tests/test_client.py +6 -4
- render_sdk/client/tests/test_sse.py +1 -0
- render_sdk/client/types.py +2 -1
- render_sdk/client/workflows.py +13 -3
- render_sdk/experimental/__init__.py +31 -0
- render_sdk/experimental/experimental.py +71 -0
- render_sdk/experimental/object/__init__.py +30 -0
- render_sdk/experimental/object/api.py +260 -0
- render_sdk/experimental/object/client.py +475 -0
- render_sdk/experimental/object/types.py +87 -0
- render_sdk/public_api/api/audit_logs/list_organization_audit_logs.py +303 -0
- render_sdk/public_api/api/audit_logs/list_owner_audit_logs.py +303 -0
- render_sdk/public_api/api/blob_storage/delete_blob.py +215 -0
- render_sdk/public_api/api/blob_storage/get_blob.py +221 -0
- render_sdk/public_api/api/{workflows/list_workflow_versions.py → blob_storage/list_blobs.py} +52 -30
- render_sdk/public_api/api/blob_storage/put_blob.py +248 -0
- render_sdk/public_api/api/blueprints/validate_blueprint.py +212 -0
- render_sdk/public_api/api/key_value/resume_key_value.py +203 -0
- render_sdk/public_api/api/key_value/suspend_key_value.py +203 -0
- render_sdk/public_api/api/metrics/get_bandwidth_sources.py +251 -0
- render_sdk/public_api/api/postgres/create_postgres_user.py +229 -0
- render_sdk/public_api/api/postgres/delete_postgres_user.py +201 -0
- render_sdk/public_api/api/postgres/list_postgres_users.py +195 -0
- render_sdk/public_api/api/redis_deprecated/__init__.py +1 -0
- render_sdk/public_api/api/{redis → redis_deprecated}/create_redis.py +4 -4
- render_sdk/public_api/api/{redis → redis_deprecated}/delete_redis.py +4 -4
- render_sdk/public_api/api/{redis → redis_deprecated}/list_redis.py +4 -0
- render_sdk/public_api/api/{redis → redis_deprecated}/retrieve_redis.py +4 -4
- render_sdk/public_api/api/{redis → redis_deprecated}/retrieve_redis_connection_info.py +4 -0
- render_sdk/public_api/api/{redis → redis_deprecated}/update_redis.py +4 -4
- render_sdk/public_api/api/services/create_service.py +4 -4
- render_sdk/public_api/api/workflow_tasks_ea/__init__.py +1 -0
- render_sdk/public_api/api/{workflows → workflow_tasks_ea}/cancel_task_run.py +12 -4
- render_sdk/public_api/api/{workflows → workflow_tasks_ea}/create_task.py +12 -4
- render_sdk/public_api/api/{workflows → workflow_tasks_ea}/get_task.py +12 -4
- render_sdk/public_api/api/{workflows → workflow_tasks_ea}/get_task_run.py +12 -4
- render_sdk/public_api/api/{workflows → workflow_tasks_ea}/list_task_runs.py +12 -0
- render_sdk/public_api/api/{workflows → workflow_tasks_ea}/list_tasks.py +24 -12
- render_sdk/public_api/api/workflows_ea/__init__.py +1 -0
- render_sdk/public_api/api/workflows_ea/create_workflow.py +199 -0
- render_sdk/public_api/api/{workflows/deploy_workflow.py → workflows_ea/create_workflow_version.py} +31 -14
- render_sdk/public_api/api/{workflows → workflows_ea}/delete_workflow.py +12 -4
- render_sdk/public_api/api/{workflows → workflows_ea}/get_workflow.py +32 -14
- render_sdk/public_api/api/{workflows → workflows_ea}/get_workflow_version.py +12 -4
- render_sdk/public_api/api/workflows_ea/list_workflow_versions.py +275 -0
- render_sdk/public_api/api/{workflows → workflows_ea}/list_workflows.py +41 -14
- render_sdk/public_api/api/workflows_ea/update_workflow.py +212 -0
- render_sdk/public_api/api/workspaces/remove_workspace_member.py +206 -0
- render_sdk/public_api/api/workspaces/update_workspace_member.py +235 -0
- render_sdk/public_api/models/__init__.py +82 -4
- render_sdk/public_api/models/audit_log.py +113 -0
- render_sdk/public_api/models/audit_log_actor.py +80 -0
- render_sdk/public_api/models/audit_log_actor_type.py +10 -0
- render_sdk/public_api/models/audit_log_event.py +80 -0
- render_sdk/public_api/models/audit_log_metadata.py +49 -0
- render_sdk/public_api/models/audit_log_status.py +9 -0
- render_sdk/public_api/models/audit_log_with_cursor.py +73 -0
- render_sdk/public_api/models/background_worker_details.py +2 -2
- render_sdk/public_api/models/background_worker_details_patch.py +1 -1
- render_sdk/public_api/models/background_worker_details_post.py +1 -1
- render_sdk/public_api/models/blob_metadata.py +85 -0
- render_sdk/public_api/models/blob_with_cursor.py +73 -0
- render_sdk/public_api/models/cache.py +6 -4
- render_sdk/public_api/models/cache_profile.py +10 -0
- render_sdk/public_api/models/create_deploy_body.py +23 -0
- render_sdk/public_api/models/create_version.py +70 -0
- render_sdk/public_api/models/credential_create_input.py +59 -0
- render_sdk/public_api/models/cron_job_details.py +2 -2
- render_sdk/public_api/models/cron_job_details_patch.py +1 -1
- render_sdk/public_api/models/cron_job_details_post.py +1 -1
- render_sdk/public_api/models/deploy_mode.py +9 -0
- render_sdk/public_api/models/event.py +11 -27
- render_sdk/public_api/models/event_type.py +1 -1
- render_sdk/public_api/models/get_bandwidth_sources_response_200.py +75 -0
- render_sdk/public_api/models/get_bandwidth_sources_response_200_data_item.py +101 -0
- render_sdk/public_api/models/get_bandwidth_sources_response_200_data_item_labels.py +78 -0
- render_sdk/public_api/models/get_bandwidth_sources_response_200_data_item_labels_traffic_source.py +12 -0
- render_sdk/public_api/models/get_bandwidth_sources_response_200_data_item_values_item.py +68 -0
- render_sdk/public_api/models/{server_unhealthy.py → get_bandwidth_sources_response_400.py} +12 -12
- render_sdk/public_api/models/get_blob_output.py +71 -0
- render_sdk/public_api/models/list_postgres_users_response_200_item.py +86 -0
- render_sdk/public_api/models/otel_provider_type.py +2 -0
- render_sdk/public_api/models/postgres.py +8 -0
- render_sdk/public_api/models/postgres_detail.py +26 -0
- render_sdk/public_api/models/postgres_parameter_overrides.py +44 -0
- render_sdk/public_api/models/postgres_patch_input.py +27 -0
- render_sdk/public_api/models/postgres_post_input.py +27 -0
- render_sdk/public_api/models/postgres_version.py +1 -0
- render_sdk/public_api/models/preview_input.py +2 -2
- render_sdk/public_api/models/private_service_details.py +2 -2
- render_sdk/public_api/models/private_service_details_patch.py +1 -1
- render_sdk/public_api/models/private_service_details_post.py +1 -1
- render_sdk/public_api/models/project_post_environment_input.py +26 -1
- render_sdk/public_api/models/put_blob_input.py +59 -0
- render_sdk/public_api/models/put_blob_output.py +79 -0
- render_sdk/public_api/models/read_replica.py +25 -1
- render_sdk/public_api/models/read_replica_input.py +25 -1
- render_sdk/public_api/models/run_task.py +35 -7
- render_sdk/public_api/models/service_event.py +12 -27
- render_sdk/public_api/models/service_event_type.py +0 -1
- render_sdk/public_api/models/service_post.py +9 -6
- render_sdk/public_api/models/task_attempt.py +88 -0
- render_sdk/public_api/models/task_attempt_details.py +108 -0
- render_sdk/public_api/models/task_data_type_1.py +44 -0
- render_sdk/public_api/models/task_run.py +23 -1
- render_sdk/public_api/models/task_run_details.py +50 -5
- render_sdk/public_api/models/task_run_status.py +1 -0
- render_sdk/public_api/models/task_with_cursor.py +73 -0
- render_sdk/public_api/models/team_member.py +5 -4
- render_sdk/public_api/models/team_member_role.py +12 -0
- render_sdk/public_api/models/update_workspace_member_body.py +61 -0
- render_sdk/public_api/models/validate_blueprint_request.py +84 -0
- render_sdk/public_api/models/validate_blueprint_response.py +105 -0
- render_sdk/public_api/models/validation_error.py +88 -0
- render_sdk/public_api/models/validation_plan_summary.py +107 -0
- render_sdk/public_api/models/web_service_details.py +2 -2
- render_sdk/public_api/models/web_service_details_patch.py +6 -5
- render_sdk/public_api/models/web_service_details_post.py +6 -5
- render_sdk/public_api/models/workflow.py +144 -0
- render_sdk/public_api/models/workflow_create.py +99 -0
- render_sdk/public_api/models/workflow_update.py +90 -0
- render_sdk/public_api/models/workflow_version.py +10 -14
- render_sdk/public_api/models/workflow_version_status.py +13 -0
- render_sdk/public_api/models/workflow_version_with_cursor.py +73 -0
- render_sdk/public_api/models/workflow_with_cursor.py +73 -0
- render_sdk/render.py +65 -0
- render_sdk/version.py +27 -0
- render_sdk/workflows/__init__.py +5 -1
- render_sdk/workflows/app.py +262 -0
- render_sdk/workflows/callback_api/models/__init__.py +2 -0
- render_sdk/workflows/callback_api/models/task.py +21 -0
- render_sdk/workflows/callback_api/models/task_options.py +18 -0
- render_sdk/workflows/callback_api/models/task_parameter.py +88 -0
- render_sdk/workflows/callback_api/py.typed +1 -1
- render_sdk/workflows/cli.py +58 -0
- render_sdk/workflows/client.py +8 -9
- render_sdk/workflows/executor.py +19 -7
- render_sdk/workflows/runner.py +43 -10
- render_sdk/workflows/task.py +84 -5
- render_sdk/workflows/tests/test_app.py +412 -0
- render_sdk/workflows/tests/test_cli.py +134 -0
- render_sdk/workflows/tests/test_end_to_end.py +71 -1
- render_sdk/workflows/tests/test_registration.py +58 -1
- {render_sdk-0.1.2.dist-info → render_sdk-0.2.0.dist-info}/METADATA +4 -3
- {render_sdk-0.1.2.dist-info → render_sdk-0.2.0.dist-info}/RECORD +155 -83
- {render_sdk-0.1.2.dist-info → render_sdk-0.2.0.dist-info}/WHEEL +1 -1
- render_sdk-0.2.0.dist-info/entry_points.txt +3 -0
- render_sdk/public_api/models/image_version.py +0 -79
- /render_sdk/public_api/api/{redis → audit_logs}/__init__.py +0 -0
- /render_sdk/public_api/api/{workflows → blob_storage}/__init__.py +0 -0
- /render_sdk/public_api/api/{workflows → workflow_tasks_ea}/stream_task_runs_events.py +0 -0
- {render_sdk-0.1.2.dist-info → render_sdk-0.2.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Workflows application for defining durable tasks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
import os
|
|
7
|
+
import warnings
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from typing import Any, TypeVar
|
|
10
|
+
|
|
11
|
+
from render_sdk.workflows.runner import register, run
|
|
12
|
+
from render_sdk.workflows.task import (
|
|
13
|
+
Options,
|
|
14
|
+
Retry,
|
|
15
|
+
TaskCallable,
|
|
16
|
+
TaskRegistry,
|
|
17
|
+
create_task_decorator,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
21
|
+
|
|
22
|
+
# Track whether auto_start has been registered
|
|
23
|
+
_auto_start_app_registered = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Workflows:
|
|
27
|
+
"""
|
|
28
|
+
Task definition app for Render Durable Workflows.
|
|
29
|
+
|
|
30
|
+
This is the primary entry point for defining tasks that run on Render.
|
|
31
|
+
For calling tasks via REST API, use the Render class instead.
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
app = Workflows(auto_start=True)
|
|
35
|
+
|
|
36
|
+
@app.task
|
|
37
|
+
def my_task(x: int) -> int:
|
|
38
|
+
return x * 2
|
|
39
|
+
|
|
40
|
+
With configuration:
|
|
41
|
+
app = Workflows(
|
|
42
|
+
default_retry=Retry(max_retries=3, wait_duration_ms=1000),
|
|
43
|
+
default_timeout=300,
|
|
44
|
+
auto_start=True,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
@app.task
|
|
48
|
+
def my_task(x: int) -> int:
|
|
49
|
+
return x * 2
|
|
50
|
+
|
|
51
|
+
Combining multiple modules:
|
|
52
|
+
from tasks_a import app as app_a
|
|
53
|
+
from tasks_b import app as app_b
|
|
54
|
+
|
|
55
|
+
combined = Workflows.from_workflows(app_a, app_b, auto_start=True)
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
_registry: TaskRegistry
|
|
59
|
+
_default_retry: Retry | None
|
|
60
|
+
_default_timeout: int | None
|
|
61
|
+
_default_plan: str | None
|
|
62
|
+
_auto_start: bool
|
|
63
|
+
_started: bool
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
*,
|
|
68
|
+
default_retry: Retry | None = None,
|
|
69
|
+
default_timeout: int | None = None,
|
|
70
|
+
default_plan: str | None = None,
|
|
71
|
+
auto_start: bool = False,
|
|
72
|
+
) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Initialize a Workflows application.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
default_retry: Default retry configuration for all tasks.
|
|
78
|
+
default_timeout: Default timeout in seconds for all tasks.
|
|
79
|
+
default_plan: Default resource plan for all tasks.
|
|
80
|
+
auto_start: If True, automatically starts the worker on program exit
|
|
81
|
+
when RENDER_SDK_MODE is set.
|
|
82
|
+
"""
|
|
83
|
+
global _auto_start_app_registered
|
|
84
|
+
|
|
85
|
+
self._registry = TaskRegistry()
|
|
86
|
+
self._default_retry = default_retry
|
|
87
|
+
self._default_timeout = default_timeout
|
|
88
|
+
self._default_plan = default_plan
|
|
89
|
+
self._auto_start = auto_start
|
|
90
|
+
self._started = False
|
|
91
|
+
|
|
92
|
+
if auto_start:
|
|
93
|
+
if _auto_start_app_registered:
|
|
94
|
+
warnings.warn(
|
|
95
|
+
"Multiple Workflows instances with auto_start=True detected. "
|
|
96
|
+
"Only one instance's tasks will be executed per invocation.",
|
|
97
|
+
UserWarning,
|
|
98
|
+
stacklevel=2,
|
|
99
|
+
)
|
|
100
|
+
_auto_start_app_registered = True
|
|
101
|
+
atexit.register(self._maybe_start)
|
|
102
|
+
|
|
103
|
+
def __repr__(self) -> str:
|
|
104
|
+
task_count = len(self._registry.get_task_names())
|
|
105
|
+
return f"Workflows(tasks={task_count}, auto_start={self._auto_start})"
|
|
106
|
+
|
|
107
|
+
def _maybe_start(self) -> None:
|
|
108
|
+
"""Start the worker if not already started and environment is configured."""
|
|
109
|
+
|
|
110
|
+
if self._started:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
mode = os.environ.get("RENDER_SDK_MODE")
|
|
114
|
+
socket_path = os.environ.get("RENDER_SDK_SOCKET_PATH")
|
|
115
|
+
|
|
116
|
+
# Only start if both environment variables are set
|
|
117
|
+
if mode and socket_path:
|
|
118
|
+
self.start()
|
|
119
|
+
|
|
120
|
+
def task(
|
|
121
|
+
self,
|
|
122
|
+
func: F | None = None,
|
|
123
|
+
*,
|
|
124
|
+
name: str | None = None,
|
|
125
|
+
retry: Retry | None = None,
|
|
126
|
+
timeout: int | None = None,
|
|
127
|
+
plan: str | None = None,
|
|
128
|
+
) -> F | Callable[[F], TaskCallable]:
|
|
129
|
+
"""
|
|
130
|
+
Decorator to register a function as a task.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
func: The function to decorate (when used without parentheses).
|
|
134
|
+
name: Optional custom name for the task (defaults to function name).
|
|
135
|
+
retry: Retry configuration (overrides default_retry).
|
|
136
|
+
timeout: Timeout in seconds (overrides default_timeout).
|
|
137
|
+
plan: Resource plan (overrides default_plan).
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
The decorated function as a TaskCallable.
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
@app.task
|
|
144
|
+
def simple_task(x: int) -> int:
|
|
145
|
+
return x * 2
|
|
146
|
+
|
|
147
|
+
@app.task(timeout=60, plan="starter")
|
|
148
|
+
def quick_task(x: int) -> int:
|
|
149
|
+
return x + 1
|
|
150
|
+
"""
|
|
151
|
+
# Build options from defaults and overrides
|
|
152
|
+
effective_retry = retry if retry is not None else self._default_retry
|
|
153
|
+
effective_timeout = timeout if timeout is not None else self._default_timeout
|
|
154
|
+
effective_plan = plan if plan is not None else self._default_plan
|
|
155
|
+
|
|
156
|
+
options = Options(
|
|
157
|
+
retry=effective_retry,
|
|
158
|
+
timeout_seconds=effective_timeout,
|
|
159
|
+
plan=effective_plan,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Create the task decorator bound to this app's registry
|
|
163
|
+
task_decorator = create_task_decorator(self._registry)
|
|
164
|
+
|
|
165
|
+
def decorator(f: F) -> TaskCallable:
|
|
166
|
+
return task_decorator(f, name=name, options=options)
|
|
167
|
+
|
|
168
|
+
if func is None:
|
|
169
|
+
# Called with arguments: @app.task(name="...", timeout=30)
|
|
170
|
+
return decorator
|
|
171
|
+
# Called without arguments: @app.task
|
|
172
|
+
return decorator(func)
|
|
173
|
+
|
|
174
|
+
def start(self) -> None:
|
|
175
|
+
"""
|
|
176
|
+
Start the workflow worker.
|
|
177
|
+
|
|
178
|
+
Reads RENDER_SDK_MODE and RENDER_SDK_SOCKET_PATH environment variables
|
|
179
|
+
to determine whether to run tasks or register them.
|
|
180
|
+
|
|
181
|
+
In most cases, use auto_start=True instead of calling this manually.
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
ValueError: If required environment variables are not set.
|
|
185
|
+
"""
|
|
186
|
+
import os
|
|
187
|
+
|
|
188
|
+
mode = os.environ.get("RENDER_SDK_MODE")
|
|
189
|
+
socket_path = os.environ.get("RENDER_SDK_SOCKET_PATH")
|
|
190
|
+
|
|
191
|
+
if not mode:
|
|
192
|
+
raise ValueError("RENDER_SDK_MODE environment variable is required")
|
|
193
|
+
|
|
194
|
+
if not socket_path:
|
|
195
|
+
raise ValueError("RENDER_SDK_SOCKET_PATH environment variable is required")
|
|
196
|
+
|
|
197
|
+
self._started = True
|
|
198
|
+
|
|
199
|
+
# Copy tasks to global registry for the runner to use
|
|
200
|
+
from render_sdk.workflows.task import _global_registry
|
|
201
|
+
|
|
202
|
+
for task_name in self._registry.get_task_names():
|
|
203
|
+
task_info = self._registry.get_task(task_name)
|
|
204
|
+
if task_info and task_name not in _global_registry._tasks:
|
|
205
|
+
_global_registry._tasks[task_name] = task_info
|
|
206
|
+
|
|
207
|
+
if mode == "run":
|
|
208
|
+
run(socket_path)
|
|
209
|
+
elif mode == "register":
|
|
210
|
+
register(socket_path)
|
|
211
|
+
else:
|
|
212
|
+
raise ValueError(f"Unknown mode: {mode}")
|
|
213
|
+
|
|
214
|
+
@classmethod
|
|
215
|
+
def from_workflows(
|
|
216
|
+
cls,
|
|
217
|
+
*apps: Workflows,
|
|
218
|
+
default_retry: Retry | None = None,
|
|
219
|
+
default_timeout: int | None = None,
|
|
220
|
+
default_plan: str | None = None,
|
|
221
|
+
auto_start: bool = False,
|
|
222
|
+
) -> Workflows:
|
|
223
|
+
"""
|
|
224
|
+
Combine multiple Workflows apps into one.
|
|
225
|
+
|
|
226
|
+
This is useful for organizing tasks across multiple modules.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
*apps: Workflows instances to combine.
|
|
230
|
+
default_retry: Default retry for new tasks on combined app.
|
|
231
|
+
default_timeout: Default timeout for new tasks on combined app.
|
|
232
|
+
default_plan: Default plan for new tasks on combined app.
|
|
233
|
+
auto_start: If True, auto-start the combined app.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
A new Workflows instance with all tasks from the input apps.
|
|
237
|
+
|
|
238
|
+
Example:
|
|
239
|
+
from tasks_a import app as app_a
|
|
240
|
+
from tasks_b import app as app_b
|
|
241
|
+
|
|
242
|
+
combined = Workflows.from_workflows(app_a, app_b, auto_start=True)
|
|
243
|
+
"""
|
|
244
|
+
combined = cls(
|
|
245
|
+
default_retry=default_retry,
|
|
246
|
+
default_timeout=default_timeout,
|
|
247
|
+
default_plan=default_plan,
|
|
248
|
+
auto_start=auto_start,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Copy tasks from all apps
|
|
252
|
+
for app in apps:
|
|
253
|
+
for task_name in app._registry.get_task_names():
|
|
254
|
+
task_info = app._registry.get_task(task_name)
|
|
255
|
+
if task_info:
|
|
256
|
+
if task_name in combined._registry._tasks:
|
|
257
|
+
raise ValueError(
|
|
258
|
+
f"Task '{task_name}' is defined in multiple apps"
|
|
259
|
+
)
|
|
260
|
+
combined._registry._tasks[task_name] = task_info
|
|
261
|
+
|
|
262
|
+
return combined
|
|
@@ -11,6 +11,7 @@ from .task import Task
|
|
|
11
11
|
from .task_complete import TaskComplete
|
|
12
12
|
from .task_error import TaskError
|
|
13
13
|
from .task_options import TaskOptions
|
|
14
|
+
from .task_parameter import TaskParameter
|
|
14
15
|
from .tasks import Tasks
|
|
15
16
|
|
|
16
17
|
__all__ = (
|
|
@@ -25,5 +26,6 @@ __all__ = (
|
|
|
25
26
|
"TaskComplete",
|
|
26
27
|
"TaskError",
|
|
27
28
|
"TaskOptions",
|
|
29
|
+
"TaskParameter",
|
|
28
30
|
"Tasks",
|
|
29
31
|
)
|
|
@@ -8,6 +8,7 @@ from ..types import UNSET, Unset
|
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
10
|
from ..models.task_options import TaskOptions
|
|
11
|
+
from ..models.task_parameter import TaskParameter
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
T = TypeVar("T", bound="Task")
|
|
@@ -19,10 +20,12 @@ class Task:
|
|
|
19
20
|
Attributes:
|
|
20
21
|
name (str):
|
|
21
22
|
options (Union[Unset, TaskOptions]):
|
|
23
|
+
parameters (Union[Unset, list['TaskParameter']]): Parameter schema extracted from the task function signature
|
|
22
24
|
"""
|
|
23
25
|
|
|
24
26
|
name: str
|
|
25
27
|
options: Union[Unset, "TaskOptions"] = UNSET
|
|
28
|
+
parameters: Union[Unset, list["TaskParameter"]] = UNSET
|
|
26
29
|
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
|
|
27
30
|
|
|
28
31
|
def to_dict(self) -> dict[str, Any]:
|
|
@@ -32,6 +35,13 @@ class Task:
|
|
|
32
35
|
if not isinstance(self.options, Unset):
|
|
33
36
|
options = self.options.to_dict()
|
|
34
37
|
|
|
38
|
+
parameters: Union[Unset, list[dict[str, Any]]] = UNSET
|
|
39
|
+
if not isinstance(self.parameters, Unset):
|
|
40
|
+
parameters = []
|
|
41
|
+
for parameters_item_data in self.parameters:
|
|
42
|
+
parameters_item = parameters_item_data.to_dict()
|
|
43
|
+
parameters.append(parameters_item)
|
|
44
|
+
|
|
35
45
|
field_dict: dict[str, Any] = {}
|
|
36
46
|
field_dict.update(self.additional_properties)
|
|
37
47
|
field_dict.update(
|
|
@@ -41,12 +51,15 @@ class Task:
|
|
|
41
51
|
)
|
|
42
52
|
if options is not UNSET:
|
|
43
53
|
field_dict["options"] = options
|
|
54
|
+
if parameters is not UNSET:
|
|
55
|
+
field_dict["parameters"] = parameters
|
|
44
56
|
|
|
45
57
|
return field_dict
|
|
46
58
|
|
|
47
59
|
@classmethod
|
|
48
60
|
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
|
49
61
|
from ..models.task_options import TaskOptions
|
|
62
|
+
from ..models.task_parameter import TaskParameter
|
|
50
63
|
|
|
51
64
|
d = dict(src_dict)
|
|
52
65
|
name = d.pop("name")
|
|
@@ -58,9 +71,17 @@ class Task:
|
|
|
58
71
|
else:
|
|
59
72
|
options = TaskOptions.from_dict(_options)
|
|
60
73
|
|
|
74
|
+
parameters = []
|
|
75
|
+
_parameters = d.pop("parameters", UNSET)
|
|
76
|
+
for parameters_item_data in _parameters or []:
|
|
77
|
+
parameters_item = TaskParameter.from_dict(parameters_item_data)
|
|
78
|
+
|
|
79
|
+
parameters.append(parameters_item)
|
|
80
|
+
|
|
61
81
|
task = cls(
|
|
62
82
|
name=name,
|
|
63
83
|
options=options,
|
|
84
|
+
parameters=parameters,
|
|
64
85
|
)
|
|
65
86
|
|
|
66
87
|
task.additional_properties = d
|
|
@@ -18,9 +18,13 @@ class TaskOptions:
|
|
|
18
18
|
"""
|
|
19
19
|
Attributes:
|
|
20
20
|
retry (Union[Unset, RetryConfig]):
|
|
21
|
+
timeout_seconds (Union[Unset, int]): Task execution timeout in seconds (30-86400)
|
|
22
|
+
plan (Union[Unset, str]): Resource plan for task execution
|
|
21
23
|
"""
|
|
22
24
|
|
|
23
25
|
retry: Union[Unset, "RetryConfig"] = UNSET
|
|
26
|
+
timeout_seconds: Union[Unset, int] = UNSET
|
|
27
|
+
plan: Union[Unset, str] = UNSET
|
|
24
28
|
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
|
|
25
29
|
|
|
26
30
|
def to_dict(self) -> dict[str, Any]:
|
|
@@ -28,11 +32,19 @@ class TaskOptions:
|
|
|
28
32
|
if not isinstance(self.retry, Unset):
|
|
29
33
|
retry = self.retry.to_dict()
|
|
30
34
|
|
|
35
|
+
timeout_seconds = self.timeout_seconds
|
|
36
|
+
|
|
37
|
+
plan = self.plan
|
|
38
|
+
|
|
31
39
|
field_dict: dict[str, Any] = {}
|
|
32
40
|
field_dict.update(self.additional_properties)
|
|
33
41
|
field_dict.update({})
|
|
34
42
|
if retry is not UNSET:
|
|
35
43
|
field_dict["retry"] = retry
|
|
44
|
+
if timeout_seconds is not UNSET:
|
|
45
|
+
field_dict["timeout_seconds"] = timeout_seconds
|
|
46
|
+
if plan is not UNSET:
|
|
47
|
+
field_dict["plan"] = plan
|
|
36
48
|
|
|
37
49
|
return field_dict
|
|
38
50
|
|
|
@@ -48,8 +60,14 @@ class TaskOptions:
|
|
|
48
60
|
else:
|
|
49
61
|
retry = RetryConfig.from_dict(_retry)
|
|
50
62
|
|
|
63
|
+
timeout_seconds = d.pop("timeout_seconds", UNSET)
|
|
64
|
+
|
|
65
|
+
plan = d.pop("plan", UNSET)
|
|
66
|
+
|
|
51
67
|
task_options = cls(
|
|
52
68
|
retry=retry,
|
|
69
|
+
timeout_seconds=timeout_seconds,
|
|
70
|
+
plan=plan,
|
|
53
71
|
)
|
|
54
72
|
|
|
55
73
|
task_options.additional_properties = d
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import Any, TypeVar, Union
|
|
3
|
+
|
|
4
|
+
from attrs import define as _attrs_define
|
|
5
|
+
from attrs import field as _attrs_field
|
|
6
|
+
|
|
7
|
+
from ..types import UNSET, Unset
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T", bound="TaskParameter")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@_attrs_define
|
|
13
|
+
class TaskParameter:
|
|
14
|
+
"""Information about a task parameter extracted from function signature
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
name (str): Parameter name
|
|
18
|
+
has_default (bool): Whether the parameter has a default value
|
|
19
|
+
type_ (Union[Unset, str]): String representation of the parameter type hint
|
|
20
|
+
default_value (Union[Unset, str]): JSON-encoded default value (if has_default is true)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
name: str
|
|
24
|
+
has_default: bool
|
|
25
|
+
type_: Union[Unset, str] = UNSET
|
|
26
|
+
default_value: Union[Unset, str] = UNSET
|
|
27
|
+
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> dict[str, Any]:
|
|
30
|
+
name = self.name
|
|
31
|
+
|
|
32
|
+
has_default = self.has_default
|
|
33
|
+
|
|
34
|
+
type_ = self.type_
|
|
35
|
+
|
|
36
|
+
default_value = self.default_value
|
|
37
|
+
|
|
38
|
+
field_dict: dict[str, Any] = {}
|
|
39
|
+
field_dict.update(self.additional_properties)
|
|
40
|
+
field_dict.update(
|
|
41
|
+
{
|
|
42
|
+
"name": name,
|
|
43
|
+
"has_default": has_default,
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
if type_ is not UNSET:
|
|
47
|
+
field_dict["type"] = type_
|
|
48
|
+
if default_value is not UNSET:
|
|
49
|
+
field_dict["default_value"] = default_value
|
|
50
|
+
|
|
51
|
+
return field_dict
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
|
55
|
+
d = dict(src_dict)
|
|
56
|
+
name = d.pop("name")
|
|
57
|
+
|
|
58
|
+
has_default = d.pop("has_default")
|
|
59
|
+
|
|
60
|
+
type_ = d.pop("type", UNSET)
|
|
61
|
+
|
|
62
|
+
default_value = d.pop("default_value", UNSET)
|
|
63
|
+
|
|
64
|
+
task_parameter = cls(
|
|
65
|
+
name=name,
|
|
66
|
+
has_default=has_default,
|
|
67
|
+
type_=type_,
|
|
68
|
+
default_value=default_value,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
task_parameter.additional_properties = d
|
|
72
|
+
return task_parameter
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def additional_keys(self) -> list[str]:
|
|
76
|
+
return list(self.additional_properties.keys())
|
|
77
|
+
|
|
78
|
+
def __getitem__(self, key: str) -> Any:
|
|
79
|
+
return self.additional_properties[key]
|
|
80
|
+
|
|
81
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
82
|
+
self.additional_properties[key] = value
|
|
83
|
+
|
|
84
|
+
def __delitem__(self, key: str) -> None:
|
|
85
|
+
del self.additional_properties[key]
|
|
86
|
+
|
|
87
|
+
def __contains__(self, key: str) -> bool:
|
|
88
|
+
return key in self.additional_properties
|
|
@@ -1 +1 @@
|
|
|
1
|
-
# Marker file for PEP 561
|
|
1
|
+
# Marker file for PEP 561
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""CLI entrypoint for running Render Workflows.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
render-workflows myapp:app
|
|
5
|
+
render-workflows myapp.tasks:app
|
|
6
|
+
|
|
7
|
+
This is an alternative to auto_start=True. It provides explicit control
|
|
8
|
+
over when and how the worker starts.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> None:
|
|
18
|
+
"""Main CLI entrypoint."""
|
|
19
|
+
if len(sys.argv) < 2:
|
|
20
|
+
print("Usage: render-workflows <module:app>", file=sys.stderr)
|
|
21
|
+
print("Example: render-workflows myapp:app", file=sys.stderr)
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
|
|
24
|
+
app_path = sys.argv[1]
|
|
25
|
+
|
|
26
|
+
if ":" not in app_path:
|
|
27
|
+
print(f"Error: Invalid app path '{app_path}'", file=sys.stderr)
|
|
28
|
+
print("Expected format: <module>:<app_variable>", file=sys.stderr)
|
|
29
|
+
print("Example: render-workflows myapp:app", file=sys.stderr)
|
|
30
|
+
sys.exit(1)
|
|
31
|
+
|
|
32
|
+
module_path, app_name = app_path.rsplit(":", 1)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
module = importlib.import_module(module_path)
|
|
36
|
+
except ImportError as e:
|
|
37
|
+
print(f"Error: Could not import module '{module_path}': {e}", file=sys.stderr)
|
|
38
|
+
sys.exit(1)
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
app = getattr(module, app_name)
|
|
42
|
+
except AttributeError:
|
|
43
|
+
print(
|
|
44
|
+
f"Error: Module '{module_path}' has no attribute '{app_name}'",
|
|
45
|
+
file=sys.stderr,
|
|
46
|
+
)
|
|
47
|
+
sys.exit(1)
|
|
48
|
+
|
|
49
|
+
if not hasattr(app, "start"):
|
|
50
|
+
print(f"Error: '{app_name}' does not have a start() method", file=sys.stderr)
|
|
51
|
+
print("Expected a Workflows instance", file=sys.stderr)
|
|
52
|
+
sys.exit(1)
|
|
53
|
+
|
|
54
|
+
app.start()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
main()
|
render_sdk/workflows/client.py
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import base64
|
|
5
|
-
import importlib.metadata
|
|
6
5
|
import json
|
|
7
6
|
import time
|
|
8
7
|
from dataclasses import dataclass
|
|
@@ -13,6 +12,7 @@ import httpx
|
|
|
13
12
|
|
|
14
13
|
from render_sdk.client.errors import RenderError, TaskRunError
|
|
15
14
|
from render_sdk.client.util import handle_http_errors
|
|
15
|
+
from render_sdk.version import get_user_agent
|
|
16
16
|
from render_sdk.workflows.callback_api.api.default import (
|
|
17
17
|
get_input,
|
|
18
18
|
post_callback,
|
|
@@ -65,11 +65,6 @@ POLLING_INTERVAL = 1.0
|
|
|
65
65
|
POLLING_TIMEOUT = 24 * 60 * 60 # 24 hours
|
|
66
66
|
QUERY_TIMEOUT = 15 # 15 seconds
|
|
67
67
|
|
|
68
|
-
try:
|
|
69
|
-
version = importlib.metadata.version("render")
|
|
70
|
-
except importlib.metadata.PackageNotFoundError:
|
|
71
|
-
version = "unknown" # fallback version
|
|
72
|
-
|
|
73
68
|
|
|
74
69
|
class UDSClient:
|
|
75
70
|
"""Client for communicating with the SDK server over Unix Domain Socket."""
|
|
@@ -80,7 +75,7 @@ class UDSClient:
|
|
|
80
75
|
def get_client(self) -> Client:
|
|
81
76
|
return Client(
|
|
82
77
|
base_url="http://localhost",
|
|
83
|
-
headers={"User-Agent":
|
|
78
|
+
headers={"User-Agent": get_user_agent()},
|
|
84
79
|
httpx_args={
|
|
85
80
|
"transport": httpx.AsyncHTTPTransport(uds=self.socket_path),
|
|
86
81
|
},
|
|
@@ -134,13 +129,17 @@ class UDSClient:
|
|
|
134
129
|
async with self.get_client() as client:
|
|
135
130
|
return await post_callback.asyncio_detailed(client=client, body=data)
|
|
136
131
|
|
|
137
|
-
async def run_subtask(
|
|
132
|
+
async def run_subtask(
|
|
133
|
+
self, task_name: str, input_data: list[Any] | dict[str, Any] | None = None
|
|
134
|
+
) -> Any:
|
|
138
135
|
"""
|
|
139
136
|
Run a subtask and wait for its completion.
|
|
140
137
|
|
|
141
138
|
Args:
|
|
142
139
|
task_name: Name of the task to run
|
|
143
|
-
input_data: Input data to pass to the task
|
|
140
|
+
input_data: Input data to pass to the task. Can be either:
|
|
141
|
+
- A list for positional arguments: [arg1, arg2, arg3]
|
|
142
|
+
- A dict for named parameters: {"param1": value1, "param2": value2}
|
|
144
143
|
|
|
145
144
|
Returns:
|
|
146
145
|
The result of the subtask execution
|
render_sdk/workflows/executor.py
CHANGED
|
@@ -17,7 +17,9 @@ class TaskExecutor:
|
|
|
17
17
|
self.task_registry = task_registry
|
|
18
18
|
self.client = client
|
|
19
19
|
|
|
20
|
-
async def _execute_task(
|
|
20
|
+
async def _execute_task(
|
|
21
|
+
self, task_name: str, input_data: list[Any] | dict[str, Any]
|
|
22
|
+
) -> Any:
|
|
21
23
|
"""Execute a task by name with the given input."""
|
|
22
24
|
func = self.task_registry.get_function(task_name)
|
|
23
25
|
if not func:
|
|
@@ -28,11 +30,19 @@ class TaskExecutor:
|
|
|
28
30
|
context = _current_client.set(self.client)
|
|
29
31
|
|
|
30
32
|
try:
|
|
31
|
-
#
|
|
32
|
-
if
|
|
33
|
-
|
|
33
|
+
# Determine how to call the function based on input type
|
|
34
|
+
if isinstance(input_data, dict):
|
|
35
|
+
# Named parameters: pass as keyword arguments
|
|
36
|
+
if inspect.iscoroutinefunction(func):
|
|
37
|
+
result = await func(**input_data)
|
|
38
|
+
else:
|
|
39
|
+
result = func(**input_data)
|
|
34
40
|
else:
|
|
35
|
-
|
|
41
|
+
# Positional parameters: unpack list
|
|
42
|
+
if inspect.iscoroutinefunction(func):
|
|
43
|
+
result = await func(*input_data)
|
|
44
|
+
else:
|
|
45
|
+
result = func(*input_data)
|
|
36
46
|
|
|
37
47
|
return TaskResult(result=result)
|
|
38
48
|
finally:
|
|
@@ -42,7 +52,9 @@ class TaskExecutor:
|
|
|
42
52
|
except Exception as e:
|
|
43
53
|
return TaskResult(error=e)
|
|
44
54
|
|
|
45
|
-
async def execute(
|
|
55
|
+
async def execute(
|
|
56
|
+
self, task_name: str, input_data: list[Any] | dict[str, Any]
|
|
57
|
+
) -> Any:
|
|
46
58
|
"""Execute a task by name with the given input."""
|
|
47
59
|
logger.debug(f"Starting execution of task: {task_name}")
|
|
48
60
|
|
|
@@ -50,7 +62,7 @@ class TaskExecutor:
|
|
|
50
62
|
|
|
51
63
|
try:
|
|
52
64
|
# Execute the task
|
|
53
|
-
result = await self._execute_task(task_name,
|
|
65
|
+
result = await self._execute_task(task_name, input_data)
|
|
54
66
|
if result.error:
|
|
55
67
|
# Send error callback and raise the error
|
|
56
68
|
await self._send_error_callback(task_name, result.error)
|