UncountablePythonSDK 0.0.19__py3-none-any.whl → 0.0.21__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.

Potentially problematic release.


This version of UncountablePythonSDK might be problematic. Click here for more details.

Files changed (50) hide show
  1. {UncountablePythonSDK-0.0.19.dist-info → UncountablePythonSDK-0.0.21.dist-info}/METADATA +4 -2
  2. {UncountablePythonSDK-0.0.19.dist-info → UncountablePythonSDK-0.0.21.dist-info}/RECORD +50 -31
  3. examples/upload_files.py +19 -0
  4. pkgs/type_spec/actions_registry/__main__.py +35 -23
  5. pkgs/type_spec/actions_registry/emit_typescript.py +71 -9
  6. pkgs/type_spec/builder.py +13 -0
  7. pkgs/type_spec/config.py +1 -0
  8. pkgs/type_spec/emit_open_api.py +11 -0
  9. pkgs/type_spec/emit_open_api_util.py +1 -0
  10. pkgs/type_spec/emit_python.py +241 -55
  11. pkgs/type_spec/type_info/emit_type_info.py +129 -8
  12. type_spec/external/api/entity/create_entities.yaml +12 -1
  13. type_spec/external/api/entity/create_entity.yaml +12 -1
  14. type_spec/external/api/entity/transition_entity_phase.yaml +44 -0
  15. type_spec/external/api/permissions/set_core_permissions.yaml +69 -0
  16. type_spec/external/api/recipes/associate_recipe_as_input.yaml +4 -4
  17. type_spec/external/api/recipes/create_recipe.yaml +2 -1
  18. type_spec/external/api/recipes/disassociate_recipe_as_input.yaml +16 -0
  19. type_spec/external/api/recipes/edit_recipe_inputs.yaml +88 -0
  20. type_spec/external/api/recipes/get_curve.yaml +4 -1
  21. type_spec/external/api/recipes/get_recipes_data.yaml +6 -0
  22. type_spec/external/api/recipes/set_recipe_metadata.yaml +1 -0
  23. type_spec/external/api/recipes/set_recipe_tags.yaml +62 -0
  24. uncountable/core/__init__.py +2 -1
  25. uncountable/core/client.py +11 -9
  26. uncountable/core/file_upload.py +95 -0
  27. uncountable/core/types.py +22 -0
  28. uncountable/types/__init__.py +18 -0
  29. uncountable/types/api/entity/create_entities.py +1 -1
  30. uncountable/types/api/entity/create_entity.py +1 -1
  31. uncountable/types/api/entity/transition_entity_phase.py +66 -0
  32. uncountable/types/api/permissions/__init__.py +1 -0
  33. uncountable/types/api/permissions/set_core_permissions.py +89 -0
  34. uncountable/types/api/recipes/associate_recipe_as_input.py +4 -3
  35. uncountable/types/api/recipes/create_recipe.py +1 -1
  36. uncountable/types/api/recipes/disassociate_recipe_as_input.py +35 -0
  37. uncountable/types/api/recipes/edit_recipe_inputs.py +107 -0
  38. uncountable/types/api/recipes/get_curve.py +2 -1
  39. uncountable/types/api/recipes/get_recipes_data.py +2 -0
  40. uncountable/types/api/recipes/set_recipe_tags.py +91 -0
  41. uncountable/types/async_batch.py +9 -0
  42. uncountable/types/async_batch_processor.py +154 -0
  43. uncountable/types/client_base.py +113 -48
  44. uncountable/types/permissions.py +46 -0
  45. uncountable/types/post_base.py +30 -0
  46. uncountable/types/recipe_inputs.py +30 -0
  47. uncountable/types/recipe_metadata.py +2 -0
  48. uncountable/types/recipe_workflow_steps.py +77 -0
  49. {UncountablePythonSDK-0.0.19.dist-info → UncountablePythonSDK-0.0.21.dist-info}/WHEEL +0 -0
  50. {UncountablePythonSDK-0.0.19.dist-info → UncountablePythonSDK-0.0.21.dist-info}/top_level.txt +0 -0
