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
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  from dataclasses import dataclass
3
3
  from io import BytesIO
4
- from typing import Optional, Union
4
+ from typing import Union
5
5
 
6
6
  import paramiko
7
7
  from azure.core.credentials import (
@@ -17,9 +17,9 @@ class FileObjectData:
17
17
  file_data: bytes
18
18
  file_IO: BytesIO
19
19
  filename: str
20
- filepath: Optional[str] = None
21
- mime_type: Optional[str] = None
22
- metadata: Optional[dict[str, str]] = None
20
+ filepath: str | None = None
21
+ mime_type: str | None = None
22
+ metadata: dict[str, str] | None = None
23
23
 
24
24
 
25
25
  @dataclass
@@ -39,7 +39,7 @@ class FileSystemFileReference:
39
39
  class RemoteObjectReference:
40
40
  file_id: str
41
41
  mime_type: str
42
- filename: Optional[str] = None
42
+ filename: str | None = None
43
43
 
44
44
  @property
45
45
  def is_dir(self) -> bool:
@@ -63,7 +63,7 @@ class FileSystemSFTPConfig:
63
63
  pem_path: str | None
64
64
  pem_key: paramiko.RSAKey | None = None
65
65
  password: str | None = None
66
- valid_extensions: Optional[tuple[str]] = None
66
+ valid_extensions: tuple[str] | None = None
67
67
  recursive: bool = True
68
68
 
69
69
 
@@ -71,10 +71,10 @@ class FileSystemSFTPConfig:
71
71
  class FileSystemS3Config:
72
72
  endpoint_url: str
73
73
  bucket_name: str
74
- region_name: Optional[str]
75
- access_key_id: Optional[str]
76
- secret_access_key: Optional[str]
77
- session_token: Optional[str]
74
+ region_name: str | None
75
+ access_key_id: str | None
76
+ secret_access_key: str | None
77
+ session_token: str | None
78
78
 
79
79
 
80
80
  @dataclass(kw_only=True)
@@ -6,7 +6,7 @@ T = typing.TypeVar("T")
6
6
 
7
7
  @dataclasses.dataclass(kw_only=True, frozen=True, eq=True)
8
8
  class SerialBase:
9
- named_type_path: typing.Optional[str] = None
9
+ named_type_path: str | None = None
10
10
  # Indicates this type is allowed in dynamic lookups, such as via a named_type_path
11
11
  # This isn't meant to be limting, but to catalog all the types where we need it
12
12
  is_dynamic_allowed: bool = False
@@ -16,7 +16,7 @@ class SerialBase:
16
16
  from_decorator: bool = False
17
17
 
18
18
 
19
- def get_serial_annotation(parsed_type: type[T]) -> SerialBase | None:
19
+ def get_serial_annotation[T](parsed_type: type[T]) -> SerialBase | None:
20
20
  if not hasattr(parsed_type, "__metadata__"):
21
21
  return None
22
22
  metadata = parsed_type.__metadata__ # type:ignore[attr-defined]
@@ -28,13 +28,13 @@ def get_serial_annotation(parsed_type: type[T]) -> SerialBase | None:
28
28
  return serial
29
29
 
30
30
 
31
- class SerialInspector(typing.Generic[T]):
31
+ class SerialInspector[T]:
32
32
  def __init__(self, parsed_type: type[T], serial_base: SerialBase) -> None:
33
33
  self._parsed_type = parsed_type
34
34
  self._serial_base = serial_base
35
35
 
36
36
  @property
37
- def named_type_path(self) -> typing.Optional[str]:
37
+ def named_type_path(self) -> str | None:
38
38
  return self._serial_base.named_type_path
39
39
 
40
40
  @property
@@ -53,7 +53,7 @@ class SerialInspector(typing.Generic[T]):
53
53
  return self._serial_base.is_dynamic_allowed
54
54
 
55
55
 
56
- def unwrap_annotated(parsed_type: type[T]) -> type[T]:
56
+ def unwrap_annotated[T](parsed_type: type[T]) -> type[T]:
57
57
  """
58
58
  If the type is an annotated type then return the origin of it.
59
59
  Otherwise return the original type.
@@ -26,5 +26,5 @@ MISSING_SENTRY = MissingSentryType()
26
26
  MissingType = Union[MissingSentryType, ClassT]
27
27
 
28
28
 
29
- def coalesce_missing_sentry(value: MissingType[ClassT]) -> Optional[ClassT]:
29
+ def coalesce_missing_sentry[ClassT](value: MissingType[ClassT]) -> ClassT | None:
30
30
  return None if isinstance(value, MissingSentryType) else value
@@ -16,7 +16,7 @@ class _SerialAlias(SerialBase):
16
16
 
17
17
  def serial_alias_annotation(
18
18
  *,
19
- named_type_path: typing.Optional[str] = None,
19
+ named_type_path: str | None = None,
20
20
  is_dynamic_allowed: bool = False,
21
21
  ) -> _SerialAlias:
22
22
  return _SerialAlias(
@@ -26,7 +26,7 @@ def serial_alias_annotation(
26
26
  )
27
27
 
28
28
 
29
- def _get_serial_alias(parsed_type: type[T]) -> _SerialAlias | None:
29
+ def _get_serial_alias[T](parsed_type: type[T]) -> _SerialAlias | None:
30
30
  serial = get_serial_annotation(parsed_type)
31
31
  if not isinstance(serial, _SerialAlias):
32
32
  return None
@@ -39,7 +39,7 @@ class SerialAliasInspector(SerialInspector[T]):
39
39
  self._serial_alias = serial_alias
40
40
 
41
41
 
42
- def get_serial_alias_data(parsed_type: type[T]) -> SerialAliasInspector[T] | None:
42
+ def get_serial_alias_data[T](parsed_type: type[T]) -> SerialAliasInspector[T] | None:
43
43
  serial = _get_serial_alias(parsed_type)
44
44
  if serial is None:
45
45
  return None
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import dataclasses
4
4
  from collections.abc import Callable
5
5
  from enum import StrEnum
6
- from typing import Any, Optional, TypeVar, cast
6
+ from typing import Any, TypeVar, cast
7
7
 
8
8
  from .annotation import SerialBase, SerialInspector
9
9
 
@@ -16,7 +16,7 @@ class _SerialClassData(SerialBase):
16
16
  unconverted_values: set[str] = dataclasses.field(default_factory=set)
17
17
  to_string_values: set[str] = dataclasses.field(default_factory=set)
18
18
  parse_require: set[str] = dataclasses.field(default_factory=set)
19
- named_type_path: Optional[str] = None
19
+ named_type_path: str | None = None
20
20
 
21
21
 
22
22
  EMPTY_SERIAL_CLASS_DATA = _SerialClassData()
@@ -24,11 +24,11 @@ EMPTY_SERIAL_CLASS_DATA = _SerialClassData()
24
24
 
25
25
  def serial_class(
26
26
  *,
27
- unconverted_keys: Optional[set[str]] = None,
28
- unconverted_values: Optional[set[str]] = None,
29
- to_string_values: Optional[set[str]] = None,
30
- parse_require: Optional[set[str]] = None,
31
- named_type_path: Optional[str] = None,
27
+ unconverted_keys: set[str] | None = None,
28
+ unconverted_values: set[str] | None = None,
29
+ to_string_values: set[str] | None = None,
30
+ parse_require: set[str] | None = None,
31
+ named_type_path: str | None = None,
32
32
  is_dynamic_allowed: bool = False,
33
33
  ) -> Callable[[_ClassT], _ClassT]:
34
34
  """
@@ -119,7 +119,7 @@ def get_merged_serial_class_data(type_class: type[Any]) -> _SerialClassData | No
119
119
  return base_class_data
120
120
 
121
121
 
122
- def get_serial_class_data(
122
+ def get_serial_class_data[_ClassT](
123
123
  type_class: type[_ClassT],
124
124
  ) -> SerialClassDataInspector[_ClassT]:
125
125
  return SerialClassDataInspector(
@@ -134,7 +134,7 @@ class _SerialStringEnumData:
134
134
 
135
135
 
136
136
  def serial_string_enum(
137
- *, labels: Optional[dict[str, str]] = None, deprecated: Optional[set[str]] = None
137
+ *, labels: dict[str, str] | None = None, deprecated: set[str] | None = None
138
138
  ) -> Callable[[_ClassT], _ClassT]:
139
139
  """
140
140
  A decorator for enums to provide serialization data, including labels.
@@ -153,7 +153,7 @@ class SerialStringEnumInspector:
153
153
  def __init__(self, current: _SerialStringEnumData) -> None:
154
154
  self.current = current
155
155
 
156
- def get_label(self, value: str) -> Optional[str]:
156
+ def get_label(self, value: str) -> str | None:
157
157
  return self.current.labels.get(value)
158
158
 
159
159
  def get_deprecated(self, value: str) -> bool:
@@ -6,7 +6,7 @@ from .serial_class import get_merged_serial_class_data
6
6
  T = typing.TypeVar("T")
7
7
 
8
8
 
9
- def get_serial_data(parsed_type: type[T]) -> SerialInspector[T] | None:
9
+ def get_serial_data[T](parsed_type: type[T]) -> SerialInspector[T] | None:
10
10
  serial = get_serial_annotation(parsed_type)
11
11
  if serial is None:
12
12
  serial = get_merged_serial_class_data(parsed_type)
@@ -6,7 +6,7 @@ from .annotation import SerialBase, SerialInspector, get_serial_annotation
6
6
  T = typing.TypeVar("T")
7
7
 
8
8
 
9
- class IdentityHashWrapper(typing.Generic[T]):
9
+ class IdentityHashWrapper[T]:
10
10
  """This allows unhashable types to be used in the SerialUnion, like dict.
11
11
  Since we have only one copy of the types themselves, we rely on
12
12
  object identity for the hashing."""
@@ -27,15 +27,15 @@ class _SerialUnion(SerialBase):
27
27
 
28
28
  # If specified, indicates the Union has a discriminator which should be used to
29
29
  # determine which type to parse.
30
- discriminator: typing.Optional[str] = None
31
- discriminator_map: typing.Optional[IdentityHashWrapper[dict[str, type]]] = None
30
+ discriminator: str | None = None
31
+ discriminator_map: IdentityHashWrapper[dict[str, type]] | None = None
32
32
 
33
33
 
34
34
  def serial_union_annotation(
35
35
  *,
36
- discriminator: typing.Optional[str] = None,
37
- discriminator_map: typing.Optional[dict[str, type]] = None,
38
- named_type_path: typing.Optional[str] = None,
36
+ discriminator: str | None = None,
37
+ discriminator_map: dict[str, type] | None = None,
38
+ named_type_path: str | None = None,
39
39
  is_dynamic_allowed: bool = False,
40
40
  ) -> _SerialUnion:
41
41
  return _SerialUnion(
@@ -49,7 +49,7 @@ def serial_union_annotation(
49
49
  )
50
50
 
51
51
 
52
- def _get_serial_union(parsed_type: type[T]) -> _SerialUnion | None:
52
+ def _get_serial_union[T](parsed_type: type[T]) -> _SerialUnion | None:
53
53
  serial = get_serial_annotation(parsed_type)
54
54
  if not isinstance(serial, _SerialUnion):
55
55
  return None
@@ -66,17 +66,17 @@ class SerialClassInspector(SerialInspector[T]):
66
66
  return typing.get_args(self._parsed_type)[0] # type:ignore[no-any-return]
67
67
 
68
68
  @property
69
- def discriminator(self) -> typing.Optional[str]:
69
+ def discriminator(self) -> str | None:
70
70
  return self._serial_union.discriminator
71
71
 
72
72
  @property
73
- def discriminator_map(self) -> typing.Optional[dict[str, type]]:
73
+ def discriminator_map(self) -> dict[str, type] | None:
74
74
  if self._serial_union.discriminator_map is None:
75
75
  return None
76
76
  return self._serial_union.discriminator_map.inner
77
77
 
78
78
 
79
- def get_serial_union_data(parsed_type: type[T]) -> SerialClassInspector[T] | None:
79
+ def get_serial_union_data[T](parsed_type: type[T]) -> SerialClassInspector[T] | None:
80
80
  serial = _get_serial_union(parsed_type)
81
81
  if serial is None:
82
82
  return None
@@ -2,6 +2,7 @@ from .convert_to_snakecase import convert_dict_to_snake_case
2
2
  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
+ JsonValue,
5
6
  serialize_for_api,
6
7
  serialize_for_storage,
7
8
  serialize_for_storage_dict,
@@ -14,4 +15,5 @@ __all__: list[str] = [
14
15
  "serialize_for_storage_dict",
15
16
  "iterate_fields",
16
17
  "dict_fields",
18
+ "JsonValue",
17
19
  ]
@@ -12,7 +12,6 @@ from typing import (
12
12
  Any,
13
13
  ClassVar,
14
14
  Protocol,
15
- TypeVar,
16
15
  Union,
17
16
  overload,
18
17
  )
@@ -34,14 +33,12 @@ if TYPE_CHECKING:
34
33
  else:
35
34
  JsonValue = Union[JsonScalar, dict[str, Any], list[Any]]
36
35
 
37
- T = TypeVar("T")
38
-
39
36
 
40
37
  class Dataclass(Protocol):
41
38
  __dataclass_fields__: ClassVar[dict] # type: ignore[type-arg,unused-ignore]
42
39
 
43
40
 
44
- def identity(x: T) -> T:
41
+ def identity[T](x: T) -> T:
45
42
  return x
46
43
 
47
44
 
@@ -6,7 +6,6 @@ import os
6
6
  import sys
7
7
  from collections import defaultdict
8
8
  from dataclasses import dataclass
9
- from typing import TypeVar
10
9
 
11
10
  from main.base.types import actions_registry_t
12
11
  from pkgs.type_spec import builder
@@ -22,9 +21,6 @@ key_short_description = "short_description"
22
21
  key_description = "description"
23
22
 
24
23
 
25
- TypeT = TypeVar("TypeT")
26
-
27
-
28
24
  class InvalidSpecException(Exception):
29
25
  pass
30
26
 
pkgs/type_spec/builder.py CHANGED
@@ -10,12 +10,13 @@ import re
10
10
  from collections import defaultdict
11
11
  from dataclasses import MISSING, dataclass
12
12
  from enum import Enum, StrEnum, auto
13
- from typing import Any, Optional, Self
13
+ from typing import Any, Self
14
14
 
15
15
  from . import util
16
16
  from .util import parse_type_str, unused
17
17
 
18
18
  RawDict = dict[Any, Any]
19
+ EndpointKey = str
19
20
 
20
21
 
21
22
  class StabilityLevel(StrEnum):
@@ -47,7 +48,7 @@ class PropertyConvertValue(StrEnum):
47
48
  @dataclass
48
49
  class SpecProperty:
49
50
  name: str
50
- label: Optional[str]
51
+ label: str | None
51
52
  spec_type: SpecType
52
53
  extant: PropertyExtant
53
54
  convert_value: PropertyConvertValue
@@ -255,7 +256,7 @@ class SpecTypeLiteralWrapper(SpecType):
255
256
  return [self.value_type]
256
257
 
257
258
 
258
- def unwrap_literal_type(stype: SpecType) -> Optional[SpecTypeLiteralWrapper]:
259
+ def unwrap_literal_type(stype: SpecType) -> SpecTypeLiteralWrapper | None:
259
260
  if isinstance(stype, SpecTypeInstance) and stype.defn_type.is_base_type(
260
261
  BaseTypeName.s_literal
261
262
  ):
@@ -283,7 +284,7 @@ class SpecTypeDefn(SpecType):
283
284
  ) -> None:
284
285
  self.namespace = namespace
285
286
  self.name = name
286
- self.label: Optional[str] = None
287
+ self.label: str | None = None
287
288
 
288
289
  self.is_predefined = is_predefined
289
290
  self.name_case = NameCase.convert
@@ -455,7 +456,7 @@ class SpecTypeGenericParameter(SpecType):
455
456
 
456
457
 
457
458
  class SpecTypeDefnObject(SpecTypeDefn):
458
- base: Optional[SpecTypeDefnObject]
459
+ base: SpecTypeDefnObject | None
459
460
  parameters: list[str]
460
461
 
461
462
  def __init__(
@@ -463,7 +464,7 @@ class SpecTypeDefnObject(SpecTypeDefn):
463
464
  namespace: SpecNamespace,
464
465
  name: str,
465
466
  *,
466
- parameters: Optional[list[str]] = None,
467
+ parameters: list[str] | None = None,
467
468
  is_base: bool = False,
468
469
  is_predefined: bool = False,
469
470
  is_hashable: bool = False,
@@ -480,7 +481,7 @@ class SpecTypeDefnObject(SpecTypeDefn):
480
481
  self.parameters = parameters if parameters is not None else []
481
482
  self.is_hashable = is_hashable
482
483
  self.base = None
483
- self.properties: Optional[dict[str, SpecProperty]] = None
484
+ self.properties: dict[str, SpecProperty] | None = None
484
485
  self._kw_only: bool = True
485
486
  self.desc: str | None = None
486
487
 
@@ -662,7 +663,7 @@ class SpecTypeDefnExternal(SpecTypeDefn):
662
663
  class StringEnumEntry:
663
664
  name: str
664
665
  value: str
665
- label: Optional[str] = None
666
+ label: str | None = None
666
667
  deprecated: bool = False
667
668
 
668
669
 
@@ -678,7 +679,7 @@ class SpecTypeDefnStringEnum(SpecTypeDefn):
678
679
  )
679
680
  self.values: dict[str, StringEnumEntry] = {}
680
681
  self.desc: str | None = None
681
- self.sql_type_name: Optional[str] = None
682
+ self.sql_type_name: str | None = None
682
683
  self.emit_id_source = False
683
684
  self.source_enums: list[SpecType] = []
684
685
 
@@ -810,13 +811,13 @@ RE_ENDPOINT_ROOT = re.compile(r"\${([_a-z]+)}")
810
811
 
811
812
  @dataclass(kw_only=True, frozen=True)
812
813
  class _EndpointPathDetails:
813
- root: str
814
+ root: EndpointKey
814
815
  root_path: str
815
816
  resolved_path: str
816
817
 
817
818
 
818
819
  def _resolve_endpoint_path(
819
- path: str, api_endpoints: dict[str, str]
820
+ path: str, api_endpoints: dict[EndpointKey, str]
820
821
  ) -> _EndpointPathDetails:
821
822
  root_path_source = path.split("/")[0]
822
823
  root_match = RE_ENDPOINT_ROOT.fullmatch(root_path_source)
@@ -840,19 +841,59 @@ class EndpointEmitType(StrEnum):
840
841
  EMIT_NOTHING = "emit_nothing"
841
842
 
842
843
 
843
- class SpecEndpoint:
844
- method: RouteMethod
845
- root: str
844
+ @dataclass(kw_only=True, frozen=True)
845
+ class EndpointSpecificPath:
846
+ root: EndpointKey
846
847
  path_root: str
847
848
  path_dirname: str
848
849
  path_basename: str
850
+ function: str | None
851
+
852
+
853
+ def parse_endpoint_specific_path(
854
+ builder: SpecBuilder,
855
+ data_per_endpoint: RawDict | None,
856
+ ) -> EndpointSpecificPath | None:
857
+ if data_per_endpoint is None:
858
+ return None
859
+ util.check_fields(
860
+ data_per_endpoint,
861
+ [
862
+ "path",
863
+ "function",
864
+ ],
865
+ )
866
+
867
+ if "path" not in data_per_endpoint or data_per_endpoint["path"] is None:
868
+ return None
869
+
870
+ path = data_per_endpoint["path"].split("/")
871
+
872
+ assert len(path) > 1, "invalid-endpoint-path"
873
+
874
+ path_details = _resolve_endpoint_path(
875
+ data_per_endpoint["path"], builder.api_endpoints
876
+ )
877
+
878
+ result = EndpointSpecificPath(
879
+ function=data_per_endpoint.get("function"),
880
+ path_dirname="/".join(path[1:-1]),
881
+ path_basename=path[-1],
882
+ root=path_details.root,
883
+ path_root=path_details.root_path,
884
+ )
885
+
886
+ return result
887
+
888
+
889
+ class SpecEndpoint:
890
+ method: RouteMethod
849
891
  data_loader: bool
850
892
  is_sdk: EndpointEmitType
851
893
  is_beta: bool
852
894
  stability_level: StabilityLevel | None
853
895
  # Don't emit TypeScript endpoint code
854
896
  suppress_ts: bool
855
- function: Optional[str]
856
897
  async_batch_path: str | None = None
857
898
  result_type: ResultType = ResultType.json
858
899
  has_attachment: bool = False
@@ -860,6 +901,10 @@ class SpecEndpoint:
860
901
  account_type: str | None
861
902
  route_group: str | None
862
903
 
904
+ # function, path details per api endpoint
905
+ path_per_api_endpoint: dict[str, EndpointSpecificPath]
906
+ default_endpoint_key: EndpointKey
907
+
863
908
  is_external: bool = False
864
909
 
865
910
  def __init__(self) -> None:
@@ -885,18 +930,11 @@ class SpecEndpoint:
885
930
  "has_attachment",
886
931
  "account_type",
887
932
  "route_group",
888
- ],
933
+ ]
934
+ + list(builder.api_endpoints.keys()),
889
935
  )
890
936
  self.method = RouteMethod(data["method"])
891
937
 
892
- path = data["path"].split("/")
893
-
894
- assert len(path) > 1, "invalid-endpoint-path"
895
-
896
- # handle ${external} in the same way we handle ${materials} for now
897
- self.path_dirname = "/".join(path[1:-1])
898
- self.path_basename = path[-1]
899
-
900
938
  data_loader = data.get("data_loader", False)
901
939
  assert isinstance(data_loader, bool)
902
940
  self.data_loader = data_loader
@@ -944,29 +982,70 @@ class SpecEndpoint:
944
982
  assert isinstance(async_batch_path, str)
945
983
  self.async_batch_path = async_batch_path
946
984
 
947
- self.function = data.get("function")
948
-
949
985
  suppress_ts = data.get("suppress_ts", False)
950
986
  assert isinstance(suppress_ts, bool)
951
987
  self.suppress_ts = suppress_ts
952
988
 
953
989
  self.result_type = ResultType(data.get("result_type", ResultType.json.value))
954
-
955
- path_details = _resolve_endpoint_path(data["path"], builder.api_endpoints)
956
- self.root = path_details.root
957
- self.path_root = path_details.root_path
990
+ self.has_attachment = data.get("has_attachment", False)
958
991
  self.desc = data.get("desc")
992
+
993
+ # compatibility with single-endpoint files
994
+ default_endpoint_path = parse_endpoint_specific_path(
995
+ builder,
996
+ {"path": data.get("path"), "function": data.get("function")},
997
+ )
998
+ if default_endpoint_path is not None:
999
+ assert default_endpoint_path.root in builder.api_endpoints, (
1000
+ "Default endpoint is not a valid API endpoint"
1001
+ )
1002
+ self.default_endpoint_key = default_endpoint_path.root
1003
+ self.path_per_api_endpoint = {
1004
+ self.default_endpoint_key: default_endpoint_path,
1005
+ }
1006
+ else:
1007
+ self.path_per_api_endpoint = {}
1008
+ shared_function_name = None
1009
+ for endpoint_key in builder.api_endpoints:
1010
+ endpoint_specific_path = parse_endpoint_specific_path(
1011
+ builder,
1012
+ data.get(endpoint_key),
1013
+ )
1014
+ if endpoint_specific_path is not None:
1015
+ self.path_per_api_endpoint[endpoint_key] = endpoint_specific_path
1016
+ if endpoint_specific_path.function is not None:
1017
+ fn_name = endpoint_specific_path.function.split(".")[-1]
1018
+ if shared_function_name is None:
1019
+ shared_function_name = fn_name
1020
+ assert shared_function_name == fn_name
1021
+
1022
+ if builder.top_namespace in self.path_per_api_endpoint:
1023
+ self.default_endpoint_key = builder.top_namespace
1024
+ elif len(self.path_per_api_endpoint) == 1:
1025
+ self.default_endpoint_key = next(
1026
+ iter(self.path_per_api_endpoint.keys())
1027
+ )
1028
+ else:
1029
+ raise RuntimeError("no clear default endpoint")
1030
+
1031
+ assert len(self.path_per_api_endpoint) > 0, (
1032
+ "Missing API endpoint path and function definitions for API call"
1033
+ )
1034
+
959
1035
  # IMPROVE: remove need for is_external flag
960
- self.is_external = self.path_root == "api/external"
961
- self.has_attachment = data.get("has_attachment", False)
1036
+ self.is_external = (
1037
+ self.path_per_api_endpoint[self.default_endpoint_key].path_root
1038
+ == "api/external"
1039
+ )
962
1040
 
963
1041
  assert self.is_sdk != EndpointEmitType.EMIT_ENDPOINT or self.desc is not None, (
964
- f"Endpoint description required for SDK endpoints, missing: {path}"
1042
+ f"Endpoint description required for SDK endpoints, missing: {self.resolved_path}"
965
1043
  )
966
1044
 
967
1045
  @property
968
1046
  def resolved_path(self: Self) -> str:
969
- return f"{self.path_root}/{self.path_dirname}/{self.path_basename}"
1047
+ default_endpoint_path = self.path_per_api_endpoint[self.default_endpoint_key]
1048
+ return f"{default_endpoint_path.path_root}/{default_endpoint_path.path_dirname}/{default_endpoint_path.path_basename}"
970
1049
 
971
1050
 
972
1051
  def _parse_const(
@@ -1079,14 +1158,14 @@ class SpecNamespace:
1079
1158
  ):
1080
1159
  self.types: dict[str, SpecTypeDefn] = {}
1081
1160
  self.constants: dict[str, SpecConstant] = {}
1082
- self.endpoint: Optional[SpecEndpoint] = None
1161
+ self.endpoint: SpecEndpoint | None = None
1083
1162
  self.emit_io_ts = False
1084
1163
  self.emit_type_info = False
1085
1164
  self.derive_types_from_io_ts = False
1086
- self._imports: Optional[list[str]] = None
1165
+ self._imports: list[str] | None = None
1087
1166
  self.path = name.split(".")
1088
1167
  self.name = self.path[-1]
1089
- self._order: Optional[int] = None
1168
+ self._order: int | None = None
1090
1169
 
1091
1170
  def _update_order(self, builder: SpecBuilder, recurse: int = 0) -> int:
1092
1171
  if self._order is not None:
@@ -1241,7 +1320,9 @@ class NameDataPair:
1241
1320
 
1242
1321
 
1243
1322
  class SpecBuilder:
1244
- def __init__(self, *, api_endpoints: dict[str, str], top_namespace: str) -> None:
1323
+ def __init__(
1324
+ self, *, api_endpoints: dict[EndpointKey, str], top_namespace: str
1325
+ ) -> None:
1245
1326
  self.top_namespace = top_namespace
1246
1327
  self.where: list[str] = []
1247
1328
  self.namespaces = {}
@@ -1339,7 +1420,7 @@ class SpecBuilder:
1339
1420
  self,
1340
1421
  path: util.ParsedTypePath,
1341
1422
  namespace: SpecNamespace,
1342
- scope: Optional[SpecTypeDefn] = None,
1423
+ scope: SpecTypeDefn | None = None,
1343
1424
  top: bool = False,
1344
1425
  ) -> SpecType:
1345
1426
  """
@@ -1420,7 +1501,7 @@ class SpecBuilder:
1420
1501
  )
1421
1502
 
1422
1503
  def parse_type(
1423
- self, namespace: SpecNamespace, spec: str, scope: Optional[SpecTypeDefn] = None
1504
+ self, namespace: SpecNamespace, spec: str, scope: SpecTypeDefn | None = None
1424
1505
  ) -> SpecType:
1425
1506
  self.push_where(spec)
1426
1507
  parsed_type = util.parse_type_str(spec)
pkgs/type_spec/config.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  from collections.abc import Callable, Mapping
3
3
  from dataclasses import dataclass
4
- from typing import Self, TypeVar
4
+ from typing import Self
5
5
 
6
6
  from pkgs.serialization import yaml
7
7
 
@@ -34,6 +34,9 @@ class TypeScriptConfig(BaseLanguageConfig):
34
34
  routes_output: str # folder for generate route files will be located.
35
35
  type_info_output: str # folder for generated type info files
36
36
  id_source_output: str | None = None # folder for emitted id source maps.
37
+ endpoint_to_frontend_app_type: dict[
38
+ str, str
39
+ ] # map from api_endpoint to frontend app type
37
40
 
38
41
  def __post_init__(self: Self) -> None:
39
42
  self.routes_output = self.routes_output
@@ -43,6 +46,11 @@ class TypeScriptConfig(BaseLanguageConfig):
43
46
  if self.id_source_output is not None
44
47
  else None
45
48
  )
49
+ self.endpoint_to_frontend_app_type = _parse_string_lookup(
50
+ "typescript_endpoint_to_frontend_app_type",
51
+ self.endpoint_to_frontend_app_type,
52
+ lambda x: x,
53
+ )
46
54
 
47
55
 
48
56
  @dataclass(kw_only=True)
@@ -95,10 +103,7 @@ class Config:
95
103
  open_api: OpenAPIConfig | None
96
104
 
97
105
 
98
- _T = TypeVar("_T")
99
-
100
-
101
- def _parse_language(config_class: type[_T], raw_value: ConfigValueType) -> _T:
106
+ def _parse_language[_T](config_class: type[_T], raw_value: ConfigValueType) -> _T:
102
107
  assert isinstance(raw_value, dict), "expecting language config to have key/values."
103
108
  return config_class(**raw_value)
104
109