UncountablePythonSDK 0.0.91__py3-none-any.whl → 0.0.93__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 (193) hide show
  1. {UncountablePythonSDK-0.0.91.dist-info → UncountablePythonSDK-0.0.93.dist-info}/METADATA +2 -2
  2. UncountablePythonSDK-0.0.93.dist-info/RECORD +301 -0
  3. {UncountablePythonSDK-0.0.91.dist-info → UncountablePythonSDK-0.0.93.dist-info}/WHEEL +1 -1
  4. examples/set_recipe_metadata_file.py +1 -1
  5. examples/upload_files.py +1 -2
  6. pkgs/argument_parser/__init__.py +3 -0
  7. pkgs/argument_parser/argument_parser.py +52 -23
  8. pkgs/filesystem_utils/__init__.py +1 -0
  9. pkgs/filesystem_utils/_gdrive_session.py +4 -4
  10. pkgs/filesystem_utils/_s3_session.py +2 -1
  11. pkgs/filesystem_utils/_sftp_session.py +2 -3
  12. pkgs/filesystem_utils/file_type_utils.py +10 -10
  13. pkgs/serialization/annotation.py +5 -5
  14. pkgs/serialization/missing_sentry.py +1 -1
  15. pkgs/serialization/serial_alias.py +3 -3
  16. pkgs/serialization/serial_class.py +10 -10
  17. pkgs/serialization/serial_generic.py +1 -1
  18. pkgs/serialization/serial_union.py +10 -10
  19. pkgs/serialization_util/__init__.py +2 -0
  20. pkgs/serialization_util/serialization_helpers.py +1 -4
  21. pkgs/type_spec/actions_registry/__main__.py +0 -4
  22. pkgs/type_spec/builder.py +121 -40
  23. pkgs/type_spec/config.py +10 -5
  24. pkgs/type_spec/emit_open_api.py +2 -2
  25. pkgs/type_spec/emit_open_api_util.py +1 -1
  26. pkgs/type_spec/emit_python.py +145 -63
  27. pkgs/type_spec/emit_typescript.py +57 -10
  28. pkgs/type_spec/load_types.py +1 -2
  29. pkgs/type_spec/open_api_util.py +1 -2
  30. pkgs/type_spec/parts/base.py.prepart +2 -0
  31. pkgs/type_spec/type_info/emit_type_info.py +8 -8
  32. pkgs/type_spec/util.py +5 -7
  33. pkgs/type_spec/value_spec/__main__.py +15 -5
  34. pkgs/type_spec/value_spec/emit_python.py +5 -2
  35. pkgs/type_spec/value_spec/types.py +1 -1
  36. uncountable/core/client.py +16 -15
  37. uncountable/core/file_upload.py +39 -15
  38. uncountable/integration/construct_client.py +3 -3
  39. uncountable/integration/executors/generic_upload_executor.py +1 -1
  40. uncountable/integration/job.py +2 -2
  41. uncountable/integration/queue_runner/command_server/types.py +1 -1
  42. uncountable/integration/queue_runner/worker.py +1 -1
  43. uncountable/integration/server.py +4 -4
  44. uncountable/integration/telemetry.py +11 -0
  45. uncountable/types/__init__.py +0 -1
  46. uncountable/types/api/batch/execute_batch.py +1 -2
  47. uncountable/types/api/batch/execute_batch_load_async.py +0 -1
  48. uncountable/types/api/chemical/convert_chemical_formats.py +0 -1
  49. uncountable/types/api/entity/create_entities.py +3 -4
  50. uncountable/types/api/entity/create_entity.py +4 -5
  51. uncountable/types/api/entity/get_entities_data.py +0 -1
  52. uncountable/types/api/entity/grant_entity_permissions.py +3 -4
  53. uncountable/types/api/entity/list_entities.py +5 -6
  54. uncountable/types/api/entity/lock_entity.py +1 -2
  55. uncountable/types/api/entity/resolve_entity_ids.py +2 -3
  56. uncountable/types/api/entity/set_entity_field_values.py +0 -1
  57. uncountable/types/api/entity/set_values.py +0 -1
  58. uncountable/types/api/entity/transition_entity_phase.py +1 -2
  59. uncountable/types/api/entity/unlock_entity.py +0 -1
  60. uncountable/types/api/equipment/associate_equipment_input.py +0 -1
  61. uncountable/types/api/field_options/upsert_field_options.py +3 -4
  62. uncountable/types/api/files/download_file.py +1 -2
  63. uncountable/types/api/id_source/list_id_source.py +3 -4
  64. uncountable/types/api/id_source/match_id_source.py +1 -2
  65. uncountable/types/api/input_groups/get_input_group_names.py +0 -1
  66. uncountable/types/api/inputs/create_inputs.py +4 -5
  67. uncountable/types/api/inputs/get_input_data.py +5 -6
  68. uncountable/types/api/inputs/get_input_names.py +3 -4
  69. uncountable/types/api/inputs/get_inputs_data.py +0 -1
  70. uncountable/types/api/inputs/set_input_attribute_values.py +2 -3
  71. uncountable/types/api/inputs/set_input_category.py +2 -3
  72. uncountable/types/api/inputs/set_input_subcategories.py +0 -1
  73. uncountable/types/api/inputs/set_intermediate_type.py +1 -2
  74. uncountable/types/api/material_families/update_entity_material_families.py +1 -2
  75. uncountable/types/api/outputs/get_output_data.py +6 -7
  76. uncountable/types/api/outputs/get_output_names.py +2 -3
  77. uncountable/types/api/outputs/resolve_output_conditions.py +2 -3
  78. uncountable/types/api/permissions/set_core_permissions.py +3 -4
  79. uncountable/types/api/project/get_projects.py +3 -4
  80. uncountable/types/api/project/get_projects_data.py +4 -5
  81. uncountable/types/api/recipe_links/create_recipe_link.py +1 -2
  82. uncountable/types/api/recipe_links/remove_recipe_link.py +1 -2
  83. uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +3 -4
  84. uncountable/types/api/recipes/add_recipe_to_project.py +0 -1
  85. uncountable/types/api/recipes/archive_recipes.py +1 -2
  86. uncountable/types/api/recipes/associate_recipe_as_input.py +2 -3
  87. uncountable/types/api/recipes/associate_recipe_as_lot.py +0 -1
  88. uncountable/types/api/recipes/clear_recipe_outputs.py +0 -1
  89. uncountable/types/api/recipes/create_recipe.py +6 -7
  90. uncountable/types/api/recipes/create_recipes.py +4 -5
  91. uncountable/types/api/recipes/disassociate_recipe_as_input.py +0 -1
  92. uncountable/types/api/recipes/edit_recipe_inputs.py +9 -10
  93. uncountable/types/api/recipes/get_column_calculation_values.py +1 -2
  94. uncountable/types/api/recipes/get_curve.py +2 -3
  95. uncountable/types/api/recipes/get_recipe_calculations.py +3 -4
  96. uncountable/types/api/recipes/get_recipe_links.py +1 -2
  97. uncountable/types/api/recipes/get_recipe_names.py +0 -1
  98. uncountable/types/api/recipes/get_recipe_output_metadata.py +0 -1
  99. uncountable/types/api/recipes/get_recipes_data.py +22 -23
  100. uncountable/types/api/recipes/lock_recipes.py +3 -4
  101. uncountable/types/api/recipes/remove_recipe_from_project.py +0 -1
  102. uncountable/types/api/recipes/set_recipe_inputs.py +6 -7
  103. uncountable/types/api/recipes/set_recipe_metadata.py +0 -1
  104. uncountable/types/api/recipes/set_recipe_output_annotations.py +5 -6
  105. uncountable/types/api/recipes/set_recipe_output_file.py +2 -3
  106. uncountable/types/api/recipes/set_recipe_outputs.py +8 -9
  107. uncountable/types/api/recipes/set_recipe_tags.py +2 -3
  108. uncountable/types/api/recipes/unarchive_recipes.py +0 -1
  109. uncountable/types/api/recipes/unlock_recipes.py +2 -3
  110. uncountable/types/api/triggers/run_trigger.py +1 -2
  111. uncountable/types/api/uploader/invoke_uploader.py +2 -3
  112. uncountable/types/async_batch.py +0 -1
  113. uncountable/types/async_batch_processor.py +23 -24
  114. uncountable/types/async_batch_t.py +5 -6
  115. uncountable/types/async_jobs.py +0 -1
  116. uncountable/types/async_jobs_t.py +1 -2
  117. uncountable/types/auth_retrieval.py +0 -1
  118. uncountable/types/auth_retrieval_t.py +2 -3
  119. uncountable/types/base.py +0 -1
  120. uncountable/types/base_t.py +2 -1
  121. uncountable/types/calculations.py +0 -1
  122. uncountable/types/calculations_t.py +0 -1
  123. uncountable/types/chemical_structure.py +0 -1
  124. uncountable/types/chemical_structure_t.py +3 -4
  125. uncountable/types/client_base.py +65 -66
  126. uncountable/types/client_config.py +0 -1
  127. uncountable/types/client_config_t.py +1 -2
  128. uncountable/types/curves.py +0 -1
  129. uncountable/types/curves_t.py +4 -5
  130. uncountable/types/entity.py +0 -1
  131. uncountable/types/entity_t.py +3 -4
  132. uncountable/types/experiment_groups.py +0 -1
  133. uncountable/types/experiment_groups_t.py +0 -1
  134. uncountable/types/field_values.py +0 -1
  135. uncountable/types/field_values_t.py +6 -7
  136. uncountable/types/fields.py +0 -1
  137. uncountable/types/fields_t.py +0 -1
  138. uncountable/types/generic_upload.py +0 -1
  139. uncountable/types/generic_upload_t.py +7 -8
  140. uncountable/types/id_source.py +0 -1
  141. uncountable/types/id_source_t.py +2 -3
  142. uncountable/types/identifier.py +0 -1
  143. uncountable/types/identifier_t.py +1 -2
  144. uncountable/types/input_attributes.py +0 -1
  145. uncountable/types/input_attributes_t.py +2 -3
  146. uncountable/types/inputs.py +0 -1
  147. uncountable/types/inputs_t.py +2 -3
  148. uncountable/types/integration_server.py +0 -1
  149. uncountable/types/integration_server_t.py +2 -3
  150. uncountable/types/job_definition.py +0 -1
  151. uncountable/types/job_definition_t.py +16 -17
  152. uncountable/types/outputs.py +0 -1
  153. uncountable/types/outputs_t.py +1 -2
  154. uncountable/types/overrides.py +0 -1
  155. uncountable/types/overrides_t.py +0 -1
  156. uncountable/types/permissions.py +0 -1
  157. uncountable/types/permissions_t.py +1 -2
  158. uncountable/types/phases.py +0 -1
  159. uncountable/types/phases_t.py +0 -1
  160. uncountable/types/post_base.py +0 -1
  161. uncountable/types/post_base_t.py +1 -2
  162. uncountable/types/queued_job.py +0 -1
  163. uncountable/types/queued_job_t.py +2 -3
  164. uncountable/types/recipe_identifiers.py +0 -1
  165. uncountable/types/recipe_identifiers_t.py +3 -4
  166. uncountable/types/recipe_inputs.py +0 -1
  167. uncountable/types/recipe_inputs_t.py +1 -2
  168. uncountable/types/recipe_links.py +0 -1
  169. uncountable/types/recipe_links_t.py +2 -3
  170. uncountable/types/recipe_metadata.py +0 -1
  171. uncountable/types/recipe_metadata_t.py +6 -7
  172. uncountable/types/recipe_output_metadata.py +0 -1
  173. uncountable/types/recipe_output_metadata_t.py +0 -1
  174. uncountable/types/recipe_tags.py +0 -1
  175. uncountable/types/recipe_tags_t.py +0 -1
  176. uncountable/types/recipe_workflow_steps.py +0 -1
  177. uncountable/types/recipe_workflow_steps_t.py +2 -3
  178. uncountable/types/recipes.py +0 -1
  179. uncountable/types/recipes_t.py +0 -1
  180. uncountable/types/response.py +0 -1
  181. uncountable/types/response_t.py +0 -1
  182. uncountable/types/secret_retrieval.py +0 -1
  183. uncountable/types/secret_retrieval_t.py +3 -4
  184. uncountable/types/units.py +0 -1
  185. uncountable/types/units_t.py +0 -1
  186. uncountable/types/users.py +0 -1
  187. uncountable/types/users_t.py +0 -1
  188. uncountable/types/webhook_job.py +0 -1
  189. uncountable/types/webhook_job_t.py +0 -1
  190. uncountable/types/workflows.py +0 -1
  191. uncountable/types/workflows_t.py +1 -2
  192. UncountablePythonSDK-0.0.91.dist-info/RECORD +0 -301
  193. {UncountablePythonSDK-0.0.91.dist-info → UncountablePythonSDK-0.0.93.dist-info}/top_level.txt +0 -0