@@ -21,6 +21,17 @@ __all__: list[str] = [
21
21
  """
22
22
  END_ALL_EXPORTS = "]\n"
23
23
 
24
+ ASYNC_BATCH_TYPE_NAMESPACE = builder.SpecNamespace(name="uncountable.types.async_batch")
25
+ ASYNC_BATCH_REQUEST_PATH_STYPE = builder.SpecTypeDefnStringEnum(
26
+ namespace=ASYNC_BATCH_TYPE_NAMESPACE, name="AsyncBatchRequestPath"
27
+ )
28
+ ASYNC_BATCH_REQUEST_STYPE = builder.SpecTypeDefnObject(
29
+ namespace=ASYNC_BATCH_TYPE_NAMESPACE, name="AsyncBatchRequest"
30
+ )
31
+ QUEUED_BATCH_REQUEST_STYPE = builder.SpecTypeDefnObject(
32
+ namespace=ASYNC_BATCH_TYPE_NAMESPACE, name="QueuedBatchRequest"
33
+ )
34
+
24
35
 
25
36
  @dataclass(kw_only=True)
26
37
  class TrackingContext:
@@ -194,6 +205,7 @@ def emit_python(builder: builder.SpecBuilder, *, config: PythonConfig) -> None:
194
205
  _emit_api_stubs(builder=builder, config=config)
195
206
  _emit_api_argument_lookup(builder=builder, config=config)
196
207
  _emit_client_class(spec_builder=builder, config=config)
208
+ _emit_async_batch_processor(spec_builder=builder, config=config)
197
209
 
198
210
 
199
211
  def _emit_types_imports(*, out: io.StringIO, ctx: Context) -> None:
@@ -353,11 +365,9 @@ def _validate_supports_handler_generation(
353
365
  def _emit_endpoint_invocation_docstring(
354
366
  ctx: Context,
355
367
  endpoint: builder.SpecEndpoint,
356
- arguments_type: builder.SpecTypeDefnObject,
368
+ properties: list[builder.SpecProperty],
357
369
  ) -> None:
358
- has_argument_desc = arguments_type.properties is not None and any(
359
- prop.desc is not None for prop in arguments_type.properties.values()
360
- )
370
+ has_argument_desc = any(prop.desc is not None for prop in properties)
361
371
  has_endpoint_desc = endpoint.desc
362
372
  if not has_argument_desc and not has_endpoint_desc:
363
373
  return
@@ -370,55 +380,39 @@ def _emit_endpoint_invocation_docstring(
370
380
  ctx.out.write(f"{endpoint.desc}\n")
371
381
  ctx.out.write("\n")
372
382
 
373
- if arguments_type.properties is not None and has_argument_desc:
374
- for prop in arguments_type.properties.values():
383
+ if has_argument_desc:
384
+ for prop in properties:
375
385
  if prop.desc:
376
386
  ctx.out.write(f"{FULL_INDENT}:param {prop.name}: {prop.desc}\n")
377
387
 
378
388
  ctx.out.write(f'{FULL_INDENT}"""\n')
379
389
 
380
390
 
381
- def _emit_endpoint_invocation_function(
382
- ctx: Context, namespace: builder.SpecNamespace
391
+ def _emit_endpoint_invocation_function_signature(
392
+ ctx: Context,
393
+ endpoint: builder.SpecEndpoint,
394
+ arguments_type: builder.SpecTypeDefnObject,
395
+ data_type: builder.SpecTypeDefnObject,
396
+ extra_params: list[builder.SpecProperty] | None = None,
383
397
  ) -> None:
384
- endpoint = namespace.endpoint
385
- if endpoint is None:
386
- return
387
- if not endpoint.is_sdk:
388
- return
389
-
390
- arguments_type = namespace.types["Arguments"]
391
- data_type = namespace.types["Data"]
398
+ all_arguments = (
399
+ list(arguments_type.properties.values())
400
+ if arguments_type.properties is not None
401
+ else []
402
+ ) + (extra_params if extra_params is not None else [])
392
403
 
393
- arguments_type = _validate_supports_handler_generation(arguments_type, "arguments")
394
- data_type = _validate_supports_handler_generation(
395
- data_type, "response", supports_inheritance=True
396
- )
397
-
398
- endpoint_method_stype = builder.SpecTypeDefnObject(
399
- namespace=arguments_type.namespace, name=ENDPOINT_METHOD
400
- )
401
- endpoint_path_stype = builder.SpecTypeDefnObject(
402
- namespace=arguments_type.namespace, name=ENDPOINT_PATH
403
- )
404
-
405
- has_arguments = (
406
- arguments_type.properties is not None
407
- and len(arguments_type.properties.values()) > 0
408
- )
409
404
  assert endpoint.function is not None
410
405
  function_name = endpoint.function.split(".")[-1]
411
- ctx.out.write("\n")
412
406
  ctx.out.write(
413
407
  f"""
414
408
  def {function_name}(
415
409
  self,\n"""
416
410
  )
417
- if has_arguments:
411
+ if len(all_arguments) > 0:
418
412
  ctx.out.write(f"{INDENT}{INDENT}*,\n")
419
- _emit_type_properties(
413
+ _emit_properties(
420
414
  ctx=ctx,
421
- stype=arguments_type,
415
+ properties=all_arguments,
422
416
  num_indent=2,
423
417
  separator=",\n",
424
418
  class_out=ctx.out,
@@ -426,23 +420,143 @@ def _emit_endpoint_invocation_function(
426
420
  ctx.out.write(f"{INDENT}) -> {refer_to(ctx=ctx, stype=data_type)}:")
427
421
  ctx.out.write("\n")
428
422
 
429
- _emit_endpoint_invocation_docstring(
430
- ctx=ctx,
431
- endpoint=endpoint,
432
- arguments_type=arguments_type,
433
- )
423
+ if len(all_arguments) > 0:
424
+ _emit_endpoint_invocation_docstring(
425
+ ctx=ctx, endpoint=endpoint, properties=all_arguments
426
+ )
427
+
434
428
 
435
- ctx.out.write(f"{INDENT}{INDENT}args = {refer_to(ctx=ctx, stype=arguments_type)}(")
436
- if has_arguments:
437
- assert arguments_type.properties is not None
429
+ def _emit_instantiate_type_from_locals(
430
+ ctx: Context, variable_name: str, variable_type: builder.SpecTypeDefnObject
431
+ ) -> None:
432
+ ctx.out.write(
433
+ f"{INDENT}{INDENT}{variable_name} = {refer_to(ctx=ctx, stype=variable_type)}("
434
+ )
435
+ if variable_type.properties is not None and len(variable_type.properties) > 0:
438
436
  ctx.out.write("\n")
439
- for prop in arguments_type.properties.values():
437
+ for prop in variable_type.properties.values():
440
438
  ctx.out.write(f"{INDENT}{INDENT}{INDENT}{prop.name}={prop.name},")
441
439
  ctx.out.write("\n")
442
440
  ctx.out.write(f"{INDENT}{INDENT})")
443
441
  else:
444
442
  ctx.out.write(")")
445
443
 
444
+
445
+ def _emit_async_batch_invocation_function(
446
+ ctx: Context, namespace: builder.SpecNamespace
447
+ ) -> None:
448
+ endpoint = namespace.endpoint
449
+ if endpoint is None:
450
+ return
451
+ if endpoint.async_batch_path is None or not endpoint.is_sdk:
452
+ return
453
+
454
+ ctx.out.write("\n")
455
+ arguments_type = namespace.types["Arguments"]
456
+ arguments_type = _validate_supports_handler_generation(arguments_type, "arguments")
457
+ data_type = QUEUED_BATCH_REQUEST_STYPE
458
+
459
+ list_str_params: list[builder.SpecType] = []
460
+ list_str_params.append(
461
+ builder.SpecTypeGenericParameter(
462
+ name="str",
463
+ spec_type_definition=builder.SpecTypeDefnObject(
464
+ namespace=namespace, name=builder.BaseTypeName.s_string, is_base=True
465
+ ),
466
+ )
467
+ )
468
+
469
+ _emit_endpoint_invocation_function_signature(
470
+ ctx=ctx,
471
+ endpoint=endpoint,
472
+ arguments_type=arguments_type,
473
+ data_type=data_type,
474
+ extra_params=[
475
+ builder.SpecProperty(
476
+ name="depends_on",
477
+ extant=builder.PropertyExtant.optional,
478
+ convert_value=builder.PropertyConvertValue.auto,
479
+ name_case=builder.NameCase.convert,
480
+ label="depends_on",
481
+ desc="A list of batch reference keys to process before processing this request",
482
+ spec_type=builder.SpecTypeInstance(
483
+ defn_type=builder.SpecTypeDefnObject(
484
+ name=builder.BaseTypeName.s_list,
485
+ is_base=True,
486
+ namespace=namespace,
487
+ ),
488
+ parameters=list_str_params,
489
+ ),
490
+ )
491
+ ],
492
+ )
493
+
494
+ # Emit function body
495
+ _emit_instantiate_type_from_locals(
496
+ ctx=ctx, variable_name="args", variable_type=arguments_type
497
+ )
498
+
499
+ path = _emit_value(
500
+ ctx=ctx,
501
+ stype=ASYNC_BATCH_REQUEST_PATH_STYPE,
502
+ value=endpoint.async_batch_path,
503
+ )
504
+
505
+ ctx.out.write(
506
+ f"""
507
+ json_data = serialize_for_api(args)
508
+
509
+ batch_reference = str(uuid.uuid4())
510
+
511
+ req = {refer_to(ctx=ctx, stype=ASYNC_BATCH_REQUEST_STYPE)}(
512
+ path={path},
513
+ data=json_data,
514
+ depends_on=depends_on,
515
+ batch_reference=batch_reference,
516
+ )
517
+
518
+ self._enqueue(req)
519
+
520
+ return {refer_to(ctx=ctx, stype=data_type)}(
521
+ path=req.path,
522
+ batch_reference=req.batch_reference,
523
+ )"""
524
+ )
525
+
526
+
527
+ def _emit_endpoint_invocation_function(
528
+ ctx: Context, namespace: builder.SpecNamespace
529
+ ) -> None:
530
+ endpoint = namespace.endpoint
531
+ if endpoint is None:
532
+ return
533
+ if not endpoint.is_sdk or endpoint.is_beta:
534
+ return
535
+
536
+ ctx.out.write("\n")
537
+ arguments_type = namespace.types["Arguments"]
538
+ data_type = namespace.types["Data"]
539
+ arguments_type = _validate_supports_handler_generation(arguments_type, "arguments")
540
+ data_type = _validate_supports_handler_generation(
541
+ data_type, "response", supports_inheritance=True
542
+ )
543
+
544
+ _emit_endpoint_invocation_function_signature(
545
+ ctx=ctx, endpoint=endpoint, arguments_type=arguments_type, data_type=data_type
546
+ )
547
+
548
+ endpoint_method_stype = builder.SpecTypeDefnObject(
549
+ namespace=arguments_type.namespace, name=ENDPOINT_METHOD
550
+ )
551
+ endpoint_path_stype = builder.SpecTypeDefnObject(
552
+ namespace=arguments_type.namespace, name=ENDPOINT_PATH
553
+ )
554
+
555
+ # Emit function body
556
+ _emit_instantiate_type_from_locals(
557
+ ctx=ctx, variable_name="args", variable_type=arguments_type
558
+ )
559
+
446
560
  ctx.out.write(
447
561
  f"""
448
562
  api_request = APIRequest(
@@ -452,7 +566,6 @@ def _emit_endpoint_invocation_function(
452
566
  )
453
567
  return self.do_request(api_request=api_request, return_type={refer_to(ctx=ctx, stype=data_type)})"""
454
568
  )
455
- ctx.out.write("\n")
456
569
 
457
570
 
458
571
  def _emit_string_enum(ctx: Context, stype: builder.SpecTypeDefnStringEnum) -> None:
@@ -516,17 +629,32 @@ def _emit_type_properties(
516
629
  stype: builder.SpecTypeDefnObject,
517
630
  num_indent: int = 1,
518
631
  separator: str = "\n",
632
+ ) -> EmittedPropertiesMetadata:
633
+ return _emit_properties(
634
+ ctx=ctx,
635
+ class_out=class_out,
636
+ properties=list((stype.properties or {}).values()),
637
+ num_indent=num_indent,
638
+ separator=separator,
639
+ )
640
+
641
+
642
+ def _emit_properties(
643
+ *,
644
+ ctx: Context,
645
+ class_out: io.StringIO,
646
+ properties: list[builder.SpecProperty],
647
+ num_indent: int = 1,
648
+ separator: str = "\n",
519
649
  ) -> EmittedPropertiesMetadata:
520
650
  unconverted_keys: set[str] = set()
521
651
  unconverted_values: set[str] = set()
522
652
  to_string_values: set[str] = set()
523
653
  parse_require: set[str] = set()
524
654
 
525
- if stype.properties is not None and len(stype.properties) > 0:
655
+ if len(properties) > 0:
526
656
 
527
657
  def write_field(prop: builder.SpecProperty) -> None:
528
- # Checked in outer function, MyPy doens't track the check inside here
529
- assert isinstance(stype, builder.SpecTypeDefn)
530
658
  if prop.name_case == builder.NameCase.preserve:
531
659
  unconverted_keys.add(prop.name)
532
660
  py_name = python_field_name(prop.name, prop.name_case)
@@ -559,10 +687,10 @@ def _emit_type_properties(
559
687
  class_out.write(f" = {default}")
560
688
  class_out.write(separator)
561
689
 
562
- for prop in stype.properties.values():
690
+ for prop in properties:
563
691
  if prop.extant == builder.PropertyExtant.required:
564
692
  write_field(prop)
565
- for prop in stype.properties.values():
693
+ for prop in properties:
566
694
  if prop.extant != builder.PropertyExtant.required:
567
695
  write_field(prop)
568
696
  else:
@@ -1014,6 +1142,64 @@ CLIENT_CLASS_IMPORTS = [
1014
1142
  "from abc import ABC, abstractmethod",
1015
1143
  "from dataclasses import dataclass",
1016
1144
  ]
1145
+ ASYNC_BATCH_PROCESSOR_FILENAME = "async_batch_processor"
1146
+ ASYNC_BATCH_PROCESSOR_IMPORTS = [
1147
+ "import uuid",
1148
+ "from abc import ABC, abstractmethod",
1149
+ "from dataclasses import dataclass",
1150
+ "from pkgs.serialization_util.serialization_helpers import serialize_for_api",
1151
+ ]
1152
+
1153
+
1154
+ def _emit_async_batch_processor(
1155
+ *, spec_builder: builder.SpecBuilder, config: PythonConfig
1156
+ ) -> None:
1157
+ if not config.emit_async_batch_processor:
1158
+ return
1159
+
1160
+ async_batch_processor_out = io.StringIO()
1161
+ ctx = Context(
1162
+ out=io.StringIO(), namespace=builder.SpecNamespace("async_batch_processor")
1163
+ )
1164
+
1165
+ for namespace in sorted(
1166
+ spec_builder.namespaces.values(),
1167
+ key=lambda ns: _resolve_namespace_name(ns),
1168
+ ):
1169
+ _emit_async_batch_invocation_function(ctx=ctx, namespace=namespace)
1170
+
1171
+ async_batch_processor_out.write(MODIFY_NOTICE)
1172
+ async_batch_processor_out.write(LINT_HEADER)
1173
+ async_batch_processor_out.write("# ruff: noqa: PLR0904\n")
1174
+
1175
+ _emit_types_imports(out=async_batch_processor_out, ctx=ctx)
1176
+ _emit_namespace_imports(
1177
+ out=async_batch_processor_out,
1178
+ namespaces=ctx.namespaces,
1179
+ from_namespace=None,
1180
+ config=config,
1181
+ )
1182
+
1183
+ async_batch_processor_out.write(
1184
+ f"""{LINE_BREAK.join(ASYNC_BATCH_PROCESSOR_IMPORTS)}
1185
+
1186
+
1187
+ class AsyncBatchProcessorBase(ABC):
1188
+ @abstractmethod
1189
+ def _enqueue(self, req: {refer_to(ctx=ctx, stype=ASYNC_BATCH_REQUEST_STYPE)}) -> None:
1190
+ ...
1191
+
1192
+ @abstractmethod
1193
+ def send(self) -> base_t.ObjectId:
1194
+ ..."""
1195
+ )
1196
+ async_batch_processor_out.write(ctx.out.getvalue())
1197
+ async_batch_processor_out.write("\n")
1198
+
1199
+ util.rewrite_file(
1200
+ f"{config.types_output}/{ASYNC_BATCH_PROCESSOR_FILENAME}.py",
1201
+ async_batch_processor_out.getvalue(),
1202
+ )
1017
1203
 
1018
1204
 
1019
1205
  def _emit_client_class(
@@ -1023,7 +1209,7 @@ def _emit_client_class(
1023
1209
  return
1024
1210
 
1025
1211
  client_base_out = io.StringIO()
1026
- ctx = Context(out=io.StringIO(), namespace=builder.SpecNamespace("client_class"))
1212
+ ctx = Context(out=io.StringIO(), namespace=builder.SpecNamespace("client_base"))
1027
1213
  for namespace in sorted(
1028
1214
  spec_builder.namespaces.values(),
1029
1215
  key=lambda ns: _resolve_namespace_name(ns),
@@ -1038,7 +1224,7 @@ def _emit_client_class(
1038
1224
  _emit_namespace_imports(
1039
1225
  out=client_base_out,
1040
1226
  namespaces=ctx.namespaces,
1041
- from_namespace=builder.SpecNamespace("client_base"),
1227
+ from_namespace=None,
1042
1228
  config=config,
1043
1229
  )
1044
1230
 
@@ -1059,10 +1245,10 @@ class ClientMethods(ABC):
1059
1245
 
1060
1246
  @abstractmethod
1061
1247
  def do_request(self, *, api_request: APIRequest, return_type: type[DT]) -> DT:
1062
- ...
1063
- """
1248
+ ..."""
1064
1249
  )
1065
1250
  client_base_out.write(ctx.out.getvalue())
1251
+ client_base_out.write("\n")
1066
1252
 
1067
1253
  util.rewrite_file(
1068
1254
  f"{config.types_output}/{CLIENT_CLASS_FILENAME}.py", client_base_out.getvalue()
@@ -23,7 +23,7 @@ def type_path_of(stype: builder.SpecType) -> object: # NamePath
23
23
  extended scopes, generics, and enum literal values.
24
24
  - Scoped Type: [ (namespace-string)..., type-string ]
25
25
  - Instance Type: [ "$instance", Scoped-Type-Base, [TypePath-Parameters...] ]
26
- - Literal Type: [ "$literal", [ "$value", value ]... ]
26
+ - Literal Type: [ "$literal", [ "$value", value, value-type-string ]... ]
27
27
 
28
28
  @return (string-specific, multiple-types)
29
29
  """
@@ -34,11 +34,15 @@ def type_path_of(stype: builder.SpecType) -> object: # NamePath
34
34
 
35
35
  if isinstance(stype, builder.SpecTypeInstance):
36
36
  if stype.defn_type.name == builder.BaseTypeName.s_literal:
37
- parts: list[str | list[str | bool]] = ["$literal"]
37
+ parts: list[object] = ["$literal"]
38
38
  for parameter in stype.parameters:
39
39
  assert isinstance(parameter, builder.SpecTypeLiteralWrapper)
40
40
  # This allows expansion to enum literal values later
41
- parts.append(["$value", parameter.value])
41
+ parts.append([
42
+ "$value",
43
+ parameter.value,
44
+ type_path_of(parameter.value_type),
45
+ ])
42
46
  return parts
43
47
 
44
48
  return [
@@ -148,6 +152,114 @@ def _build_map_all(build: builder.SpecBuilder) -> MapAll:
148
152
  return map_all
149
153
 
150
154
 
155
+ @dataclasses.dataclass(kw_only=True)
156
+ class InheritablePropertyParts:
157
+ """This uses only the "soft" information for now, things that aren't relevant
158
+ to the language emitted types. There are some fields that should be inherited
159
+ at that level, but that needs to be done in builder. When that is done, the
160
+ "label" and "desc" could probably be removed from this list."""
161
+
162
+ label: Optional[str] = None
163
+ desc: Optional[str] = None
164
+ ext_info: Optional[data_t.ExtInfo] = None
165
+
166
+
167
+ def _extract_inheritable_property_parts(
168
+ stype: builder.SpecTypeDefnObject,
169
+ prop: builder.SpecProperty,
170
+ ) -> InheritablePropertyParts:
171
+ if not stype.is_base and isinstance(stype.base, builder.SpecTypeDefn):
172
+ base_prop = (stype.base.properties or {}).get(prop.name)
173
+ if base_prop is None:
174
+ base_parts = InheritablePropertyParts()
175
+ else:
176
+ base_parts = _extract_inheritable_property_parts(stype.base, base_prop)
177
+ # Layout should not be inherited, as it'd end up hiding properties in the derived type
178
+ if base_parts.ext_info is not None:
179
+ base_parts.ext_info.layout = None
180
+ else:
181
+ base_parts = InheritablePropertyParts()
182
+
183
+ label = prop.label or base_parts.label
184
+ desc = prop.desc or base_parts.desc
185
+ local_ext_info = _parse_ext_info(prop.ext_info)
186
+ if local_ext_info is None:
187
+ ext_info = base_parts.ext_info
188
+ elif base_parts.ext_info is None:
189
+ ext_info = local_ext_info
190
+ else:
191
+ ext_info = data_t.ExtInfo(
192
+ **(local_ext_info.__dict__ | base_parts.ext_info.__dict__)
193
+ )
194
+
195
+ return InheritablePropertyParts(label=label, desc=desc, ext_info=ext_info)
196
+
197
+
198
+ ExtInfoLayout = dict[str, set[str]]
199
+ ALL_FIELDS_GROUP = "*all_fields"
200
+
201
+
202
+ def _extract_and_validate_layout(
203
+ stype: builder.SpecTypeDefnObject,
204
+ ext_info: data_t.ExtInfo,
205
+ base_layout: ExtInfoLayout | None,
206
+ ) -> ExtInfoLayout:
207
+ """
208
+ Produce a map of groups to fields, for validation.
209
+ """
210
+ if ext_info.layout is None:
211
+ return {}
212
+ assert stype.properties is not None
213
+
214
+ all_fields_group: set[str] = set()
215
+ layout: ExtInfoLayout = {ALL_FIELDS_GROUP: all_fields_group}
216
+
217
+ for group in ext_info.layout.groups:
218
+ fields = set(group.fields or [])
219
+ for field in fields:
220
+ assert field in stype.properties, f"layout-refers-to-missing-field:{field}"
221
+
222
+ local_ref_name = None
223
+ if group.ref_name is not None:
224
+ assert (
225
+ base_layout is None or base_layout.get(group.ref_name) is None
226
+ ), f"group-name-duplicate-in-base:{group.ref_name}"
227
+ local_ref_name = group.ref_name
228
+
229
+ if group.extends:
230
+ assert base_layout is not None, "missing-base-layout"
231
+ base_group = base_layout.get(group.extends)
232
+ assert base_group is not None, f"missing-base-group:{group.extends}"
233
+ fields.update(base_group)
234
+ local_ref_name = group.extends
235
+
236
+ assert local_ref_name not in layout, f"duplicate-group:{local_ref_name}"
237
+ if local_ref_name is not None:
238
+ layout[local_ref_name] = fields
239
+ all_fields_group.update(fields)
240
+
241
+ for group_ref_name in base_layout or {}:
242
+ assert group_ref_name in layout, f"missing-base-group:{group_ref_name}"
243
+
244
+ for prop_ref_name in stype.properties:
245
+ assert prop_ref_name in all_fields_group, f"layout-missing-field:{prop_ref_name}"
246
+
247
+ return layout
248
+
249
+
250
+ def _validate_type_ext_info(stype: builder.SpecTypeDefnObject) -> ExtInfoLayout | None:
251
+ ext_info = _parse_ext_info(stype.ext_info)
252
+ if ext_info is None:
253
+ return None
254
+
255
+ if not stype.is_base and isinstance(stype.base, builder.SpecTypeDefnObject):
256
+ base_layout = _validate_type_ext_info(stype.base)
257
+ else:
258
+ base_layout = None
259
+
260
+ return _extract_and_validate_layout(stype, ext_info, base_layout)
261
+
262
+
151
263
  def _build_map_type(
152
264
  build: builder.SpecBuilder, stype: builder.SpecTypeDefn
153
265
  ) -> MapType | None:
@@ -158,6 +270,8 @@ def _build_map_type(
158
270
  and not stype.is_base
159
271
  and stype.base is not None
160
272
  ):
273
+ _validate_type_ext_info(stype)
274
+
161
275
  properties: dict[str, MapProperty] = {}
162
276
  map_type = MapTypeObject(
163
277
  type_name=stype.name,
@@ -170,14 +284,17 @@ def _build_map_type(
170
284
 
171
285
  if stype.properties is not None:
172
286
  for prop in stype.properties.values():
287
+ parts = _extract_inheritable_property_parts(stype, prop)
288
+ # Propertis can't have layouts
289
+ assert parts.ext_info is None or parts.ext_info.layout is None
173
290
  map_property = MapProperty(
174
291
  type_name=prop.name,
175
- label=prop.label,
292
+ label=parts.label,
176
293
  api_name=ts_name(prop.name, prop.name_case),
177
294
  extant=prop.extant,
178
295
  type_path=type_path_of(prop.spec_type),
179
- ext_info=_convert_ext_info(prop.ext_info),
180
- desc=prop.desc,
296
+ ext_info=serialize_for_api(parts.ext_info),
297
+ desc=parts.desc,
181
298
  default=prop.default,
182
299
  )
183
300
  map_type.properties[prop.name] = map_property
@@ -211,7 +328,7 @@ def _build_map_type(
211
328
  return None
212
329
 
213
330
 
214
- def _convert_ext_info(in_ext: Any) -> Optional[PureJsonValue]:
331
+ def _parse_ext_info(in_ext: Any) -> Optional[data_t.ExtInfo]:
215
332
  if in_ext is None:
216
333
  return None
217
334
  assert isinstance(in_ext, dict)
@@ -229,6 +346,10 @@ def _convert_ext_info(in_ext: Any) -> Optional[PureJsonValue]:
229
346
  df["result_type"] = serialize_for_storage(converted)
230
347
  mod_ext["data_format"] = df
231
348
 
232
- parsed = ext_info_parser.parse_storage(mod_ext)
349
+ return ext_info_parser.parse_storage(mod_ext)
350
+
351
+
352
+ def _convert_ext_info(in_ext: Any) -> Optional[PureJsonValue]:
233
353
  # we need to convert this to API storage since it'll be used as-is in the UI
354
+ parsed = _parse_ext_info(in_ext)
234
355
  return cast(PureJsonValue, serialize_for_api(parsed))
@@ -19,7 +19,18 @@ Arguments:
19
19
  type: ObjectId
20
20
  desc: "Definition id for the entities to create"
21
21
  entity_type:
22
- type: Union<Literal<entity.EntityType.lab_request>, Literal<entity.EntityType.approval>, Literal<entity.EntityType.custom_entity>, Literal<entity.EntityType.task>, Literal<entity.EntityType.project>, Literal<entity.EntityType.equipment>, Literal<entity.EntityType.inv_local_locations>>
22
+ type: |
23
+ Union<
24
+ Literal<entity.EntityType.lab_request>,
25
+ Literal<entity.EntityType.approval>,
26
+ Literal<entity.EntityType.custom_entity>,
27
+ Literal<entity.EntityType.task>,
28
+ Literal<entity.EntityType.project>,
29
+ Literal<entity.EntityType.equipment>,
30
+ Literal<entity.EntityType.inv_local_locations>,
31
+ Literal<entity.EntityType.field_option_set>,
32
+ Literal<entity.EntityType.webhook>
33
+ >
23
34
  desc: "The type of the entities to create"
24
35
  entities_to_create:
25
36
  type: List<EntityToCreate>
@@ -26,7 +26,18 @@ Arguments:
26
26
  type: ObjectId
27
27
  desc: "Definition id of the entity to create"
28
28
  entity_type:
29
- type: Union<Literal<entity.EntityType.lab_request>, Literal<entity.EntityType.approval>, Literal<entity.EntityType.custom_entity>, Literal<entity.EntityType.task>, Literal<entity.EntityType.project>, Literal<entity.EntityType.equipment>, Literal<entity.EntityType.inv_local_locations>>
29
+ type: |
30
+ Union<
31
+ Literal<entity.EntityType.lab_request>,
32
+ Literal<entity.EntityType.approval>,
33
+ Literal<entity.EntityType.custom_entity>,
34
+ Literal<entity.EntityType.task>,
35
+ Literal<entity.EntityType.project>,
36
+ Literal<entity.EntityType.equipment>,
37
+ Literal<entity.EntityType.inv_local_locations>,
38
+ Literal<entity.EntityType.field_option_set>,
39
+ Literal<entity.EntityType.webhook>
40
+ >
30
41
  desc: "The type of the entities requested"
31
42
  field_values?:
32
43
  type: Optional<List<field_values.FieldRefNameValue>>
@@ -0,0 +1,44 @@
1
+ $endpoint:
2
+ is_sdk: true
3
+ method: post
4
+ path: ${external}/entity/transition_entity_phase
5
+ function: main.site.app.external.entity.transition_entity_phase.transition_entity_phase
6
+ desc: "Transitions an entity from one phase to another"
7
+
8
+
9
+ TransitionIdentifierPhases:
10
+ type: Object
11
+ properties:
12
+ type:
13
+ type: Literal<'phases'>
14
+ phase_from_key:
15
+ type: identifier.IdentifierKey
16
+ phase_to_key:
17
+ type: identifier.IdentifierKey
18
+
19
+ TransitionIdentifierTransition:
20
+ type: Object
21
+ properties:
22
+ type:
23
+ type: Literal<'transition'>
24
+ transition_key:
25
+ type: identifier.IdentifierKey
26
+
27
+
28
+ TransitionIdentifier:
29
+ type: Alias
30
+ alias: |
31
+ Union<TransitionIdentifierPhases, TransitionIdentifierTransition>
32
+
33
+
34
+ Arguments:
35
+ type: Object
36
+ properties:
37
+ entity:
38
+ type: entity.Entity
39
+ transition:
40
+ type: TransitionIdentifier
41
+
42
+ Data:
43
+ type: response.Response
44
+ properties: