UncountablePythonSDK 0.0.164__py3-none-any.whl → 0.0.166__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.
@@ -3,12 +3,14 @@ from .dataclasses import dict_fields as dict_fields
3
3
  from .dataclasses import iterate_fields as iterate_fields
4
4
  from .serialization_helpers import (
5
5
  JsonValue,
6
+ convert_decimals_to_strings,
6
7
  serialize_for_api,
7
8
  serialize_for_storage,
8
9
  serialize_for_storage_dict,
9
10
  )
10
11
 
11
12
  __all__: list[str] = [
13
+ "convert_decimals_to_strings",
12
14
  "convert_dict_to_snake_case",
13
15
  "serialize_for_api",
14
16
  "serialize_for_storage",
@@ -14,6 +14,7 @@ from typing import (
14
14
  Protocol,
15
15
  TypeVar,
16
16
  Union,
17
+ assert_never,
17
18
  overload,
18
19
  )
19
20
 
@@ -37,6 +38,30 @@ else:
37
38
  T = TypeVar("T")
38
39
 
39
40
 
41
+ # IMPROVE: this match should be exhaustive over JsonValue, but mypy can't verify
42
+ # that Mapping()/Sequence() fully narrow the TYPE_CHECKING definition of JsonValue.
43
+ def convert_decimals_to_strings(obj: JsonValue) -> JsonValue:
44
+ match obj:
45
+ case Decimal():
46
+ return str(obj)
47
+ case (
48
+ str()
49
+ | int()
50
+ | float()
51
+ | bool()
52
+ | None
53
+ | datetime.datetime()
54
+ | datetime.date()
55
+ ):
56
+ return obj
57
+ case Mapping():
58
+ return {k: convert_decimals_to_strings(v) for k, v in obj.items()}
59
+ case Sequence():
60
+ return [convert_decimals_to_strings(v) for v in obj]
61
+ case _ as unreachable:
62
+ assert_never(unreachable)
63
+
64
+
40
65
  class Dataclass(Protocol):
41
66
  __dataclass_fields__: ClassVar[dict] # type: ignore[type-arg,unused-ignore]
42
67
 
@@ -144,11 +144,10 @@ def asdict_for_yaml_dump(dataclass_instance: Any) -> Any:
144
144
  def emit_type_info_python(build: builder.SpecBuilder, output: str) -> None:
145
145
  type_map = _build_map_all(build, python=True)
146
146
 
147
- stripped = _dict_null_strip(asdict_for_yaml_dump(type_map))
148
- serialized = serialize_for_storage(stripped)
149
-
150
- yaml_content = yaml.dump(serialized, default_flow_style=False, sort_keys=True)
151
- util.rewrite_file(f"{output}/type_map.yaml", yaml_content)
147
+ for namespace, data in type_map.namespaces.items():
148
+ serial = serialize_for_storage(asdict_for_yaml_dump(data))
149
+ yaml_content = yaml.dump(serial, default_flow_style=False, sort_keys=True)
150
+ util.rewrite_file(f"{output}/namespace_{namespace}.yaml", yaml_content)
152
151
 
153
152
 
154
153
  @dataclasses.dataclass
@@ -1,4 +1,6 @@
1
1
  import asyncio
2
+ import logging
3
+ import pathlib
2
4
  import sys
3
5
  import typing
4
6
  from concurrent.futures import ProcessPoolExecutor
@@ -105,14 +107,48 @@ def _resolve_queued_job_request_context(
105
107
  return None
106
108
 
107
109
 
110
+ _CGROUP_V2_MEMORY_MAX = pathlib.Path("/sys/fs/cgroup/memory.max")
111
+ _CGROUP_V1_MEMORY_LIMIT = pathlib.Path("/sys/fs/cgroup/memory/memory.limit_in_bytes")
112
+
113
+ _MEMORY_LIMIT_FRACTION_CGROUP = 0.75
114
+ _MEMORY_LIMIT_FRACTION_HOST = 0.9
115
+
116
+ _logger = logging.getLogger(__name__)
117
+
118
+
119
+ def _get_cgroup_memory_limit_bytes() -> int | None:
120
+ for cgroup_path in (_CGROUP_V2_MEMORY_MAX, _CGROUP_V1_MEMORY_LIMIT):
121
+ if not cgroup_path.exists():
122
+ continue
123
+ raw = cgroup_path.read_text().strip()
124
+ if raw == "max":
125
+ # cgroups v2 "max" means no limit is set
126
+ return None
127
+ value = int(raw)
128
+ # cgroups v1 uses a very large sentinel for "no limit"
129
+ if value >= 2**63 - 1:
130
+ return None
131
+ return value
132
+ return None
133
+
134
+
108
135
  def run_queued_job(
109
136
  queued_job: queued_job_t.QueuedJob,
110
137
  ) -> job_definition_t.JobResult:
111
138
  with get_otel_tracer().start_as_current_span(name="run_queued_job") as span:
112
139
  if resource is not None:
113
- total_mem = psutil.virtual_memory().total
114
- limit_bytes = int(total_mem * 0.9)
140
+ cgroup_limit = _get_cgroup_memory_limit_bytes()
141
+ if cgroup_limit is not None:
142
+ limit_bytes = int(cgroup_limit * _MEMORY_LIMIT_FRACTION_CGROUP)
143
+ else:
144
+ total_mem = psutil.virtual_memory().total
145
+ limit_bytes = int(total_mem * _MEMORY_LIMIT_FRACTION_HOST)
115
146
  resource.setrlimit(resource.RLIMIT_AS, (limit_bytes, limit_bytes))
147
+ _logger.info(
148
+ "RLIMIT_AS set to %.2f GiB (source: %s)",
149
+ limit_bytes / (1024**3),
150
+ "cgroup" if cgroup_limit is not None else "host",
151
+ )
116
152
 
117
153
  job_details = get_registered_job_details(queued_job.job_ref_name)
118
154
  job_logger = JobLogger(
@@ -220,9 +220,15 @@ class Logger:
220
220
 
221
221
 
222
222
  class PerJobResourceTracker:
223
- def __init__(self, logger: "JobLogger", sample_interval: float = 0.5) -> None:
223
+ def __init__(
224
+ self,
225
+ logger: "JobLogger",
226
+ sample_interval: float = 0.5,
227
+ log_interval: float = 30.0,
228
+ ) -> None:
224
229
  self.logger = logger
225
230
  self.sample_interval = sample_interval
231
+ self.log_interval = log_interval
226
232
  self._process = psutil.Process(os.getpid())
227
233
  self._stop_event = threading.Event()
228
234
  self._thread: threading.Thread | None = None
@@ -233,15 +239,40 @@ class PerJobResourceTracker:
233
239
  self.start_wall_time: float | None = None
234
240
  self.end_wall_time: float | None = None
235
241
 
242
+ def _current_stats(self) -> Attributes:
243
+ assert self.start_cpu_times is not None
244
+ assert self.start_wall_time is not None
245
+ current_cpu_times = self._process.cpu_times()
246
+ cpu_user = current_cpu_times.user - self.start_cpu_times.user
247
+ cpu_sys = current_cpu_times.system - self.start_cpu_times.system
248
+ cpu_total = cpu_user + cpu_sys
249
+ elapsed = time.monotonic() - self.start_wall_time
250
+ return {
251
+ "cpu_user_s": round(cpu_user, 3),
252
+ "cpu_system_s": round(cpu_sys, 3),
253
+ "cpu_total_s": round(cpu_total, 3),
254
+ "wall_time_s": round(elapsed, 3),
255
+ "current_rss_mb": round(self._process.memory_info().rss / (1024 * 1024), 2),
256
+ "peak_rss_mb": round(self.max_rss / (1024 * 1024), 2),
257
+ }
258
+
236
259
  def start(self) -> None:
237
260
  self.start_cpu_times = self._process.cpu_times()
238
261
  self.start_wall_time = time.monotonic()
239
262
 
240
263
  def _monitor() -> None:
264
+ last_log_time = time.monotonic()
241
265
  try:
242
266
  while not self._stop_event.is_set():
243
267
  rss = self._process.memory_info().rss
244
268
  self.max_rss = max(self.max_rss, rss)
269
+ now = time.monotonic()
270
+ if now - last_log_time >= self.log_interval:
271
+ self.logger.log_info(
272
+ "Job resource usage (periodic)",
273
+ attributes=self._current_stats(),
274
+ )
275
+ last_log_time = now
245
276
  time.sleep(self.sample_interval)
246
277
  except Exception:
247
278
  self._stop_event.set()
@@ -24,8 +24,22 @@ def register_route(
24
24
  job: job_definition_t.HttpJobDefinitionBase,
25
25
  ) -> None:
26
26
  route = f"/{profile_meta.name}/{job.id}"
27
+ log_attributes = {
28
+ "profile.name": profile_meta.name,
29
+ "profile.base_url": profile_meta.base_url,
30
+ "job.name": job.name,
31
+ "job.id": job.id,
32
+ "http.route": route,
33
+ }
27
34
 
28
35
  def handle_request() -> ResponseReturnValue:
36
+ method = flask.request.method
37
+ url = flask.request.url
38
+ request_attributes = {
39
+ **log_attributes,
40
+ "http.method": method,
41
+ "http.url": url,
42
+ }
29
43
  with server_logger.push_scope(route):
30
44
  try:
31
45
  if not isinstance(job.executor, job_definition_t.JobExecutorScript):
@@ -56,10 +70,18 @@ def register_route(
56
70
  http_response.headers,
57
71
  )
58
72
  except HttpException as e:
59
- server_logger.log_exception(e)
73
+ server_logger.log_exception(
74
+ e,
75
+ message=f"HTTP error on {method} {url}",
76
+ attributes=request_attributes,
77
+ )
60
78
  return e.make_error_response()
61
79
  except Exception as e:
62
- server_logger.log_exception(e)
80
+ server_logger.log_exception(
81
+ e,
82
+ message=f"Unexpected error on {method} {url}",
83
+ attributes=request_attributes,
84
+ )
63
85
  return HttpException.unknown_error().make_error_response()
64
86
 
65
87
  app.add_url_rule(
@@ -69,7 +91,9 @@ def register_route(
69
91
  methods=["POST"],
70
92
  )
71
93
 
72
- server_logger.log_info(f"job {job.id} webhook registered at: {route}")
94
+ server_logger.log_info(
95
+ f"job {job.id} webhook registered at: {route}", attributes=log_attributes
96
+ )
73
97
 
74
98
 
75
99
  def main() -> None:
@@ -135,6 +135,7 @@ from .api.entity import set_values as set_values_t
135
135
  from . import sockets_t as sockets_t
136
136
  from . import specs_t as specs_t
137
137
  from . import step_relationships_t as step_relationships_t
138
+ from .api.output_parameters import swap_output_condition_parameters as swap_output_condition_parameters_t
138
139
  from .api.entity import transition_entity_phase as transition_entity_phase_t
139
140
  from .api.recipes import unarchive_recipes as unarchive_recipes_t
140
141
  from . import units_t as units_t
@@ -286,6 +287,7 @@ __all__: list[str] = [
286
287
  "sockets_t",
287
288
  "specs_t",
288
289
  "step_relationships_t",
290
+ "swap_output_condition_parameters_t",
289
291
  "transition_entity_phase_t",
290
292
  "unarchive_recipes_t",
291
293
  "units_t",
@@ -0,0 +1 @@
1
+ # DO NOT MODIFY -- This file is generated by type_spec
@@ -0,0 +1,150 @@
1
+ # DO NOT MODIFY -- This file is generated by type_spec
2
+ # ruff: noqa: E402 Q003
3
+ # fmt: off
4
+ # isort: skip_file
5
+ from __future__ import annotations
6
+ import typing # noqa: F401
7
+ import datetime # noqa: F401
8
+ from decimal import Decimal # noqa: F401
9
+ from enum import StrEnum
10
+ import dataclasses
11
+ from pkgs.serialization import serial_class
12
+ from pkgs.serialization import serial_union_annotation
13
+ from ... import async_batch_t
14
+ from ... import base_t
15
+ from ... import identifier_t
16
+
17
+ __all__: list[str] = [
18
+ "AllInProjectSwapScope",
19
+ "Arguments",
20
+ "ConditionParameterDefinition",
21
+ "Data",
22
+ "ENDPOINT_METHOD",
23
+ "ENDPOINT_PATH",
24
+ "NewOutputCondition",
25
+ "NewOutputConditionExisting",
26
+ "NewOutputConditionFromDefinition",
27
+ "NewOutputConditionType",
28
+ "SwapScope",
29
+ "SwapScopeType",
30
+ "TargetedSwapScope",
31
+ ]
32
+
33
+ ENDPOINT_METHOD = "POST"
34
+ ENDPOINT_PATH = "api/external/output_parameters/swap_output_condition_parameters"
35
+
36
+
37
+ # DO NOT MODIFY -- This file is generated by type_spec
38
+ @serial_class(
39
+ named_type_path="sdk.api.output_parameters.swap_output_condition_parameters.ConditionParameterDefinition",
40
+ )
41
+ @dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
42
+ class ConditionParameterDefinition:
43
+ condition_parameter_key: identifier_t.IdentifierKey
44
+ value_numeric: Decimal | None = None
45
+ value_text: str | None = None
46
+
47
+
48
+ # DO NOT MODIFY -- This file is generated by type_spec
49
+ class NewOutputConditionType(StrEnum):
50
+ EXISTING = "existing"
51
+ DEFINITION = "definition"
52
+
53
+
54
+ # DO NOT MODIFY -- This file is generated by type_spec
55
+ @serial_class(
56
+ named_type_path="sdk.api.output_parameters.swap_output_condition_parameters.NewOutputConditionExisting",
57
+ parse_require={"type"},
58
+ )
59
+ @dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
60
+ class NewOutputConditionExisting:
61
+ type: typing.Literal[NewOutputConditionType.EXISTING] = NewOutputConditionType.EXISTING
62
+ condition_key: identifier_t.IdentifierKey
63
+
64
+
65
+ # DO NOT MODIFY -- This file is generated by type_spec
66
+ @serial_class(
67
+ named_type_path="sdk.api.output_parameters.swap_output_condition_parameters.NewOutputConditionFromDefinition",
68
+ parse_require={"type"},
69
+ )
70
+ @dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
71
+ class NewOutputConditionFromDefinition:
72
+ type: typing.Literal[NewOutputConditionType.DEFINITION] = NewOutputConditionType.DEFINITION
73
+ parameters: list[ConditionParameterDefinition]
74
+
75
+
76
+ # DO NOT MODIFY -- This file is generated by type_spec
77
+ NewOutputCondition = typing.Annotated[
78
+ NewOutputConditionExisting | NewOutputConditionFromDefinition,
79
+ serial_union_annotation(
80
+ named_type_path="sdk.api.output_parameters.swap_output_condition_parameters.NewOutputCondition",
81
+ discriminator="type",
82
+ discriminator_map={
83
+ "existing": NewOutputConditionExisting,
84
+ "definition": NewOutputConditionFromDefinition,
85
+ },
86
+ ),
87
+ ]
88
+
89
+
90
+ # DO NOT MODIFY -- This file is generated by type_spec
91
+ class SwapScopeType(StrEnum):
92
+ TARGETED = "targeted"
93
+ ALL_IN_PROJECT = "all_in_project"
94
+
95
+
96
+ # DO NOT MODIFY -- This file is generated by type_spec
97
+ @serial_class(
98
+ named_type_path="sdk.api.output_parameters.swap_output_condition_parameters.TargetedSwapScope",
99
+ parse_require={"type"},
100
+ )
101
+ @dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
102
+ class TargetedSwapScope:
103
+ type: typing.Literal[SwapScopeType.TARGETED] = SwapScopeType.TARGETED
104
+ recipe_keys: list[identifier_t.IdentifierKey]
105
+
106
+
107
+ # DO NOT MODIFY -- This file is generated by type_spec
108
+ @serial_class(
109
+ named_type_path="sdk.api.output_parameters.swap_output_condition_parameters.AllInProjectSwapScope",
110
+ parse_require={"type"},
111
+ )
112
+ @dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
113
+ class AllInProjectSwapScope:
114
+ type: typing.Literal[SwapScopeType.ALL_IN_PROJECT] = SwapScopeType.ALL_IN_PROJECT
115
+
116
+
117
+ # DO NOT MODIFY -- This file is generated by type_spec
118
+ SwapScope = typing.Annotated[
119
+ TargetedSwapScope | AllInProjectSwapScope,
120
+ serial_union_annotation(
121
+ named_type_path="sdk.api.output_parameters.swap_output_condition_parameters.SwapScope",
122
+ discriminator="type",
123
+ discriminator_map={
124
+ "targeted": TargetedSwapScope,
125
+ "all_in_project": AllInProjectSwapScope,
126
+ },
127
+ ),
128
+ ]
129
+
130
+
131
+ # DO NOT MODIFY -- This file is generated by type_spec
132
+ @serial_class(
133
+ named_type_path="sdk.api.output_parameters.swap_output_condition_parameters.Arguments",
134
+ )
135
+ @dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
136
+ class Arguments:
137
+ scope: SwapScope
138
+ output_key: identifier_t.IdentifierKey
139
+ new_condition: NewOutputCondition
140
+ old_condition_key: identifier_t.IdentifierKey | None = None
141
+
142
+
143
+ # DO NOT MODIFY -- This file is generated by type_spec
144
+ @serial_class(
145
+ named_type_path="sdk.api.output_parameters.swap_output_condition_parameters.Data",
146
+ )
147
+ @dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
148
+ class Data(async_batch_t.AsyncBatchActionReturn):
149
+ pass
150
+ # DO NOT MODIFY -- This file is generated by type_spec
@@ -40,6 +40,7 @@ import uncountable.types.api.triggers.run_trigger as run_trigger_t
40
40
  import uncountable.types.api.entity.set_barcode as set_barcode_t
41
41
  import uncountable.types.api.entity.set_entity_field_values as set_entity_field_values_t
42
42
  import uncountable.types.api.recipes.set_recipe_metadata as set_recipe_metadata_t
43
+ import uncountable.types.api.output_parameters.swap_output_condition_parameters as swap_output_condition_parameters_t
43
44
  import uncountable.types.api.entity.transition_entity_phase as transition_entity_phase_t
44
45
  import uncountable.types.api.entity.unlock_entity as unlock_entity_t
45
46
  import uncountable.types.api.recipes.unlock_recipes as unlock_recipes_t
@@ -69,7 +70,7 @@ class AsyncBatchProcessorBase(ABC):
69
70
  depends_on: list[str] | None = None,
70
71
  _request_options: client_config_t.RequestOptions | None = None,
71
72
  ) -> async_batch_t.QueuedAsyncBatchRequest:
72
- """Create or return the input association for equipment
73
+ """Create or return an input association for a piece of equipment within specified material families. The equipment is resolved via identifier key. Supports both direct invocation and async batch execution. Returns modification_made and result_id of the association.
73
74
 
74
75
  :param equipment_key: Identifier of the equipment to associate
75
76
  :param material_family_ids: The list of material families to add the input to. This must be non-empty
@@ -106,7 +107,7 @@ class AsyncBatchProcessorBase(ABC):
106
107
  depends_on: list[str] | None = None,
107
108
  _request_options: client_config_t.RequestOptions | None = None,
108
109
  ) -> async_batch_t.QueuedAsyncBatchRequest:
109
- """Create or return the input association for a recipe
110
+ """Associates a recipe as an input (ingredient). When no existing input is specified, creates a new input or returns the existing association if one already exists. When an existing input is specified, associates it with the recipe; fails if that input is already associated with a different recipe. Use `disassociate_recipe_as_input` to remove an existing association first. Returns the input ID and whether a modification was made.
110
111
 
111
112
  :param recipe_key: Identifier for the recipe
112
113
  :param input_key: Identifier for an input to use for the association. Optionally supplied. If not supplied, one is created
@@ -144,7 +145,7 @@ class AsyncBatchProcessorBase(ABC):
144
145
  depends_on: list[str] | None = None,
145
146
  _request_options: client_config_t.RequestOptions | None = None,
146
147
  ) -> async_batch_t.QueuedAsyncBatchRequest:
147
- """Create a new lot association for the provided recipe with the provided ingredient
148
+ """Creates a lot association between a recipe and an ingredient. The recipe becomes a lot for the specified ingredient. Returns whether a modification was made.
148
149
 
149
150
  :param recipe_key: Identifier for the recipe
150
151
  :param ingredient_key: Identifier for the ingredient
@@ -179,7 +180,7 @@ class AsyncBatchProcessorBase(ABC):
179
180
  depends_on: list[str] | None = None,
180
181
  _request_options: client_config_t.RequestOptions | None = None,
181
182
  ) -> async_batch_t.QueuedAsyncBatchRequest:
182
- """Clears all output values & output metadata for a given recipe
183
+ """Clears all output values and output metadata for a given recipe identified by key. Returns whether a modification was made.
183
184
 
184
185
  :param recipe_key: The identifier of the recipe
185
186
  :param depends_on: A list of batch reference keys to process before processing this request
@@ -215,7 +216,7 @@ class AsyncBatchProcessorBase(ABC):
215
216
  depends_on: list[str] | None = None,
216
217
  _request_options: client_config_t.RequestOptions | None = None,
217
218
  ) -> async_batch_t.QueuedAsyncBatchRequest:
218
- """Parses uploaded files asynchronously
219
+ """Completes the asynchronous parsing step of a file upload workflow. Accepts either inline parsed_file_data or a file_id referencing a JSON file containing the parsed data (exactly one must be provided). Resolves the async job by its identifier key and applies the parsed data to the upload destination. Updates the async job status to COMPLETED upon success. Supports async batch execution.
219
220
 
220
221
  :param parsed_file_data: Inline parsed data. Provide either parsed_file_data or file_id, not both.
221
222
  :param file_id: File ID containing parsed data. Provide either file_id or parsed_file_data, not both.
@@ -253,7 +254,7 @@ class AsyncBatchProcessorBase(ABC):
253
254
  depends_on: list[str] | None = None,
254
255
  _request_options: client_config_t.RequestOptions | None = None,
255
256
  ) -> async_batch_t.QueuedAsyncBatchRequest:
256
- """Processes an file id with a given async job id to be uploaded asynchronously
257
+ """Completes an asynchronous runsheet export by processing an uploaded file and placing it in the job owner's export downloads folder. Sends an export-complete notification to the job owner on completion. Supports async batch execution.
257
258
 
258
259
  :param depends_on: A list of batch reference keys to process before processing this request
259
260
  """
@@ -287,7 +288,7 @@ class AsyncBatchProcessorBase(ABC):
287
288
  depends_on: list[str] | None = None,
288
289
  _request_options: client_config_t.RequestOptions | None = None,
289
290
  ) -> async_batch_t.QueuedAsyncBatchRequest:
290
- """Creates mix order on a recipe workflow step
291
+ """Creates a mix order on a recipe workflow step. The recipe and workflow step are identified by their respective keys. Returns whether a modification was made.
291
292
 
292
293
  :param depends_on: A list of batch reference keys to process before processing this request
293
294
  """