@@ -10,7 +10,7 @@ import re
10
10
  from typing import Collection, cast
11
11
 
12
12
  from pkgs.serialization import yaml
13
- from pkgs.serialization_util.serialization_helpers import serialize_for_api
13
+ from pkgs.serialization_util import serialize_for_api
14
14
 
15
15
  from . import builder, util
16
16
  from .builder import EndpointGuideKey, RootGuideKey
@@ -563,7 +563,7 @@ def _emit_endpoint(
563
563
  ep = namespace.endpoint
564
564
  gctx.paths.append(
565
565
  EmitOpenAPIPath(
566
- path=f"/{ep.path_root}/{ep.path_dirname}/{ep.path_basename}",
566
+ path=f"/{ep.resolved_path}",
567
567
  ref=ref_path,
568
568
  )
569
569
  )
@@ -7,7 +7,7 @@ WORK-IN-PROGRESS, DON'T USE!
7
7
  from collections import defaultdict
8
8
  from dataclasses import dataclass, field
9
9
 
10
- from pkgs.serialization_util.serialization_helpers import JsonValue
10
+ from pkgs.serialization_util import JsonValue
11
11
 
12
12
  from . import builder
13
13
  from .open_api_util import OpenAPIType
@@ -2,19 +2,17 @@ import dataclasses
2
2
  import io
3
3
  import os
4
4
  from decimal import Decimal
5
- from typing import Any, Optional
5
+ from typing import Any
6
6
 
7
7
  from . import builder, util
8
- from .builder import EndpointEmitType
8
+ from .builder import EndpointEmitType, EndpointSpecificPath
9
9
  from .config import PythonConfig
10
10
 
11
11
  INDENT = " "
12
12
  LINE_BREAK = "\n"
13
13
  MODIFY_NOTICE = "# DO NOT MODIFY -- This file is generated by type_spec\n"
14
14
  # Turn excess line length warning and turn off ruff formatting
15
- LINT_HEADER = (
16
- "# flake8: noqa: F821\n# ruff: noqa: E402 Q003\n# fmt: off\n# isort: skip_file\n"
17
- )
15
+ LINT_HEADER = "# ruff: noqa: E402 Q003\n# fmt: off\n# isort: skip_file\n"
18
16
  LINT_FOOTER = "# fmt: on\n"
19
17
  ROUTE_NOTICE = """# Routes are generated from $endpoint specifications in the
20
18
  # type_spec API YAML files. Refer to the section on endpoints in the type_spec/README"""
@@ -38,7 +36,7 @@ QUEUED_BATCH_REQUEST_STYPE = builder.SpecTypeDefnObject(
38
36
 
39
37
  @dataclasses.dataclass(kw_only=True)
40
38
  class TrackingContext:
41
- namespace: Optional[builder.SpecNamespace] = None
39
+ namespace: builder.SpecNamespace | None = None
42
40
  namespaces: set[builder.SpecNamespace] = dataclasses.field(default_factory=set)
43
41
  names: set[str] = dataclasses.field(default_factory=set)
44
42
 
@@ -221,7 +219,7 @@ def _emit_types_imports(*, out: io.StringIO, ctx: Context) -> None:
221
219
  out.write("import datetime # noqa: F401\n")
222
220
  out.write("from decimal import Decimal # noqa: F401\n")
223
221
  if ctx.use_enum:
224
- out.write("from pkgs.strenum_compat import StrEnum\n")
222
+ out.write("from enum import StrEnum\n")
225
223
  if ctx.use_dataclass:
226
224
  out.write("import dataclasses\n")
227
225
  out.write("from pkgs.serialization import serial_class\n")
@@ -344,6 +342,13 @@ def _emit_types(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
344
342
 
345
343
  ENDPOINT_METHOD = "ENDPOINT_METHOD"
346
344
  ENDPOINT_PATH = "ENDPOINT_PATH"
345
+ # will be removed in Q1 2025 when ENDPOINT_PATH is made api_endpoint-agnostic
346
+ # is used when the API call has multiple endpoints for the one endpoint that isn't equal to the top_namespace
347
+ ENDPOINT_PATH_ALTERNATE = "ENDPOINT_PATH_ALTERNATE"
348
+
349
+
350
+ def _get_epf_root(endpoint_specific_path: EndpointSpecificPath) -> str:
351
+ return endpoint_specific_path.root
347
352
 
348
353
 
349
354
  def _emit_namespace(ctx: Context, namespace: builder.SpecNamespace) -> None:
@@ -351,9 +356,20 @@ def _emit_namespace(ctx: Context, namespace: builder.SpecNamespace) -> None:
351
356
  if endpoint is not None:
352
357
  ctx.out.write("\n")
353
358
  ctx.out.write(f'{ENDPOINT_METHOD} = "{endpoint.method.upper()}"\n')
354
- ctx.out.write(
355
- f'{ENDPOINT_PATH} = "{endpoint.path_root}/{endpoint.path_dirname}/{endpoint.path_basename}"\n'
356
- )
359
+ for endpoint_specific_path in sorted(
360
+ endpoint.path_per_api_endpoint.values(), key=_get_epf_root
361
+ ):
362
+ endpoint_path_name = ENDPOINT_PATH
363
+
364
+ if (
365
+ len(endpoint.path_per_api_endpoint.keys()) > 1
366
+ and endpoint_specific_path.root != ctx.builder.top_namespace
367
+ ):
368
+ endpoint_path_name = ENDPOINT_PATH_ALTERNATE
369
+ ctx.names.add(ENDPOINT_PATH_ALTERNATE)
370
+ ctx.out.write(
371
+ f'{endpoint_path_name} = "{endpoint_specific_path.path_root}/{endpoint_specific_path.path_dirname}/{endpoint_specific_path.path_basename}"\n'
372
+ )
357
373
 
358
374
  ctx.names.add(ENDPOINT_METHOD)
359
375
  ctx.names.add(ENDPOINT_PATH)
@@ -443,8 +459,10 @@ def _emit_endpoint_invocation_function_signature(
443
459
  else []
444
460
  ) + (extra_params if extra_params is not None else [])
445
461
 
446
- assert endpoint.function is not None
447
- function_name = endpoint.function.split(".")[-1]
462
+ # All endpoints share a function name
463
+ function = endpoint.path_per_api_endpoint[endpoint.default_endpoint_key].function
464
+ assert function is not None
465
+ function_name = function.split(".")[-1]
448
466
  ctx.out.write(
449
467
  f"""
450
468
  def {function_name}(
@@ -702,39 +720,43 @@ def _emit_properties(
702
720
  if len(properties) > 0:
703
721
 
704
722
  def write_field(prop: builder.SpecProperty) -> None:
723
+ stype = prop.spec_type
705
724
  if prop.name_case == builder.NameCase.preserve:
706
725
  unconverted_keys.add(prop.name)
707
726
  py_name = python_field_name(prop.name, prop.name_case)
708
727
 
709
728
  if prop.convert_value == builder.PropertyConvertValue.no_convert:
710
729
  unconverted_values.add(py_name)
711
- elif not prop.spec_type.is_value_converted():
730
+ elif not stype.is_value_converted():
712
731
  assert prop.convert_value == builder.PropertyConvertValue.auto
713
732
  unconverted_values.add(py_name)
714
- if prop.spec_type.is_value_to_string():
733
+ if stype.is_value_to_string():
715
734
  to_string_values.add(py_name)
716
735
 
717
736
  if prop.parse_require:
718
737
  parse_require.add(py_name)
719
738
 
720
- ref_type = refer_to(ctx, prop.spec_type)
739
+ ref_type = refer_to(ctx, stype)
721
740
  default = None
722
741
  if prop.extant == builder.PropertyExtant.missing:
723
742
  ref_type = f"MissingType[{ref_type}]"
724
743
  default = "MISSING_SENTRY"
725
744
  ctx.use_missing = True
726
745
  elif prop.extant == builder.PropertyExtant.optional:
727
- ref_type = f"typing.Optional[{ref_type}]"
746
+ if isinstance(
747
+ stype, builder.SpecTypeInstance
748
+ ) and stype.defn_type.is_base_type(builder.BaseTypeName.s_optional):
749
+ pass # base type already adds the None union
750
+ elif ref_type == "None":
751
+ pass # no need to add a None union to a none type
752
+ else:
753
+ ref_type = f"{ref_type} | None"
728
754
  default = "None"
729
755
  elif prop.has_default:
730
- default = _emit_value(ctx, prop.spec_type, prop.default)
756
+ default = _emit_value(ctx, stype, prop.default)
731
757
  if (
732
- isinstance(prop.spec_type, builder.SpecTypeInstance)
733
- and (
734
- prop.spec_type.defn_type.is_base_type(
735
- builder.BaseTypeName.s_list
736
- )
737
- )
758
+ isinstance(stype, builder.SpecTypeInstance)
759
+ and (stype.defn_type.is_base_type(builder.BaseTypeName.s_list))
738
760
  and default == "[]"
739
761
  ):
740
762
  default = "dataclasses.field(default_factory=list)"
@@ -924,13 +946,22 @@ base_name_map = {
924
946
 
925
947
  def refer_to(ctx: TrackingContext, stype: builder.SpecType) -> str:
926
948
  if isinstance(stype, builder.SpecTypeInstance):
927
- params = ", ".join([refer_to(ctx, p) for p in stype.parameters])
949
+ params = [refer_to(ctx, p) for p in stype.parameters]
950
+
951
+ if stype.defn_type.is_base_type(builder.BaseTypeName.s_union):
952
+ if len(stype.parameters) == 1:
953
+ return f"typing.Union[{params[0]}]"
954
+ return " | ".join(params)
928
955
 
929
956
  if stype.defn_type.is_base_type(builder.BaseTypeName.s_readonly_array):
930
- assert len(stype.parameters) == 1, "Read Only Array takes one parameter"
931
- params = f"{params}, ..."
957
+ assert len(params) == 1, "Read Only Array takes one parameter"
958
+ return f"tuple[{params[0]}, ...]"
959
+
960
+ if stype.defn_type.is_base_type(builder.BaseTypeName.s_optional):
961
+ assert len(params) == 1, "Optional only takes one parameter"
962
+ return f"{params[0]} | None"
932
963
 
933
- return f"{refer_to(ctx, stype.defn_type)}[{params}]"
964
+ return f"{refer_to(ctx, stype.defn_type)}[{', '.join(params)}]"
934
965
 
935
966
  if isinstance(stype, builder.SpecTypeLiteralWrapper):
936
967
  return _emit_value(ctx, stype.value_type, stype.value)
@@ -956,23 +987,21 @@ def refer_to(ctx: TrackingContext, stype: builder.SpecType) -> str:
956
987
  SpecEndpoint = builder.SpecEndpoint
957
988
 
958
989
 
959
- def _route_identifier(endpoint: builder.SpecEndpoint) -> tuple[str, str, str]:
960
- return (endpoint.path_dirname, endpoint.path_basename, endpoint.method)
961
-
962
-
963
990
  def _emit_routes(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
964
991
  for endpoint_root in builder.api_endpoints:
965
992
  endpoints: list[SpecEndpoint] = []
966
993
  output = config.routes_output.get(endpoint_root)
967
994
  if output is None:
968
995
  continue
996
+ last_endpoint: SpecEndpoint | None = None
969
997
  for namespace in builder.namespaces.values():
970
998
  endpoint = namespace.endpoint
999
+ last_endpoint = endpoint
971
1000
  if endpoint is None:
972
1001
  continue
973
- if endpoint.root != endpoint_root:
1002
+ if endpoint_root not in endpoint.path_per_api_endpoint:
974
1003
  continue
975
- if endpoint.function is None:
1004
+ if endpoint.path_per_api_endpoint[endpoint_root].function is None:
976
1005
  continue
977
1006
 
978
1007
  endpoints.append(endpoint)
@@ -985,6 +1014,15 @@ def _emit_routes(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
985
1014
  from main.site.framework.types import StaticRouteType
986
1015
  """
987
1016
  )
1017
+
1018
+ def _route_identifier(endpoint: SpecEndpoint) -> tuple[str, str, str]:
1019
+ endpoint_specific_path = endpoint.path_per_api_endpoint[endpoint_root]
1020
+ return (
1021
+ endpoint_specific_path.path_dirname,
1022
+ endpoint_specific_path.path_basename,
1023
+ endpoint.method,
1024
+ )
1025
+
988
1026
  sorted_endpoints = sorted(endpoints, key=_route_identifier)
989
1027
 
990
1028
  assert len(endpoints) == len(set(map(_route_identifier, endpoints))), (
@@ -993,17 +1031,21 @@ from main.site.framework.types import StaticRouteType
993
1031
 
994
1032
  path_set = set()
995
1033
  for endpoint in sorted_endpoints:
996
- assert endpoint.function
997
- func_bits = endpoint.function.split(".")
1034
+ last_endpoint = endpoint
1035
+ endpoint_function_path = endpoint.path_per_api_endpoint[endpoint_root]
1036
+ assert endpoint_function_path.function
1037
+ func_bits = endpoint_function_path.function.split(".")
998
1038
  path = ".".join(func_bits[:-1])
999
1039
  if path in path_set:
1000
1040
  continue
1001
1041
  path_set.add(path)
1002
1042
  static_out.write(f"import {path}\n")
1003
1043
 
1044
+ assert last_endpoint is not None
1045
+
1004
1046
  static_out.write(
1005
1047
  f"""
1006
- ROUTE_PREFIX = "/{endpoint.path_root}"
1048
+ ROUTE_PREFIX = "/{last_endpoint.path_per_api_endpoint[endpoint_root].path_root}"
1007
1049
 
1008
1050
  ROUTES: list[StaticRouteType] = [
1009
1051
  """
@@ -1017,20 +1059,21 @@ ROUTES: list[StaticRouteType] = [
1017
1059
 
1018
1060
  from main.site.framework.types import DynamicRouteType
1019
1061
 
1020
- ROUTE_PREFIX = "/{endpoint.path_root}"
1062
+ ROUTE_PREFIX = "/{last_endpoint.path_per_api_endpoint[endpoint_root].path_root}"
1021
1063
 
1022
1064
  ROUTES: list[DynamicRouteType] = [
1023
1065
  """
1024
1066
  )
1025
1067
 
1026
1068
  for endpoint in sorted_endpoints:
1069
+ endpoint_function_path = endpoint.path_per_api_endpoint[endpoint_root]
1027
1070
  dynamic_out.write(
1028
- f'{INDENT}("{endpoint.path_dirname}/{endpoint.path_basename}", "{endpoint.function}", ["{endpoint.method.upper()}"]),\n'
1071
+ f'{INDENT}("{endpoint_function_path.path_dirname}/{endpoint_function_path.path_basename}", "{endpoint_function_path.function}", ["{endpoint.method.upper()}"]),\n'
1029
1072
  )
1030
1073
 
1031
- assert endpoint.function
1074
+ assert endpoint_function_path.function
1032
1075
  static_out.write(
1033
- f'{INDENT}("{endpoint.path_dirname}/{endpoint.path_basename}", {endpoint.function}, ["{endpoint.method.upper()}"]),\n'
1076
+ f'{INDENT}("{endpoint_function_path.path_dirname}/{endpoint_function_path.path_basename}", {endpoint_function_path.function}, ["{endpoint.method.upper()}"]),\n'
1034
1077
  )
1035
1078
 
1036
1079
  dynamic_out.write(f"{MODIFY_NOTICE}]\n")
@@ -1046,7 +1089,7 @@ def _emit_namespace_imports(
1046
1089
  *,
1047
1090
  out: io.StringIO,
1048
1091
  namespaces: set[builder.SpecNamespace],
1049
- from_namespace: Optional[builder.SpecNamespace],
1092
+ from_namespace: builder.SpecNamespace | None,
1050
1093
  config: PythonConfig,
1051
1094
  skip_non_sdk: bool = False,
1052
1095
  ) -> None:
@@ -1084,8 +1127,8 @@ def _emit_id_source(*, builder: builder.SpecBuilder, config: PythonConfig) -> No
1084
1127
  return None
1085
1128
  enum_out = io.StringIO()
1086
1129
  enum_out.write(f"{LINT_HEADER}{MODIFY_NOTICE}\n")
1087
- enum_out.write("from typing import Literal, Union\n")
1088
- enum_out.write("from pkgs.strenum_compat import StrEnum\n")
1130
+ enum_out.write("import typing\n")
1131
+ enum_out.write("from enum import StrEnum\n")
1089
1132
 
1090
1133
  ctx = TrackingContext()
1091
1134
  # In this context the propername
@@ -1101,11 +1144,11 @@ def _emit_id_source(*, builder: builder.SpecBuilder, config: PythonConfig) -> No
1101
1144
  known_keys = []
1102
1145
  enum_out.write("\nENUM_MAP: dict[str, type[StrEnum]] = {\n")
1103
1146
  for key in sorted(named_enums.keys()):
1104
- enum_out.write(f'"{key}": {named_enums[key]},\n')
1105
- known_keys.append(f'Literal["{key}"]')
1147
+ enum_out.write(f'{INDENT}"{key}": {named_enums[key]},\n')
1148
+ known_keys.append(f'"{key}"')
1106
1149
  enum_out.write(f"}}\n{MODIFY_NOTICE}\n")
1107
1150
 
1108
- enum_out.write(f"\nKnownEnumsType = Union[\n{INDENT}")
1151
+ enum_out.write(f"\nKnownEnumsType = typing.Literal[\n{INDENT}")
1109
1152
  enum_out.write(f",\n{INDENT}".join(known_keys))
1110
1153
  enum_out.write(f"\n]\n{MODIFY_NOTICE}\n")
1111
1154
 
@@ -1122,21 +1165,36 @@ def _emit_api_stubs(*, builder: builder.SpecBuilder, config: PythonConfig) -> No
1122
1165
 
1123
1166
  if endpoint is None:
1124
1167
  continue
1125
- if endpoint.root != endpoint_root:
1168
+ if endpoint_root not in endpoint.path_per_api_endpoint:
1126
1169
  continue
1127
- if endpoint.function is None:
1170
+
1171
+ endpoint_function = endpoint.path_per_api_endpoint[endpoint_root].function
1172
+ if endpoint_function is None:
1128
1173
  continue
1129
1174
 
1130
- module_dir, file_name, func_name = endpoint.function.rsplit(".", 2)
1175
+ module_dir, file_name, func_name = endpoint_function.rsplit(".", 2)
1131
1176
  module_path = os.path.abspath(module_dir.replace(".", "/"))
1132
1177
  api_stub_file = f"{module_path}/{file_name}.py"
1133
1178
  if os.path.isfile(api_stub_file):
1134
1179
  continue
1135
- _create_api_stub(api_stub_file, file_name, endpoint, config)
1180
+ _create_api_stub(
1181
+ api_stub_file=api_stub_file,
1182
+ file_name=file_name,
1183
+ endpoint=endpoint,
1184
+ config=config,
1185
+ endpoint_root=endpoint_root,
1186
+ top_namespace=builder.top_namespace,
1187
+ )
1136
1188
 
1137
1189
 
1138
1190
  def _create_api_stub(
1139
- api_stub_file: str, file_name: str, endpoint: SpecEndpoint, config: PythonConfig
1191
+ *,
1192
+ api_stub_file: str,
1193
+ file_name: str,
1194
+ endpoint: SpecEndpoint,
1195
+ config: PythonConfig,
1196
+ endpoint_root: str,
1197
+ top_namespace: str,
1140
1198
  ) -> None:
1141
1199
  assert (
1142
1200
  endpoint.method == builder.RouteMethod.post
@@ -1144,7 +1202,13 @@ def _create_api_stub(
1144
1202
  or endpoint.method == builder.RouteMethod.delete
1145
1203
  or endpoint.method == builder.RouteMethod.patch
1146
1204
  )
1147
- api_out = _create_api_function(file_name, endpoint, config)
1205
+ api_out = _create_api_function(
1206
+ file_name=file_name,
1207
+ endpoint=endpoint,
1208
+ config=config,
1209
+ endpoint_root=endpoint_root,
1210
+ top_namespace=top_namespace,
1211
+ )
1148
1212
  util.rewrite_file(api_stub_file, api_out.getvalue())
1149
1213
 
1150
1214
 
@@ -1153,15 +1217,22 @@ WRAP_ARGS_END = "\n"
1153
1217
 
1154
1218
 
1155
1219
  def _create_api_function(
1156
- file_name: str, endpoint: SpecEndpoint, config: PythonConfig
1220
+ *,
1221
+ file_name: str,
1222
+ endpoint: SpecEndpoint,
1223
+ config: PythonConfig,
1224
+ endpoint_root: str,
1225
+ top_namespace: str,
1157
1226
  ) -> io.StringIO:
1227
+ endpoint_specific_path = endpoint.path_per_api_endpoint[endpoint_root]
1228
+ assert endpoint_specific_path is not None
1158
1229
  api_out = io.StringIO()
1159
1230
  python_api_type_root = f"{config.types_package}.api"
1160
- dot_dirname = endpoint.path_dirname.replace("/", ".")
1231
+ dot_dirname = endpoint_specific_path.path_dirname.replace("/", ".")
1161
1232
  api_import = (
1162
1233
  f"{python_api_type_root}.{dot_dirname}.{file_name}"
1163
1234
  if dot_dirname != ""
1164
- else f"{python_api_type_root}.{endpoint.path_basename}"
1235
+ else f"{python_api_type_root}.{endpoint_specific_path.path_basename}"
1165
1236
  )
1166
1237
 
1167
1238
  if endpoint.method == builder.RouteMethod.post:
@@ -1173,9 +1244,20 @@ def _create_api_function(
1173
1244
  elif endpoint.method == builder.RouteMethod.patch:
1174
1245
  validated_method = "validated_patch"
1175
1246
 
1176
- ruff_requires_wrap = len(endpoint.path_basename) > 14
1247
+ ruff_requires_wrap = len(endpoint_specific_path.path_basename) > 14
1248
+
1249
+ account_type = (
1250
+ endpoint_specific_path.root
1251
+ if endpoint_specific_path.root not in ["external", "portal"]
1252
+ else "materials"
1253
+ )
1177
1254
 
1178
- account_type = endpoint.root if endpoint.root != "external" else "materials"
1255
+ endpoint_path_name = (
1256
+ ENDPOINT_PATH_ALTERNATE
1257
+ if len(endpoint.path_per_api_endpoint.keys()) > 1
1258
+ and endpoint_specific_path.root != top_namespace
1259
+ else ENDPOINT_PATH
1260
+ )
1179
1261
 
1180
1262
  api_out.write(
1181
1263
  f"""import {api_import} as api
@@ -1183,8 +1265,8 @@ from main.db.session import Session, SessionMaker
1183
1265
  from main.site.decorators import APIError, APIResponse, {validated_method}
1184
1266
 
1185
1267
 
1186
- @{validated_method}(api.ENDPOINT_PATH, "{account_type}", api.Arguments)
1187
- def {endpoint.path_basename}({WRAP_ARGS_START if ruff_requires_wrap else ""}args: api.Arguments, client_sm: SessionMaker{WRAP_ARGS_END if ruff_requires_wrap else ""}) -> APIResponse[api.Data]:
1268
+ @{validated_method}(api.{endpoint_path_name}, "{account_type}", api.Arguments)
1269
+ def {endpoint_specific_path.path_basename}({WRAP_ARGS_START if ruff_requires_wrap else ""}args: api.Arguments, client_sm: SessionMaker{WRAP_ARGS_END if ruff_requires_wrap else ""}) -> APIResponse[api.Data]:
1188
1270
  with Session(client_sm) as session:
1189
1271
  # return APIResponse(data=api.Data())
1190
1272
  pass
@@ -1216,9 +1298,9 @@ def _emit_api_argument_lookup(
1216
1298
 
1217
1299
  if endpoint is None:
1218
1300
  continue
1219
- if endpoint.root != endpoint_root:
1301
+ if endpoint_root not in endpoint.path_per_api_endpoint:
1220
1302
  continue
1221
- if endpoint.function is None:
1303
+ if endpoint.path_per_api_endpoint[endpoint_root].function is None:
1222
1304
  continue
1223
1305
  if "Arguments" not in namespace.types or "Data" not in namespace.types:
1224
1306
  continue
@@ -1244,7 +1326,7 @@ def _emit_api_argument_lookup(
1244
1326
  mapping += f"{INDENT}{INDENT}route_group={route_group},\n"
1245
1327
  mapping += f"{INDENT}{INDENT}account_type={account_type},\n"
1246
1328
  mapping += f"{INDENT}{INDENT}route={import_alias}.ENDPOINT_PATH,\n"
1247
- mapping += f'{INDENT}{INDENT}handler="{endpoint.function}",\n'
1329
+ mapping += f'{INDENT}{INDENT}handler="{endpoint.path_per_api_endpoint[endpoint_root].function}",\n'
1248
1330
  mapping += f"{INDENT}{INDENT}method={import_alias}.ENDPOINT_METHOD,\n"
1249
1331
  mapping += f"{INDENT})"
1250
1332
  mappings.append(mapping)
@@ -1299,7 +1381,7 @@ ASYNC_BATCH_PROCESSOR_FILENAME = "async_batch_processor"
1299
1381
  ASYNC_BATCH_PROCESSOR_IMPORTS = [
1300
1382
  "import uuid",
1301
1383
  "from abc import ABC, abstractmethod",
1302
- "from pkgs.serialization_util.serialization_helpers import serialize_for_api",
1384
+ "from pkgs.serialization_util import serialize_for_api",
1303
1385
  ]
1304
1386
 
1305
1387
 
@@ -2,6 +2,7 @@ import io
2
2
  import os
3
3
 
4
4
  from . import builder, util
5
+ from .builder import EndpointKey, EndpointSpecificPath
5
6
  from .config import TypeScriptConfig
6
7
  from .emit_io_ts import emit_type_io_ts
7
8
  from .emit_typescript_util import (
@@ -46,7 +47,10 @@ def _emit_types(builder: builder.SpecBuilder, config: TypeScriptConfig) -> None:
46
47
  and len(namespace.constants) == 0
47
48
  ):
48
49
  # Try to capture some common incompleteness errors
49
- if namespace.endpoint is None or namespace.endpoint.function is None:
50
+ if namespace.endpoint is None or any(
51
+ endpoint_specific_path.function is None
52
+ for endpoint_specific_path in namespace.endpoint.path_per_api_endpoint.values()
53
+ ):
50
54
  raise Exception(
51
55
  f"Namespace {'/'.join(namespace.path)} is incomplete. It should have an endpoint with function, types, and/or constants"
52
56
  )
@@ -123,6 +127,7 @@ def _emit_endpoint(
123
127
  has_data = "Data" in namespace.types
124
128
  has_deprecated_result = "DeprecatedResult" in namespace.types
125
129
  is_binary = endpoint.result_type == builder.ResultType.binary
130
+ has_multiple_endpoints = len(endpoint.path_per_api_endpoint) > 1
126
131
 
127
132
  result_type_count = sum([has_data, has_deprecated_result, is_binary])
128
133
  assert result_type_count < 2
@@ -157,42 +162,84 @@ def _emit_endpoint(
157
162
  wrap_call = (
158
163
  f"{wrap_name}<Arguments>" if is_binary else f"{wrap_name}<Arguments, Response>"
159
164
  )
165
+
166
+ unc_base_api_imports = (
167
+ f"appSpecificApiPath, {wrap_name}" if has_multiple_endpoints else wrap_name
168
+ )
169
+ unc_types_imports = (
170
+ 'import { ApplicationT } from "unc_types"\n' if has_multiple_endpoints else ""
171
+ )
172
+
160
173
  type_path = f"unc_types/{'/'.join(namespace.path)}"
161
174
 
162
175
  if is_binary:
163
- tsx_response_part = f"""import {{ {wrap_name} }} from "unc_base/api"
176
+ tsx_response_part = f"""import {{ {unc_base_api_imports} }} from "unc_base/api"
164
177
  import type {{ Arguments }} from "{type_path}"
165
-
178
+ {unc_types_imports}
166
179
  export type {{ Arguments }}
167
180
  """
168
181
  elif has_data and endpoint.has_attachment:
169
- tsx_response_part = f"""import {{ {wrap_name}, type AttachmentResponse }} from "unc_base/api"
182
+ tsx_response_part = f"""import {{ {unc_base_api_imports}, type AttachmentResponse }} from "unc_base/api"
170
183
  import type {{ Arguments, Data }} from "{type_path}"
171
-
184
+ {unc_types_imports}
172
185
  export type {{ Arguments, Data }}
173
186
  export type Response = AttachmentResponse<Data>
174
187
  """
175
188
  elif has_data:
176
- tsx_response_part = f"""import {{ {wrap_name}, type JsonResponse }} from "unc_base/api"
189
+ tsx_response_part = f"""import {{ {unc_base_api_imports}, type JsonResponse }} from "unc_base/api"
177
190
  import type {{ Arguments, Data }} from "{type_path}"
178
-
191
+ {unc_types_imports}
179
192
  export type {{ Arguments, Data }}
180
193
  export type Response = JsonResponse<Data>
181
194
  """
182
195
 
183
196
  else:
184
197
  assert has_deprecated_result
185
- tsx_response_part = f"""import {{ {wrap_name} }} from "unc_base/api"
198
+ tsx_response_part = f"""import {{ {unc_base_api_imports} }} from "unc_base/api"
186
199
  import type {{ Arguments, DeprecatedResult }} from "{type_path}"
187
-
200
+ {unc_types_imports}
188
201
  export type {{ Arguments }}
189
202
  export type Response = DeprecatedResult
190
203
  """
191
204
 
205
+ """
206
+
207
+ export const apiCall = buildWrappedGetCall<Arguments, Response>(
208
+ appSpecificApiPath({
209
+ [ApplicationT.FrontendApplication.materials]: "api/materials/common/list_id_source",
210
+ }),
211
+ )
212
+
213
+
214
+ """
215
+
216
+ if not has_multiple_endpoints:
217
+ default_endpoint_path = endpoint.path_per_api_endpoint[
218
+ endpoint.default_endpoint_key
219
+ ]
220
+ endpoint_path_part = f'"{default_endpoint_path.path_root}/{default_endpoint_path.path_dirname}/{default_endpoint_path.path_basename}",'
221
+ else:
222
+ path_lookup_map = ""
223
+ api_endpoint_key: EndpointKey
224
+ endpoint_specific_path: EndpointSpecificPath
225
+ for (
226
+ api_endpoint_key,
227
+ endpoint_specific_path,
228
+ ) in endpoint.path_per_api_endpoint.items():
229
+ full_path = f"{endpoint_specific_path.path_root}/{endpoint_specific_path.path_dirname}/{endpoint_specific_path.path_basename}"
230
+ frontend_app_value = config.endpoint_to_frontend_app_type[api_endpoint_key]
231
+
232
+ path_lookup_map += (
233
+ f'\n [ApplicationT.{frontend_app_value}]: "{full_path}",'
234
+ )
235
+
236
+ endpoint_path_part = f"""appSpecificApiPath({{{path_lookup_map}
237
+ }}),"""
238
+
192
239
  tsx_api = f"""{MODIFY_NOTICE}
193
240
  {data_loader_head}{tsx_response_part}
194
241
  export const apiCall = {wrap_call}(
195
- "{endpoint.path_root}/{endpoint.path_dirname}/{endpoint.path_basename}",
242
+ {endpoint_path_part}
196
243
  )
197
244
  {data_loader_body}"""
198
245
 
@@ -1,7 +1,6 @@
1
1
  import os
2
2
  from collections.abc import Callable
3
3
  from io import StringIO
4
- from typing import Optional
5
4
 
6
5
  from shelljob import fs
7
6
 
@@ -41,7 +40,7 @@ def find_and_handle_files(
41
40
  handler(file_name, file.read())
42
41
 
43
42
 
44
- def load_types(config: Config) -> Optional[SpecBuilder]:
43
+ def load_types(config: Config) -> SpecBuilder | None:
45
44
  builder = SpecBuilder(
46
45
  api_endpoints=config.api_endpoint, top_namespace=config.top_namespace
47
46
  )
@@ -1,6 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from enum import StrEnum
3
- from typing import Optional
4
3
 
5
4
 
6
5
  class OpenAPIType(ABC):
@@ -158,7 +157,7 @@ class OpenAPIObjectType(OpenAPIType):
158
157
  description: str | None = None,
159
158
  nullable: bool = False,
160
159
  *,
161
- property_desc: Optional[dict[str, str]] = None,
160
+ property_desc: dict[str, str] | None = None,
162
161
  ) -> None:
163
162
  self.properties = properties
164
163
  if property_desc is None:
@@ -22,6 +22,8 @@ PureJsonScalar = Union[str, float, bool, None]
22
22
  # Regular expressions for identifying ref names and IDs. Ref names should be
23
23
  # using this regular expression as a constriant in the database.
24
24
  REF_NAME_REGEX = r"^[a-zA-Z0-9_/-]+$"
25
+ REF_NAME_STRICT_REGEX_STRING = "^[a-zA-Z_][a-zA-Z0-9_]*$"
26
+ REF_NAME_STRICT_REGEX = rf"{REF_NAME_STRICT_REGEX_STRING}"
25
27
  # Ids matching a strict integer number are converted to integers
26
28
  ID_REGEX = r"-?[1-9][0-9]{0,20}"
27
29