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.
Files changed (156) hide show
  1. render_sdk/__init__.py +41 -4
  2. render_sdk/client/__init__.py +25 -0
  3. render_sdk/client/client.py +5 -0
  4. render_sdk/client/sse.py +5 -1
  5. render_sdk/client/tests/test_client.py +6 -4
  6. render_sdk/client/tests/test_sse.py +1 -0
  7. render_sdk/client/types.py +2 -1
  8. render_sdk/client/workflows.py +13 -3
  9. render_sdk/experimental/__init__.py +31 -0
  10. render_sdk/experimental/experimental.py +71 -0
  11. render_sdk/experimental/object/__init__.py +30 -0
  12. render_sdk/experimental/object/api.py +260 -0
  13. render_sdk/experimental/object/client.py +475 -0
  14. render_sdk/experimental/object/types.py +87 -0
  15. render_sdk/public_api/api/audit_logs/list_organization_audit_logs.py +303 -0
  16. render_sdk/public_api/api/audit_logs/list_owner_audit_logs.py +303 -0
  17. render_sdk/public_api/api/blob_storage/delete_blob.py +215 -0
  18. render_sdk/public_api/api/blob_storage/get_blob.py +221 -0
  19. render_sdk/public_api/api/{workflows/list_workflow_versions.py → blob_storage/list_blobs.py} +52 -30
  20. render_sdk/public_api/api/blob_storage/put_blob.py +248 -0
  21. render_sdk/public_api/api/blueprints/validate_blueprint.py +212 -0
  22. render_sdk/public_api/api/key_value/resume_key_value.py +203 -0
  23. render_sdk/public_api/api/key_value/suspend_key_value.py +203 -0
  24. render_sdk/public_api/api/metrics/get_bandwidth_sources.py +251 -0
  25. render_sdk/public_api/api/postgres/create_postgres_user.py +229 -0
  26. render_sdk/public_api/api/postgres/delete_postgres_user.py +201 -0
  27. render_sdk/public_api/api/postgres/list_postgres_users.py +195 -0
  28. render_sdk/public_api/api/redis_deprecated/__init__.py +1 -0
  29. render_sdk/public_api/api/{redis → redis_deprecated}/create_redis.py +4 -4
  30. render_sdk/public_api/api/{redis → redis_deprecated}/delete_redis.py +4 -4
  31. render_sdk/public_api/api/{redis → redis_deprecated}/list_redis.py +4 -0
  32. render_sdk/public_api/api/{redis → redis_deprecated}/retrieve_redis.py +4 -4
  33. render_sdk/public_api/api/{redis → redis_deprecated}/retrieve_redis_connection_info.py +4 -0
  34. render_sdk/public_api/api/{redis → redis_deprecated}/update_redis.py +4 -4
  35. render_sdk/public_api/api/services/create_service.py +4 -4
  36. render_sdk/public_api/api/workflow_tasks_ea/__init__.py +1 -0
  37. render_sdk/public_api/api/{workflows → workflow_tasks_ea}/cancel_task_run.py +12 -4
  38. render_sdk/public_api/api/{workflows → workflow_tasks_ea}/create_task.py +12 -4
  39. render_sdk/public_api/api/{workflows → workflow_tasks_ea}/get_task.py +12 -4
  40. render_sdk/public_api/api/{workflows → workflow_tasks_ea}/get_task_run.py +12 -4
  41. render_sdk/public_api/api/{workflows → workflow_tasks_ea}/list_task_runs.py +12 -0
  42. render_sdk/public_api/api/{workflows → workflow_tasks_ea}/list_tasks.py +24 -12
  43. render_sdk/public_api/api/workflows_ea/__init__.py +1 -0
  44. render_sdk/public_api/api/workflows_ea/create_workflow.py +199 -0
  45. render_sdk/public_api/api/{workflows/deploy_workflow.py → workflows_ea/create_workflow_version.py} +31 -14
  46. render_sdk/public_api/api/{workflows → workflows_ea}/delete_workflow.py +12 -4
  47. render_sdk/public_api/api/{workflows → workflows_ea}/get_workflow.py +32 -14
  48. render_sdk/public_api/api/{workflows → workflows_ea}/get_workflow_version.py +12 -4
  49. render_sdk/public_api/api/workflows_ea/list_workflow_versions.py +275 -0
  50. render_sdk/public_api/api/{workflows → workflows_ea}/list_workflows.py +41 -14
  51. render_sdk/public_api/api/workflows_ea/update_workflow.py +212 -0
  52. render_sdk/public_api/api/workspaces/remove_workspace_member.py +206 -0
  53. render_sdk/public_api/api/workspaces/update_workspace_member.py +235 -0
  54. render_sdk/public_api/models/__init__.py +82 -4
  55. render_sdk/public_api/models/audit_log.py +113 -0
  56. render_sdk/public_api/models/audit_log_actor.py +80 -0
  57. render_sdk/public_api/models/audit_log_actor_type.py +10 -0
  58. render_sdk/public_api/models/audit_log_event.py +80 -0
  59. render_sdk/public_api/models/audit_log_metadata.py +49 -0
  60. render_sdk/public_api/models/audit_log_status.py +9 -0
  61. render_sdk/public_api/models/audit_log_with_cursor.py +73 -0
  62. render_sdk/public_api/models/background_worker_details.py +2 -2
  63. render_sdk/public_api/models/background_worker_details_patch.py +1 -1
  64. render_sdk/public_api/models/background_worker_details_post.py +1 -1
  65. render_sdk/public_api/models/blob_metadata.py +85 -0
  66. render_sdk/public_api/models/blob_with_cursor.py +73 -0
  67. render_sdk/public_api/models/cache.py +6 -4
  68. render_sdk/public_api/models/cache_profile.py +10 -0
  69. render_sdk/public_api/models/create_deploy_body.py +23 -0
  70. render_sdk/public_api/models/create_version.py +70 -0
  71. render_sdk/public_api/models/credential_create_input.py +59 -0
  72. render_sdk/public_api/models/cron_job_details.py +2 -2
  73. render_sdk/public_api/models/cron_job_details_patch.py +1 -1
  74. render_sdk/public_api/models/cron_job_details_post.py +1 -1
  75. render_sdk/public_api/models/deploy_mode.py +9 -0
  76. render_sdk/public_api/models/event.py +11 -27
  77. render_sdk/public_api/models/event_type.py +1 -1
  78. render_sdk/public_api/models/get_bandwidth_sources_response_200.py +75 -0
  79. render_sdk/public_api/models/get_bandwidth_sources_response_200_data_item.py +101 -0
  80. render_sdk/public_api/models/get_bandwidth_sources_response_200_data_item_labels.py +78 -0
  81. render_sdk/public_api/models/get_bandwidth_sources_response_200_data_item_labels_traffic_source.py +12 -0
  82. render_sdk/public_api/models/get_bandwidth_sources_response_200_data_item_values_item.py +68 -0
  83. render_sdk/public_api/models/{server_unhealthy.py → get_bandwidth_sources_response_400.py} +12 -12
  84. render_sdk/public_api/models/get_blob_output.py +71 -0
  85. render_sdk/public_api/models/list_postgres_users_response_200_item.py +86 -0
  86. render_sdk/public_api/models/otel_provider_type.py +2 -0
  87. render_sdk/public_api/models/postgres.py +8 -0
  88. render_sdk/public_api/models/postgres_detail.py +26 -0
  89. render_sdk/public_api/models/postgres_parameter_overrides.py +44 -0
  90. render_sdk/public_api/models/postgres_patch_input.py +27 -0
  91. render_sdk/public_api/models/postgres_post_input.py +27 -0
  92. render_sdk/public_api/models/postgres_version.py +1 -0
  93. render_sdk/public_api/models/preview_input.py +2 -2
  94. render_sdk/public_api/models/private_service_details.py +2 -2
  95. render_sdk/public_api/models/private_service_details_patch.py +1 -1
  96. render_sdk/public_api/models/private_service_details_post.py +1 -1
  97. render_sdk/public_api/models/project_post_environment_input.py +26 -1
  98. render_sdk/public_api/models/put_blob_input.py +59 -0
  99. render_sdk/public_api/models/put_blob_output.py +79 -0
  100. render_sdk/public_api/models/read_replica.py +25 -1
  101. render_sdk/public_api/models/read_replica_input.py +25 -1
  102. render_sdk/public_api/models/run_task.py +35 -7
  103. render_sdk/public_api/models/service_event.py +12 -27
  104. render_sdk/public_api/models/service_event_type.py +0 -1
  105. render_sdk/public_api/models/service_post.py +9 -6
  106. render_sdk/public_api/models/task_attempt.py +88 -0
  107. render_sdk/public_api/models/task_attempt_details.py +108 -0
  108. render_sdk/public_api/models/task_data_type_1.py +44 -0
  109. render_sdk/public_api/models/task_run.py +23 -1
  110. render_sdk/public_api/models/task_run_details.py +50 -5
  111. render_sdk/public_api/models/task_run_status.py +1 -0
  112. render_sdk/public_api/models/task_with_cursor.py +73 -0
  113. render_sdk/public_api/models/team_member.py +5 -4
  114. render_sdk/public_api/models/team_member_role.py +12 -0
  115. render_sdk/public_api/models/update_workspace_member_body.py +61 -0
  116. render_sdk/public_api/models/validate_blueprint_request.py +84 -0
  117. render_sdk/public_api/models/validate_blueprint_response.py +105 -0
  118. render_sdk/public_api/models/validation_error.py +88 -0
  119. render_sdk/public_api/models/validation_plan_summary.py +107 -0
  120. render_sdk/public_api/models/web_service_details.py +2 -2
  121. render_sdk/public_api/models/web_service_details_patch.py +6 -5
  122. render_sdk/public_api/models/web_service_details_post.py +6 -5
  123. render_sdk/public_api/models/workflow.py +144 -0
  124. render_sdk/public_api/models/workflow_create.py +99 -0
  125. render_sdk/public_api/models/workflow_update.py +90 -0
  126. render_sdk/public_api/models/workflow_version.py +10 -14
  127. render_sdk/public_api/models/workflow_version_status.py +13 -0
  128. render_sdk/public_api/models/workflow_version_with_cursor.py +73 -0
  129. render_sdk/public_api/models/workflow_with_cursor.py +73 -0
  130. render_sdk/render.py +65 -0
  131. render_sdk/version.py +27 -0
  132. render_sdk/workflows/__init__.py +5 -1
  133. render_sdk/workflows/app.py +262 -0
  134. render_sdk/workflows/callback_api/models/__init__.py +2 -0
  135. render_sdk/workflows/callback_api/models/task.py +21 -0
  136. render_sdk/workflows/callback_api/models/task_options.py +18 -0
  137. render_sdk/workflows/callback_api/models/task_parameter.py +88 -0
  138. render_sdk/workflows/callback_api/py.typed +1 -1
  139. render_sdk/workflows/cli.py +58 -0
  140. render_sdk/workflows/client.py +8 -9
  141. render_sdk/workflows/executor.py +19 -7
  142. render_sdk/workflows/runner.py +43 -10
  143. render_sdk/workflows/task.py +84 -5
  144. render_sdk/workflows/tests/test_app.py +412 -0
  145. render_sdk/workflows/tests/test_cli.py +134 -0
  146. render_sdk/workflows/tests/test_end_to_end.py +71 -1
  147. render_sdk/workflows/tests/test_registration.py +58 -1
  148. {render_sdk-0.1.2.dist-info → render_sdk-0.2.0.dist-info}/METADATA +4 -3
  149. {render_sdk-0.1.2.dist-info → render_sdk-0.2.0.dist-info}/RECORD +155 -83
  150. {render_sdk-0.1.2.dist-info → render_sdk-0.2.0.dist-info}/WHEEL +1 -1
  151. render_sdk-0.2.0.dist-info/entry_points.txt +3 -0
  152. render_sdk/public_api/models/image_version.py +0 -79
  153. /render_sdk/public_api/api/{redis → audit_logs}/__init__.py +0 -0
  154. /render_sdk/public_api/api/{workflows → blob_storage}/__init__.py +0 -0
  155. /render_sdk/public_api/api/{workflows → workflow_tasks_ea}/stream_task_runs_events.py +0 -0
  156. {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()
@@ -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": f"render-workflows-python-sdk/{version}"},
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(self, task_name: str, input_data: Any = None) -> Any:
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
@@ -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(self, task_name: str, input_args: list[Any]) -> Any:
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
- # Check if the function is async
32
- if inspect.iscoroutinefunction(func):
33
- result = await func(*input_args)
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
- result = func(*input_args)
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(self, task_name: str, input_args: list[Any]) -> Any:
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, input_args)
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)