@@ -369,7 +370,7 @@ class AsyncBatchProcessorBase(ABC):
369
370
  depends_on: list[str] | None = None,
370
371
  _request_options: client_config_t.RequestOptions | None = None,
371
372
  ) -> async_batch_t.QueuedAsyncBatchRequest:
372
- """Returns the id of the recipe being created.
373
+ """Creates a single recipe in the specified material family and workflow. If a matching recipe already exists, no new recipe is created and the existing recipe ID is returned. Returns the recipe ID.
373
374
 
374
375
  :param name: The name for the recipe
375
376
  :param material_family_id: The material family for the recipe
@@ -420,7 +421,7 @@ class AsyncBatchProcessorBase(ABC):
420
421
  depends_on: list[str] | None = None,
421
422
  _request_options: client_config_t.RequestOptions | None = None,
422
423
  ) -> async_batch_t.QueuedAsyncBatchRequest:
423
- """Clear, update, or add inputs on a recipe
424
+ """Applies a list of edits to inputs on a specific recipe workflow step. Supported edit types include clearing all inputs, upserting or adding inputs, updating annotations, setting lots, setting propagated percentages, changing basis, adding instructions, setting roles, and managing placeholders.
424
425
 
425
426
  :param recipe_key: Identifier for the recipe
426
427
  :param depends_on: A list of batch reference keys to process before processing this request
@@ -500,7 +501,7 @@ class AsyncBatchProcessorBase(ABC):
500
501
  depends_on: list[str] | None = None,
501
502
  _request_options: client_config_t.RequestOptions | None = None,
502
503
  ) -> async_batch_t.QueuedAsyncBatchRequest:
503
- """Runs a file through an uploader. Non-SDK users: use file_upload/external_create_file_record to upload files. SDK users: use client.upload_files() helper method.
504
+ """Processes one or more uploaded files through a configured uploader, parsing and writing the extracted data to a recipe destination. Resolves the uploader by its identifier key and requires READ_ALL permission on the uploader entity. The file_ids parameter is preferred; file_id is deprecated. Non-SDK users should use file_upload/external_create_file_record to upload files first. SDK users can use the client.upload_files() helper method. Supports async batch execution.
504
505
 
505
506
  :param file_id: DEPRECATED: use file_ids
506
507
  :param depends_on: A list of batch reference keys to process before processing this request
@@ -578,7 +579,7 @@ class AsyncBatchProcessorBase(ABC):
578
579
  depends_on: list[str] | None = None,
579
580
  _request_options: client_config_t.RequestOptions | None = None,
580
581
  ) -> async_batch_t.QueuedAsyncBatchRequest:
581
- """Lock experiments. Experiments will require unlocking to be editable. Edits to the experiments are blocked while they are locked.
582
+ """Locks experiments to prevent editing. Supports locking all data (inputs and measurements) or inputs only. Returns whether a modification was made.
582
583
 
583
584
  :param type: The type of lock to set.
584
585
  All = both inputs and measurements are locked.
@@ -661,7 +662,7 @@ class AsyncBatchProcessorBase(ABC):
661
662
  depends_on: list[str] | None = None,
662
663
  _request_options: client_config_t.RequestOptions | None = None,
663
664
  ) -> async_batch_t.QueuedAsyncBatchRequest:
664
- """Push a notification to a user or user group
665
+ """Send a notification to one or more users or user groups. Supports specifying a subject, message body, and optional entity link. Can optionally display as an in-app notice with custom configuration. Supports both direct invocation and async batch execution.
665
666
 
666
667
  :param depends_on: A list of batch reference keys to process before processing this request
667
668
  """
@@ -700,7 +701,7 @@ class AsyncBatchProcessorBase(ABC):
700
701
  depends_on: list[str] | None = None,
701
702
  _request_options: client_config_t.RequestOptions | None = None,
702
703
  ) -> async_batch_t.QueuedAsyncBatchRequest:
703
- """Runs a trigger. Requires admin access
704
+ """Executes a trigger script identified by ref name, optionally on a specific entity, and returns only after the trigger has fully completed. Requires admin user access; non-admin users receive a 403 error. Accepts either entity_identifier (preferred) or entity (deprecated) to specify the target entity, but not both. Returns whether any entities were modified by the trigger execution.
704
705
 
705
706
  :param entity_identifier: Identifier of the entity to run the trigger on.
706
707
  :param entity: [Deprecated: use entity_identifier] Entity to run the trigger on.
@@ -807,7 +808,7 @@ class AsyncBatchProcessorBase(ABC):
807
808
  depends_on: list[str] | None = None,
808
809
  _request_options: client_config_t.RequestOptions | None = None,
809
810
  ) -> async_batch_t.QueuedAsyncBatchRequest:
810
- """Set metadata values on a recipe
811
+ """Sets metadata values on a recipe identified by key. Accepts a list of metadata field-value pairs to populate on the recipe. Fails if any of the metadata fields are read-only.
811
812
 
812
813
  :param recipe_key: Identifier for the recipe
813
814
  :param recipe_metadata: Metadata values to populate the recipe with
@@ -835,6 +836,48 @@ class AsyncBatchProcessorBase(ABC):
835
836
  batch_reference=req.batch_reference,
836
837
  )
837
838
 
839
+ def swap_output_condition_parameters(
840
+ self,
841
+ *,
842
+ scope: swap_output_condition_parameters_t.SwapScope,
843
+ output_key: identifier_t.IdentifierKey,
844
+ new_condition: swap_output_condition_parameters_t.NewOutputCondition,
845
+ old_condition_key: identifier_t.IdentifierKey | None = None,
846
+ depends_on: list[str] | None = None,
847
+ _request_options: client_config_t.RequestOptions | None = None,
848
+ ) -> async_batch_t.QueuedAsyncBatchRequest:
849
+ """Swap output condition parameters on recipes. Either specify recipe keys for targeted swaps or set swap_all to swap across all recipes in the project.
850
+
851
+ :param scope: Which recipes to apply the swap to. See SwapScope variants for options.
852
+ :param output_key: Identifier for the output on which to swap conditions.
853
+ :param old_condition_key: Identifier for the old condition to replace. If omitted, recipes without a condition on the output are targeted.
854
+ :param new_condition: The new output condition to swap to. See NewOutputCondition variants for options.
855
+ :param depends_on: A list of batch reference keys to process before processing this request
856
+ """
857
+ args = swap_output_condition_parameters_t.Arguments(
858
+ scope=scope,
859
+ output_key=output_key,
860
+ old_condition_key=old_condition_key,
861
+ new_condition=new_condition,
862
+ )
863
+ json_data = serialize_for_api(args)
864
+
865
+ batch_reference = str(uuid.uuid4())
866
+
867
+ req = async_batch_t.AsyncBatchRequest(
868
+ path=async_batch_t.AsyncBatchRequestPath.SWAP_OUTPUT_CONDITION_PARAMETERS,
869
+ data=json_data,
870
+ depends_on=depends_on,
871
+ batch_reference=batch_reference,
872
+ )
873
+
874
+ self._enqueue(req)
875
+
876
+ return async_batch_t.QueuedAsyncBatchRequest(
877
+ path=req.path,
878
+ batch_reference=req.batch_reference,
879
+ )
880
+
838
881
  def transition_entity_phase(
839
882
  self,
840
883
  *,
@@ -918,7 +961,7 @@ class AsyncBatchProcessorBase(ABC):
918
961
  depends_on: list[str] | None = None,
919
962
  _request_options: client_config_t.RequestOptions | None = None,
920
963
  ) -> async_batch_t.QueuedAsyncBatchRequest:
921
- """Unlock experiments. Experiments will edtiable after unlocking if they are currently locked.
964
+ """Unlocks previously locked experiments, making them editable again. Returns whether a modification was made.
922
965
 
923
966
  :param type: The method to unlock recipes. Default is standard.
924
967
  :param recipes: The recipes to unlock, a maximum of 100 can be sent
@@ -959,7 +1002,7 @@ class AsyncBatchProcessorBase(ABC):
959
1002
  depends_on: list[str] | None = None,
960
1003
  _request_options: client_config_t.RequestOptions | None = None,
961
1004
  ) -> async_batch_t.QueuedAsyncBatchRequest:
962
- """Creates or updates condition match
1005
+ """Creates or updates a condition match record. Condition matches are used to set goals on outputs. Each call creates a new condition match — there is no idempotent behavior, so the same inputs will produce a new condition match with each invocation. Returns modification_made and result_id of the created or updated condition match.
963
1006
 
964
1007
  :param depends_on: A list of batch reference keys to process before processing this request
965
1008
  """
@@ -55,6 +55,7 @@ class AsyncBatchRequestPath(StrEnum):
55
55
  RUN_TRIGGER = "triggers/run_trigger"
56
56
  UPSERT_STEP_RELATIONSHIPS = "recipes/upsert_step_relationships"
57
57
  UPSERT_RECIPE_WORKFLOW_STEP = "recipes/upsert_recipe_workflow_step"
58
+ SWAP_OUTPUT_CONDITION_PARAMETERS = "output_parameters/swap_output_condition_parameters"
58
59
 
59
60
 
60
61
  # DO NOT MODIFY -- This file is generated by type_spec