UncountablePythonSDK 0.0.82__py3-none-any.whl → 0.0.132__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 (298) hide show
  1. docs/conf.py +54 -7
  2. docs/index.md +107 -4
  3. docs/integration_examples/create_ingredient.md +43 -0
  4. docs/integration_examples/create_output.md +56 -0
  5. docs/integration_examples/index.md +6 -0
  6. docs/justfile +2 -2
  7. docs/requirements.txt +6 -4
  8. examples/basic_auth.py +7 -0
  9. examples/create_ingredient_sdk.py +34 -0
  10. examples/download_files.py +26 -0
  11. examples/integration-server/jobs/materials_auto/concurrent_cron.py +11 -0
  12. examples/integration-server/jobs/materials_auto/example_cron.py +3 -0
  13. examples/integration-server/jobs/materials_auto/example_http.py +47 -0
  14. examples/integration-server/jobs/materials_auto/example_instrument.py +100 -0
  15. examples/integration-server/jobs/materials_auto/example_parse.py +140 -0
  16. examples/integration-server/jobs/materials_auto/example_predictions.py +61 -0
  17. examples/integration-server/jobs/materials_auto/example_runsheet_wh.py +39 -0
  18. examples/integration-server/jobs/materials_auto/example_wh.py +17 -9
  19. examples/integration-server/jobs/materials_auto/profile.yaml +61 -0
  20. examples/integration-server/pyproject.toml +10 -10
  21. examples/oauth.py +7 -0
  22. examples/set_recipe_metadata_file.py +1 -1
  23. examples/upload_files.py +1 -2
  24. pkgs/argument_parser/__init__.py +8 -0
  25. pkgs/argument_parser/_is_namedtuple.py +3 -0
  26. pkgs/argument_parser/argument_parser.py +196 -63
  27. pkgs/filesystem_utils/__init__.py +1 -0
  28. pkgs/filesystem_utils/_blob_session.py +144 -0
  29. pkgs/filesystem_utils/_gdrive_session.py +5 -5
  30. pkgs/filesystem_utils/_s3_session.py +2 -1
  31. pkgs/filesystem_utils/_sftp_session.py +6 -3
  32. pkgs/filesystem_utils/file_type_utils.py +30 -10
  33. pkgs/serialization/__init__.py +7 -2
  34. pkgs/serialization/annotation.py +64 -0
  35. pkgs/serialization/missing_sentry.py +1 -1
  36. pkgs/serialization/opaque_key.py +1 -1
  37. pkgs/serialization/serial_alias.py +47 -0
  38. pkgs/serialization/serial_class.py +40 -48
  39. pkgs/serialization/serial_generic.py +16 -0
  40. pkgs/serialization/serial_union.py +16 -16
  41. pkgs/serialization_util/__init__.py +6 -0
  42. pkgs/serialization_util/dataclasses.py +14 -0
  43. pkgs/serialization_util/serialization_helpers.py +15 -5
  44. pkgs/type_spec/actions_registry/__main__.py +0 -4
  45. pkgs/type_spec/actions_registry/emit_typescript.py +2 -4
  46. pkgs/type_spec/builder.py +248 -70
  47. pkgs/type_spec/builder_types.py +9 -0
  48. pkgs/type_spec/config.py +40 -7
  49. pkgs/type_spec/cross_output_links.py +99 -0
  50. pkgs/type_spec/emit_open_api.py +121 -34
  51. pkgs/type_spec/emit_open_api_util.py +5 -5
  52. pkgs/type_spec/emit_python.py +277 -86
  53. pkgs/type_spec/emit_typescript.py +102 -29
  54. pkgs/type_spec/emit_typescript_util.py +66 -10
  55. pkgs/type_spec/load_types.py +16 -3
  56. pkgs/type_spec/non_discriminated_union_exceptions.py +14 -0
  57. pkgs/type_spec/open_api_util.py +29 -4
  58. pkgs/type_spec/parts/base.py.prepart +11 -8
  59. pkgs/type_spec/parts/base.ts.prepart +4 -0
  60. pkgs/type_spec/type_info/__main__.py +3 -1
  61. pkgs/type_spec/type_info/emit_type_info.py +115 -22
  62. pkgs/type_spec/ui_entry_actions/__init__.py +4 -0
  63. pkgs/type_spec/ui_entry_actions/generate_ui_entry_actions.py +308 -0
  64. pkgs/type_spec/util.py +3 -3
  65. pkgs/type_spec/value_spec/__main__.py +26 -9
  66. pkgs/type_spec/value_spec/convert_type.py +18 -0
  67. pkgs/type_spec/value_spec/emit_python.py +13 -3
  68. pkgs/type_spec/value_spec/types.py +1 -1
  69. uncountable/core/async_batch.py +1 -1
  70. uncountable/core/client.py +133 -34
  71. uncountable/core/environment.py +3 -3
  72. uncountable/core/file_upload.py +39 -15
  73. uncountable/integration/cli.py +116 -23
  74. uncountable/integration/construct_client.py +3 -3
  75. uncountable/integration/executors/executors.py +12 -2
  76. uncountable/integration/executors/generic_upload_executor.py +66 -14
  77. uncountable/integration/http_server/__init__.py +5 -0
  78. uncountable/integration/http_server/types.py +69 -0
  79. uncountable/integration/job.py +192 -7
  80. uncountable/integration/queue_runner/command_server/__init__.py +4 -0
  81. uncountable/integration/queue_runner/command_server/command_client.py +65 -0
  82. uncountable/integration/queue_runner/command_server/command_server.py +83 -5
  83. uncountable/integration/queue_runner/command_server/constants.py +4 -0
  84. uncountable/integration/queue_runner/command_server/protocol/command_server.proto +36 -0
  85. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +28 -11
  86. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +77 -1
  87. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +135 -0
  88. uncountable/integration/queue_runner/command_server/types.py +25 -2
  89. uncountable/integration/queue_runner/datastore/datastore_sqlite.py +168 -11
  90. uncountable/integration/queue_runner/datastore/interface.py +10 -0
  91. uncountable/integration/queue_runner/datastore/model.py +8 -1
  92. uncountable/integration/queue_runner/job_scheduler.py +63 -23
  93. uncountable/integration/queue_runner/queue_runner.py +10 -2
  94. uncountable/integration/queue_runner/worker.py +22 -17
  95. uncountable/integration/scan_profiles.py +1 -1
  96. uncountable/integration/scheduler.py +74 -25
  97. uncountable/integration/secret_retrieval/retrieve_secret.py +1 -1
  98. uncountable/integration/server.py +42 -12
  99. uncountable/integration/telemetry.py +63 -10
  100. uncountable/integration/webhook_server/entrypoint.py +39 -112
  101. uncountable/types/__init__.py +58 -1
  102. uncountable/types/api/batch/execute_batch.py +5 -6
  103. uncountable/types/api/batch/execute_batch_load_async.py +2 -3
  104. uncountable/types/api/chemical/convert_chemical_formats.py +10 -5
  105. uncountable/types/api/condition_parameters/__init__.py +1 -0
  106. uncountable/types/api/condition_parameters/upsert_condition_match.py +72 -0
  107. uncountable/types/api/entity/create_entities.py +7 -7
  108. uncountable/types/api/entity/create_entity.py +8 -8
  109. uncountable/types/api/entity/create_or_update_entity.py +48 -0
  110. uncountable/types/api/entity/export_entities.py +59 -0
  111. uncountable/types/api/entity/get_entities_data.py +3 -4
  112. uncountable/types/api/entity/grant_entity_permissions.py +6 -6
  113. uncountable/types/api/entity/list_aggregate.py +79 -0
  114. uncountable/types/api/entity/list_entities.py +34 -10
  115. uncountable/types/api/entity/lock_entity.py +4 -4
  116. uncountable/types/api/entity/lookup_entity.py +116 -0
  117. uncountable/types/api/entity/resolve_entity_ids.py +5 -6
  118. uncountable/types/api/entity/set_entity_field_values.py +44 -0
  119. uncountable/types/api/entity/set_values.py +3 -3
  120. uncountable/types/api/entity/transition_entity_phase.py +14 -7
  121. uncountable/types/api/entity/unlock_entity.py +3 -3
  122. uncountable/types/api/equipment/associate_equipment_input.py +2 -3
  123. uncountable/types/api/field_options/upsert_field_options.py +7 -7
  124. uncountable/types/api/files/__init__.py +1 -0
  125. uncountable/types/api/files/download_file.py +77 -0
  126. uncountable/types/api/id_source/list_id_source.py +6 -7
  127. uncountable/types/api/id_source/match_id_source.py +4 -5
  128. uncountable/types/api/input_groups/get_input_group_names.py +3 -4
  129. uncountable/types/api/inputs/create_inputs.py +10 -9
  130. uncountable/types/api/inputs/get_input_data.py +11 -12
  131. uncountable/types/api/inputs/get_input_names.py +6 -7
  132. uncountable/types/api/inputs/get_inputs_data.py +6 -7
  133. uncountable/types/api/inputs/set_input_attribute_values.py +5 -6
  134. uncountable/types/api/inputs/set_input_category.py +5 -5
  135. uncountable/types/api/inputs/set_input_subcategories.py +3 -3
  136. uncountable/types/api/inputs/set_intermediate_type.py +4 -4
  137. uncountable/types/api/integrations/__init__.py +1 -0
  138. uncountable/types/api/integrations/publish_realtime_data.py +41 -0
  139. uncountable/types/api/integrations/push_notification.py +49 -0
  140. uncountable/types/api/integrations/register_sockets_token.py +41 -0
  141. uncountable/types/api/listing/__init__.py +1 -0
  142. uncountable/types/api/listing/fetch_listing.py +58 -0
  143. uncountable/types/api/material_families/update_entity_material_families.py +3 -4
  144. uncountable/types/api/notebooks/__init__.py +1 -0
  145. uncountable/types/api/notebooks/add_notebook_content.py +119 -0
  146. uncountable/types/api/outputs/get_output_data.py +12 -13
  147. uncountable/types/api/outputs/get_output_names.py +5 -6
  148. uncountable/types/api/outputs/get_output_organization.py +173 -0
  149. uncountable/types/api/outputs/resolve_output_conditions.py +7 -8
  150. uncountable/types/api/permissions/set_core_permissions.py +16 -10
  151. uncountable/types/api/project/get_projects.py +6 -7
  152. uncountable/types/api/project/get_projects_data.py +7 -8
  153. uncountable/types/api/recipe_links/create_recipe_link.py +5 -5
  154. uncountable/types/api/recipe_links/remove_recipe_link.py +4 -4
  155. uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +6 -7
  156. uncountable/types/api/recipes/add_recipe_to_project.py +3 -3
  157. uncountable/types/api/recipes/add_time_series_data.py +64 -0
  158. uncountable/types/api/recipes/archive_recipes.py +4 -4
  159. uncountable/types/api/recipes/associate_recipe_as_input.py +5 -5
  160. uncountable/types/api/recipes/associate_recipe_as_lot.py +3 -3
  161. uncountable/types/api/recipes/clear_recipe_outputs.py +3 -3
  162. uncountable/types/api/recipes/create_mix_order.py +44 -0
  163. uncountable/types/api/recipes/create_recipe.py +8 -9
  164. uncountable/types/api/recipes/create_recipes.py +8 -9
  165. uncountable/types/api/recipes/disassociate_recipe_as_input.py +3 -3
  166. uncountable/types/api/recipes/edit_recipe_inputs.py +101 -24
  167. uncountable/types/api/recipes/get_column_calculation_values.py +4 -5
  168. uncountable/types/api/recipes/get_curve.py +4 -5
  169. uncountable/types/api/recipes/get_recipe_calculations.py +6 -7
  170. uncountable/types/api/recipes/get_recipe_links.py +3 -4
  171. uncountable/types/api/recipes/get_recipe_names.py +3 -4
  172. uncountable/types/api/recipes/get_recipe_output_metadata.py +5 -6
  173. uncountable/types/api/recipes/get_recipes_data.py +62 -34
  174. uncountable/types/api/recipes/lock_recipes.py +9 -8
  175. uncountable/types/api/recipes/remove_recipe_from_project.py +3 -3
  176. uncountable/types/api/recipes/set_recipe_inputs.py +9 -10
  177. uncountable/types/api/recipes/set_recipe_metadata.py +3 -3
  178. uncountable/types/api/recipes/set_recipe_output_annotations.py +11 -12
  179. uncountable/types/api/recipes/set_recipe_output_file.py +5 -6
  180. uncountable/types/api/recipes/set_recipe_outputs.py +24 -13
  181. uncountable/types/api/recipes/set_recipe_tags.py +14 -9
  182. uncountable/types/api/recipes/set_recipe_total.py +59 -0
  183. uncountable/types/api/recipes/unarchive_recipes.py +3 -3
  184. uncountable/types/api/recipes/unlock_recipes.py +7 -6
  185. uncountable/types/api/runsheet/__init__.py +1 -0
  186. uncountable/types/api/runsheet/complete_async_upload.py +41 -0
  187. uncountable/types/api/triggers/run_trigger.py +4 -4
  188. uncountable/types/api/uploader/complete_async_parse.py +46 -0
  189. uncountable/types/api/uploader/invoke_uploader.py +4 -5
  190. uncountable/types/api/user/__init__.py +1 -0
  191. uncountable/types/api/user/get_current_user_info.py +40 -0
  192. uncountable/types/async_batch.py +1 -1
  193. uncountable/types/async_batch_processor.py +506 -23
  194. uncountable/types/async_batch_t.py +35 -8
  195. uncountable/types/async_jobs.py +0 -1
  196. uncountable/types/async_jobs_t.py +1 -2
  197. uncountable/types/auth_retrieval.py +0 -1
  198. uncountable/types/auth_retrieval_t.py +6 -6
  199. uncountable/types/base.py +0 -1
  200. uncountable/types/base_t.py +11 -9
  201. uncountable/types/calculations.py +0 -1
  202. uncountable/types/calculations_t.py +1 -2
  203. uncountable/types/chemical_structure.py +0 -1
  204. uncountable/types/chemical_structure_t.py +5 -5
  205. uncountable/types/client_base.py +614 -69
  206. uncountable/types/client_config.py +1 -1
  207. uncountable/types/client_config_t.py +13 -3
  208. uncountable/types/curves.py +0 -1
  209. uncountable/types/curves_t.py +6 -7
  210. uncountable/types/data.py +12 -0
  211. uncountable/types/data_t.py +103 -0
  212. uncountable/types/entity.py +1 -1
  213. uncountable/types/entity_t.py +90 -10
  214. uncountable/types/experiment_groups.py +0 -1
  215. uncountable/types/experiment_groups_t.py +1 -2
  216. uncountable/types/exports.py +8 -0
  217. uncountable/types/exports_t.py +34 -0
  218. uncountable/types/field_values.py +19 -1
  219. uncountable/types/field_values_t.py +242 -9
  220. uncountable/types/fields.py +0 -1
  221. uncountable/types/fields_t.py +1 -2
  222. uncountable/types/generic_upload.py +0 -1
  223. uncountable/types/generic_upload_t.py +14 -14
  224. uncountable/types/id_source.py +0 -1
  225. uncountable/types/id_source_t.py +13 -7
  226. uncountable/types/identifier.py +0 -1
  227. uncountable/types/identifier_t.py +10 -5
  228. uncountable/types/input_attributes.py +0 -1
  229. uncountable/types/input_attributes_t.py +3 -4
  230. uncountable/types/inputs.py +0 -1
  231. uncountable/types/inputs_t.py +3 -4
  232. uncountable/types/integration_server.py +0 -1
  233. uncountable/types/integration_server_t.py +13 -4
  234. uncountable/types/integration_session.py +10 -0
  235. uncountable/types/integration_session_t.py +60 -0
  236. uncountable/types/integrations.py +10 -0
  237. uncountable/types/integrations_t.py +62 -0
  238. uncountable/types/job_definition.py +2 -1
  239. uncountable/types/job_definition_t.py +57 -32
  240. uncountable/types/listing.py +9 -0
  241. uncountable/types/listing_t.py +51 -0
  242. uncountable/types/notices.py +8 -0
  243. uncountable/types/notices_t.py +37 -0
  244. uncountable/types/notifications.py +11 -0
  245. uncountable/types/notifications_t.py +74 -0
  246. uncountable/types/outputs.py +0 -1
  247. uncountable/types/outputs_t.py +2 -3
  248. uncountable/types/overrides.py +0 -1
  249. uncountable/types/overrides_t.py +10 -4
  250. uncountable/types/permissions.py +0 -1
  251. uncountable/types/permissions_t.py +1 -2
  252. uncountable/types/phases.py +0 -1
  253. uncountable/types/phases_t.py +1 -2
  254. uncountable/types/post_base.py +0 -1
  255. uncountable/types/post_base_t.py +1 -2
  256. uncountable/types/queued_job.py +2 -1
  257. uncountable/types/queued_job_t.py +29 -12
  258. uncountable/types/recipe_identifiers.py +0 -1
  259. uncountable/types/recipe_identifiers_t.py +18 -8
  260. uncountable/types/recipe_inputs.py +0 -1
  261. uncountable/types/recipe_inputs_t.py +1 -2
  262. uncountable/types/recipe_links.py +0 -1
  263. uncountable/types/recipe_links_t.py +3 -4
  264. uncountable/types/recipe_metadata.py +0 -1
  265. uncountable/types/recipe_metadata_t.py +9 -10
  266. uncountable/types/recipe_output_metadata.py +0 -1
  267. uncountable/types/recipe_output_metadata_t.py +1 -2
  268. uncountable/types/recipe_tags.py +0 -1
  269. uncountable/types/recipe_tags_t.py +1 -2
  270. uncountable/types/recipe_workflow_steps.py +0 -1
  271. uncountable/types/recipe_workflow_steps_t.py +7 -7
  272. uncountable/types/recipes.py +0 -1
  273. uncountable/types/recipes_t.py +2 -2
  274. uncountable/types/response.py +0 -1
  275. uncountable/types/response_t.py +2 -2
  276. uncountable/types/secret_retrieval.py +0 -1
  277. uncountable/types/secret_retrieval_t.py +7 -7
  278. uncountable/types/sockets.py +20 -0
  279. uncountable/types/sockets_t.py +169 -0
  280. uncountable/types/structured_filters.py +25 -0
  281. uncountable/types/structured_filters_t.py +248 -0
  282. uncountable/types/units.py +0 -1
  283. uncountable/types/units_t.py +1 -2
  284. uncountable/types/uploader.py +24 -0
  285. uncountable/types/uploader_t.py +222 -0
  286. uncountable/types/users.py +0 -1
  287. uncountable/types/users_t.py +1 -2
  288. uncountable/types/webhook_job.py +1 -1
  289. uncountable/types/webhook_job_t.py +14 -3
  290. uncountable/types/workflows.py +0 -1
  291. uncountable/types/workflows_t.py +3 -4
  292. uncountablepythonsdk-0.0.132.dist-info/METADATA +64 -0
  293. uncountablepythonsdk-0.0.132.dist-info/RECORD +363 -0
  294. {UncountablePythonSDK-0.0.82.dist-info → uncountablepythonsdk-0.0.132.dist-info}/WHEEL +1 -1
  295. UncountablePythonSDK-0.0.82.dist-info/METADATA +0 -60
  296. UncountablePythonSDK-0.0.82.dist-info/RECORD +0 -292
  297. docs/quickstart.md +0 -19
  298. {UncountablePythonSDK-0.0.82.dist-info → uncountablepythonsdk-0.0.132.dist-info}/top_level.txt +0 -0
pkgs/type_spec/builder.py CHANGED
@@ -10,12 +10,26 @@ 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
- from .util import parse_type_str, unused
16
+ from .builder_types import CrossOutputPaths
17
+ from .non_discriminated_union_exceptions import NON_DISCRIMINATED_UNION_EXCEPTIONS
18
+ from .util import parse_type_str
17
19
 
18
20
  RawDict = dict[Any, Any]
21
+ EndpointKey = str
22
+
23
+
24
+ class PathMapping(StrEnum):
25
+ NO_MAPPING = "no_mapping"
26
+ DEFAULT_MAPPING = "default_mapping"
27
+
28
+
29
+ @dataclass(kw_only=True)
30
+ class APIEndpointInfo:
31
+ root_path: str
32
+ path_mapping: PathMapping
19
33
 
20
34
 
21
35
  class StabilityLevel(StrEnum):
@@ -24,7 +38,6 @@ class StabilityLevel(StrEnum):
24
38
  """
25
39
 
26
40
  draft = "draft"
27
- alpha = "alpha"
28
41
  beta = "beta"
29
42
  stable = "stable"
30
43
 
@@ -47,7 +60,7 @@ class PropertyConvertValue(StrEnum):
47
60
  @dataclass
48
61
  class SpecProperty:
49
62
  name: str
50
- label: Optional[str]
63
+ label: str | None
51
64
  spec_type: SpecType
52
65
  extant: PropertyExtant
53
66
  convert_value: PropertyConvertValue
@@ -61,6 +74,7 @@ class SpecProperty:
61
74
  # Holds extra information that will be emitted along with type_info. The builder knows nothing
62
75
  # about the contents of this information.
63
76
  ext_info: Any = None
77
+ explicit_default: bool = False
64
78
 
65
79
 
66
80
  class NameCase(StrEnum):
@@ -255,7 +269,7 @@ class SpecTypeLiteralWrapper(SpecType):
255
269
  return [self.value_type]
256
270
 
257
271
 
258
- def unwrap_literal_type(stype: SpecType) -> Optional[SpecTypeLiteralWrapper]:
272
+ def unwrap_literal_type(stype: SpecType) -> SpecTypeLiteralWrapper | None:
259
273
  if isinstance(stype, SpecTypeInstance) and stype.defn_type.is_base_type(
260
274
  BaseTypeName.s_literal
261
275
  ):
@@ -283,7 +297,7 @@ class SpecTypeDefn(SpecType):
283
297
  ) -> None:
284
298
  self.namespace = namespace
285
299
  self.name = name
286
- self.label: Optional[str] = None
300
+ self.label: str | None = None
287
301
 
288
302
  self.is_predefined = is_predefined
289
303
  self.name_case = NameCase.convert
@@ -293,6 +307,8 @@ class SpecTypeDefn(SpecType):
293
307
  self._is_value_converted = _is_value_converted
294
308
  self._is_value_to_string = False
295
309
  self._is_valid_parameter = True
310
+ self._is_dynamic_allowed = False
311
+ self._default_extant: PropertyExtant | None = None
296
312
  self.ext_info: Any = None
297
313
 
298
314
  def is_value_converted(self) -> bool:
@@ -304,6 +320,9 @@ class SpecTypeDefn(SpecType):
304
320
  def is_valid_parameter(self) -> bool:
305
321
  return self._is_valid_parameter
306
322
 
323
+ def is_dynamic_allowed(self) -> bool:
324
+ return self._is_dynamic_allowed
325
+
307
326
  def is_base_type(self, type_: BaseTypeName) -> bool:
308
327
  return self.is_base and self.name == type_
309
328
 
@@ -316,11 +335,28 @@ class SpecTypeDefn(SpecType):
316
335
  def base_process(
317
336
  self, builder: SpecBuilder, data: RawDict, extra_names: list[str]
318
337
  ) -> None:
319
- util.check_fields(data, ["ext_info", "label"] + extra_names)
338
+ util.check_fields(
339
+ data,
340
+ [
341
+ "ext_info",
342
+ "label",
343
+ "is_dynamic_allowed",
344
+ "default_extant",
345
+ ]
346
+ + extra_names,
347
+ )
320
348
 
321
349
  self.ext_info = data.get("ext_info")
322
350
  self.label = data.get("label")
323
351
 
352
+ is_dynamic_allowed = data.get("is_dynamic_allowed", False)
353
+ assert isinstance(is_dynamic_allowed, bool)
354
+ self._is_dynamic_allowed = is_dynamic_allowed
355
+
356
+ default_extant = data.get("default_extant")
357
+ if default_extant is not None:
358
+ self._default_extant = PropertyExtant(default_extant)
359
+
324
360
  def _process_property(
325
361
  self, builder: SpecBuilder, spec_name: str, data: RawDict
326
362
  ) -> SpecProperty:
@@ -339,18 +375,18 @@ class SpecTypeDefn(SpecType):
339
375
  ],
340
376
  )
341
377
  try:
342
- extant_type = data.get("extant")
378
+ extant_type_str = data.get("extant")
379
+ extant_type = (
380
+ PropertyExtant(extant_type_str) if extant_type_str is not None else None
381
+ )
382
+ extant = extant_type or self._default_extant
343
383
  if spec_name.endswith("?"):
344
- if extant_type is not None:
384
+ if extant is not None:
345
385
  raise Exception("cannot specify extant with ?")
346
386
  extant = PropertyExtant.optional
347
387
  name = spec_name[:-1]
348
388
  else:
349
- extant = (
350
- PropertyExtant.required
351
- if extant_type is None
352
- else PropertyExtant(extant_type)
353
- )
389
+ extant = extant or PropertyExtant.required
354
390
  name = spec_name
355
391
 
356
392
  property_name_case = self.name_case
@@ -372,6 +408,7 @@ class SpecTypeDefn(SpecType):
372
408
  ptype = builder.parse_type(self.namespace, data_type, scope=self)
373
409
 
374
410
  default_spec = data.get("default", MISSING)
411
+ explicit_default = default_spec != MISSING
375
412
  if default_spec == MISSING:
376
413
  has_default = False
377
414
  default = None
@@ -379,7 +416,10 @@ class SpecTypeDefn(SpecType):
379
416
  has_default = True
380
417
  # IMPROVE: check the type against the ptype
381
418
  default = default_spec
382
-
419
+ if extant == PropertyExtant.missing and explicit_default:
420
+ raise Exception(
421
+ f"cannot have extant missing and default for property {name}"
422
+ )
383
423
  parse_require = False
384
424
  literal = unwrap_literal_type(ptype)
385
425
  if literal is not None:
@@ -402,6 +442,7 @@ class SpecTypeDefn(SpecType):
402
442
  parse_require=parse_require,
403
443
  desc=data.get("desc", None),
404
444
  ext_info=ext_info,
445
+ explicit_default=explicit_default,
405
446
  )
406
447
  finally:
407
448
  builder.pop_where()
@@ -439,7 +480,7 @@ class SpecTypeGenericParameter(SpecType):
439
480
 
440
481
 
441
482
  class SpecTypeDefnObject(SpecTypeDefn):
442
- base: Optional[SpecTypeDefnObject]
483
+ base: SpecTypeDefnObject | None
443
484
  parameters: list[str]
444
485
 
445
486
  def __init__(
@@ -447,7 +488,7 @@ class SpecTypeDefnObject(SpecTypeDefn):
447
488
  namespace: SpecNamespace,
448
489
  name: str,
449
490
  *,
450
- parameters: Optional[list[str]] = None,
491
+ parameters: list[str] | None = None,
451
492
  is_base: bool = False,
452
493
  is_predefined: bool = False,
453
494
  is_hashable: bool = False,
@@ -464,7 +505,7 @@ class SpecTypeDefnObject(SpecTypeDefn):
464
505
  self.parameters = parameters if parameters is not None else []
465
506
  self.is_hashable = is_hashable
466
507
  self.base = None
467
- self.properties: Optional[dict[str, SpecProperty]] = None
508
+ self.properties: dict[str, SpecProperty] | None = None
468
509
  self._kw_only: bool = True
469
510
  self.desc: str | None = None
470
511
 
@@ -601,13 +642,23 @@ class SpecTypeDefnUnion(SpecTypeDefn):
601
642
  prop_type = unwrap_literal_type(discriminator_type.spec_type)
602
643
  assert prop_type is not None
603
644
  assert prop_type.is_value_to_string()
604
- discriminant = str(prop_type.value)
645
+ value_type = prop_type.value_type
646
+ if isinstance(value_type, SpecTypeDefnStringEnum):
647
+ assert isinstance(prop_type.value, str)
648
+ discriminant = value_type.values[prop_type.value].value
649
+ else:
650
+ discriminant = str(prop_type.value)
605
651
  assert discriminant not in self.discriminator_map, (
606
652
  f"duplicated-discriminant, {discriminant} in {sub_type}"
607
653
  )
608
654
  self.discriminator_map[discriminant] = sub_type
609
655
 
610
656
  builder.pop_where()
657
+ elif (
658
+ f"{self.namespace.name}.{self.name}"
659
+ not in NON_DISCRIMINATED_UNION_EXCEPTIONS
660
+ ):
661
+ raise Exception(f"union requires a discriminator: {self.name}")
611
662
 
612
663
  def get_referenced_types(self) -> list[SpecType]:
613
664
  return self.types
@@ -646,7 +697,7 @@ class SpecTypeDefnExternal(SpecTypeDefn):
646
697
  class StringEnumEntry:
647
698
  name: str
648
699
  value: str
649
- label: Optional[str] = None
700
+ label: str | None = None
650
701
  deprecated: bool = False
651
702
 
652
703
 
@@ -662,7 +713,7 @@ class SpecTypeDefnStringEnum(SpecTypeDefn):
662
713
  )
663
714
  self.values: dict[str, StringEnumEntry] = {}
664
715
  self.desc: str | None = None
665
- self.sql_type_name: Optional[str] = None
716
+ self.sql_type_name: str | None = None
666
717
  self.emit_id_source = False
667
718
  self.source_enums: list[SpecType] = []
668
719
 
@@ -700,6 +751,8 @@ class SpecTypeDefnStringEnum(SpecTypeDefn):
700
751
  builder.ensure(
701
752
  isinstance(enum_value, str), "enum value should be string"
702
753
  )
754
+ assert isinstance(enum_value, str)
755
+
703
756
  deprecated = value.get("deprecated", False)
704
757
  builder.ensure(
705
758
  isinstance(deprecated, bool),
@@ -770,6 +823,7 @@ class SpecTypeDefnStringEnum(SpecTypeDefn):
770
823
  TOKEN_ENDPOINT = "$endpoint"
771
824
  TOKEN_EMIT_IO_TS = "$emit_io_ts"
772
825
  TOKEN_EMIT_TYPE_INFO = "$emit_type_info"
826
+ TOKEN_EMIT_TYPE_INFO_PYTHON = "$emit_type_info_python"
773
827
  # The import token is only for explicit ordering of the files, to process constants
774
828
  # and enums correctly. It does not impact the final generation of files, or the
775
829
  # language imports. Those are still auto-resolved.
@@ -794,13 +848,13 @@ RE_ENDPOINT_ROOT = re.compile(r"\${([_a-z]+)}")
794
848
 
795
849
  @dataclass(kw_only=True, frozen=True)
796
850
  class _EndpointPathDetails:
797
- root: str
851
+ root: EndpointKey
798
852
  root_path: str
799
853
  resolved_path: str
800
854
 
801
855
 
802
856
  def _resolve_endpoint_path(
803
- path: str, api_endpoints: dict[str, str]
857
+ path: str, api_endpoints: dict[EndpointKey, APIEndpointInfo]
804
858
  ) -> _EndpointPathDetails:
805
859
  root_path_source = path.split("/")[0]
806
860
  root_match = RE_ENDPOINT_ROOT.fullmatch(root_path_source)
@@ -808,7 +862,7 @@ def _resolve_endpoint_path(
808
862
  raise Exception(f"invalid-api-path-root:{root_path_source}")
809
863
 
810
864
  root_var = root_match.group(1)
811
- root_path = api_endpoints[root_var]
865
+ root_path = api_endpoints[root_var].root_path
812
866
 
813
867
  _, *rest_path = path.split("/", 1)
814
868
  resolved_path = "/".join([root_path] + rest_path)
@@ -818,19 +872,65 @@ def _resolve_endpoint_path(
818
872
  )
819
873
 
820
874
 
821
- class SpecEndpoint:
822
- method: RouteMethod
823
- root: str
875
+ class EndpointEmitType(StrEnum):
876
+ EMIT_ENDPOINT = "emit_endpoint"
877
+ EMIT_TYPES = "emit_types"
878
+ EMIT_NOTHING = "emit_nothing"
879
+
880
+
881
+ @dataclass(kw_only=True, frozen=True)
882
+ class EndpointSpecificPath:
883
+ root: EndpointKey
824
884
  path_root: str
825
885
  path_dirname: str
826
886
  path_basename: str
887
+ function: str | None
888
+
889
+
890
+ def parse_endpoint_specific_path(
891
+ builder: SpecBuilder,
892
+ data_per_endpoint: RawDict | None,
893
+ ) -> EndpointSpecificPath | None:
894
+ if data_per_endpoint is None:
895
+ return None
896
+ util.check_fields(
897
+ data_per_endpoint,
898
+ [
899
+ "path",
900
+ "function",
901
+ ],
902
+ )
903
+
904
+ if "path" not in data_per_endpoint or data_per_endpoint["path"] is None:
905
+ return None
906
+
907
+ path = data_per_endpoint["path"].split("/")
908
+
909
+ assert len(path) > 1, "invalid-endpoint-path"
910
+
911
+ path_details = _resolve_endpoint_path(
912
+ data_per_endpoint["path"], builder.api_endpoints
913
+ )
914
+
915
+ result = EndpointSpecificPath(
916
+ function=data_per_endpoint.get("function"),
917
+ path_dirname="/".join(path[1:-1]),
918
+ path_basename=path[-1],
919
+ root=path_details.root,
920
+ path_root=path_details.root_path,
921
+ )
922
+
923
+ return result
924
+
925
+
926
+ class SpecEndpoint:
927
+ method: RouteMethod
827
928
  data_loader: bool
828
- is_sdk: bool
829
- is_beta: bool
929
+ is_sdk: EndpointEmitType
830
930
  stability_level: StabilityLevel | None
831
931
  # Don't emit TypeScript endpoint code
832
932
  suppress_ts: bool
833
- function: Optional[str]
933
+ deprecated: bool = False
834
934
  async_batch_path: str | None = None
835
935
  result_type: ResultType = ResultType.json
836
936
  has_attachment: bool = False
@@ -838,21 +938,24 @@ class SpecEndpoint:
838
938
  account_type: str | None
839
939
  route_group: str | None
840
940
 
941
+ # function, path details per api endpoint
942
+ path_per_api_endpoint: dict[str, EndpointSpecificPath]
943
+ default_endpoint_key: EndpointKey
944
+
841
945
  is_external: bool = False
842
946
 
843
947
  def __init__(self) -> None:
844
948
  pass
845
949
 
846
950
  def process(self, builder: SpecBuilder, data: RawDict) -> None:
847
- unused(builder)
848
951
  util.check_fields(
849
952
  data,
850
953
  [
851
954
  "method",
852
955
  "path",
853
956
  "data_loader",
957
+ "deprecated",
854
958
  "is_sdk",
855
- "is_beta",
856
959
  "stability_level",
857
960
  "async_batch_path",
858
961
  "function",
@@ -863,24 +966,32 @@ class SpecEndpoint:
863
966
  "has_attachment",
864
967
  "account_type",
865
968
  "route_group",
866
- ],
969
+ ]
970
+ + list(builder.api_endpoints.keys()),
867
971
  )
868
972
  self.method = RouteMethod(data["method"])
869
973
 
870
- path = data["path"].split("/")
871
-
872
- assert len(path) > 1, "invalid-endpoint-path"
873
-
874
- # handle ${external} in the same way we handle ${materials} for now
875
- self.path_dirname = "/".join(path[1:-1])
876
- self.path_basename = path[-1]
877
-
878
974
  data_loader = data.get("data_loader", False)
879
975
  assert isinstance(data_loader, bool)
880
976
  self.data_loader = data_loader
977
+ self.deprecated = data.get("deprecated", False)
978
+
979
+ is_sdk = data.get("is_sdk", EndpointEmitType.EMIT_NOTHING)
980
+
981
+ # backwards compatibility
982
+ if isinstance(is_sdk, bool):
983
+ if is_sdk is True:
984
+ is_sdk = EndpointEmitType.EMIT_ENDPOINT
985
+ else:
986
+ is_sdk = EndpointEmitType.EMIT_NOTHING
987
+ elif isinstance(is_sdk, str):
988
+ try:
989
+ is_sdk = EndpointEmitType(is_sdk)
990
+ except ValueError as e:
991
+ raise ValueError(f"Invalid value for is_sdk: {is_sdk}") from e
992
+
993
+ assert isinstance(is_sdk, EndpointEmitType)
881
994
 
882
- is_sdk = data.get("is_sdk", False)
883
- assert isinstance(is_sdk, bool)
884
995
  self.is_sdk = is_sdk
885
996
 
886
997
  route_group = data.get("route_group")
@@ -891,10 +1002,6 @@ class SpecEndpoint:
891
1002
  assert account_type is None or isinstance(account_type, str)
892
1003
  self.account_type = account_type
893
1004
 
894
- is_beta = data.get("is_beta", False)
895
- assert isinstance(is_beta, bool)
896
- self.is_beta = is_beta
897
-
898
1005
  stability_level_raw = data.get("stability_level")
899
1006
  assert stability_level_raw is None or isinstance(stability_level_raw, str)
900
1007
  self.stability_level = (
@@ -908,29 +1015,70 @@ class SpecEndpoint:
908
1015
  assert isinstance(async_batch_path, str)
909
1016
  self.async_batch_path = async_batch_path
910
1017
 
911
- self.function = data.get("function")
912
-
913
1018
  suppress_ts = data.get("suppress_ts", False)
914
1019
  assert isinstance(suppress_ts, bool)
915
1020
  self.suppress_ts = suppress_ts
916
1021
 
917
1022
  self.result_type = ResultType(data.get("result_type", ResultType.json.value))
918
-
919
- path_details = _resolve_endpoint_path(data["path"], builder.api_endpoints)
920
- self.root = path_details.root
921
- self.path_root = path_details.root_path
1023
+ self.has_attachment = data.get("has_attachment", False)
922
1024
  self.desc = data.get("desc")
1025
+
1026
+ # compatibility with single-endpoint files
1027
+ default_endpoint_path = parse_endpoint_specific_path(
1028
+ builder,
1029
+ {"path": data.get("path"), "function": data.get("function")},
1030
+ )
1031
+ if default_endpoint_path is not None:
1032
+ assert default_endpoint_path.root in builder.api_endpoints, (
1033
+ "Default endpoint is not a valid API endpoint"
1034
+ )
1035
+ self.default_endpoint_key = default_endpoint_path.root
1036
+ self.path_per_api_endpoint = {
1037
+ self.default_endpoint_key: default_endpoint_path,
1038
+ }
1039
+ else:
1040
+ self.path_per_api_endpoint = {}
1041
+ shared_function_name = None
1042
+ for endpoint_key in builder.api_endpoints:
1043
+ endpoint_specific_path = parse_endpoint_specific_path(
1044
+ builder,
1045
+ data.get(endpoint_key),
1046
+ )
1047
+ if endpoint_specific_path is not None:
1048
+ self.path_per_api_endpoint[endpoint_key] = endpoint_specific_path
1049
+ if endpoint_specific_path.function is not None:
1050
+ fn_name = endpoint_specific_path.function.split(".")[-1]
1051
+ if shared_function_name is None:
1052
+ shared_function_name = fn_name
1053
+ assert shared_function_name == fn_name
1054
+
1055
+ if builder.top_namespace in self.path_per_api_endpoint:
1056
+ self.default_endpoint_key = builder.top_namespace
1057
+ elif len(self.path_per_api_endpoint) == 1:
1058
+ self.default_endpoint_key = next(
1059
+ iter(self.path_per_api_endpoint.keys())
1060
+ )
1061
+ else:
1062
+ raise RuntimeError("no clear default endpoint")
1063
+
1064
+ assert len(self.path_per_api_endpoint) > 0, (
1065
+ "Missing API endpoint path and function definitions for API call"
1066
+ )
1067
+
923
1068
  # IMPROVE: remove need for is_external flag
924
- self.is_external = self.path_root == "api/external"
925
- self.has_attachment = data.get("has_attachment", False)
1069
+ self.is_external = (
1070
+ self.path_per_api_endpoint[self.default_endpoint_key].path_root
1071
+ == "api/external"
1072
+ )
926
1073
 
927
- assert not is_sdk or self.desc is not None, (
928
- f"Endpoint description required for SDK endpoints, missing: {path}"
1074
+ assert self.is_sdk != EndpointEmitType.EMIT_ENDPOINT or self.desc is not None, (
1075
+ f"Endpoint description required for SDK endpoints, missing: {self.resolved_path}"
929
1076
  )
930
1077
 
931
1078
  @property
932
1079
  def resolved_path(self: Self) -> str:
933
- return f"{self.path_root}/{self.path_dirname}/{self.path_basename}"
1080
+ default_endpoint_path = self.path_per_api_endpoint[self.default_endpoint_key]
1081
+ return f"{default_endpoint_path.path_root}/{default_endpoint_path.path_dirname}/{default_endpoint_path.path_basename}"
934
1082
 
935
1083
 
936
1084
  def _parse_const(
@@ -953,7 +1101,7 @@ def _parse_const(
953
1101
  elif const_type.defn_type.name == BaseTypeName.s_dict:
954
1102
  assert isinstance(value, dict)
955
1103
  builder.ensure(
956
- len(const_type.parameters) == 2, "constant-dict-expects-one-type"
1104
+ len(const_type.parameters) == 2, "constant-dict-expects-two-types"
957
1105
  )
958
1106
  key_type = const_type.parameters[0]
959
1107
  value_type = const_type.parameters[1]
@@ -1002,6 +1150,11 @@ def _parse_const(
1002
1150
  )
1003
1151
  return value
1004
1152
 
1153
+ if not const_type.is_base:
1154
+ # IMPROVE: validate the object type properties before emission stage
1155
+ builder.ensure(isinstance(value, dict), "invalid value for object constant")
1156
+ return value
1157
+
1005
1158
  raise Exception("unsupported-const-scalar-type", const_type)
1006
1159
 
1007
1160
 
@@ -1043,14 +1196,15 @@ class SpecNamespace:
1043
1196
  ):
1044
1197
  self.types: dict[str, SpecTypeDefn] = {}
1045
1198
  self.constants: dict[str, SpecConstant] = {}
1046
- self.endpoint: Optional[SpecEndpoint] = None
1199
+ self.endpoint: SpecEndpoint | None = None
1047
1200
  self.emit_io_ts = False
1048
1201
  self.emit_type_info = False
1202
+ self.emit_type_info_python = False
1049
1203
  self.derive_types_from_io_ts = False
1050
- self._imports: Optional[list[str]] = None
1204
+ self._imports: list[str] | None = None
1051
1205
  self.path = name.split(".")
1052
1206
  self.name = self.path[-1]
1053
- self._order: Optional[int] = None
1207
+ self._order: int | None = None
1054
1208
 
1055
1209
  def _update_order(self, builder: SpecBuilder, recurse: int = 0) -> int:
1056
1210
  if self._order is not None:
@@ -1100,6 +1254,11 @@ class SpecNamespace:
1100
1254
  self.emit_type_info = defn
1101
1255
  continue
1102
1256
 
1257
+ if name == TOKEN_EMIT_TYPE_INFO_PYTHON:
1258
+ assert defn in (True, False)
1259
+ self.emit_type_info_python = defn
1260
+ continue
1261
+
1103
1262
  if name == TOKEN_IMPORT:
1104
1263
  assert self._imports is None
1105
1264
  imports = [defn] if isinstance(defn, str) else defn
@@ -1117,7 +1276,8 @@ class SpecNamespace:
1117
1276
 
1118
1277
  assert util.is_valid_type_name(name), f"{name} is not a valid type name"
1119
1278
  assert name not in self.types, f"{name} is duplicate"
1120
- defn_type = defn["type"]
1279
+ defn_type = defn.get("type")
1280
+ assert isinstance(defn_type, str), f"{name} requires a string type"
1121
1281
  spec_type: SpecTypeDefn
1122
1282
  if defn_type == DefnTypeName.s_alias:
1123
1283
  spec_type = SpecTypeDefnAlias(self, name)
@@ -1161,7 +1321,12 @@ class SpecNamespace:
1161
1321
  parsed_name = parse_type_str(full_name)[0]
1162
1322
  name = parsed_name.name
1163
1323
 
1164
- if name in [TOKEN_EMIT_IO_TS, TOKEN_EMIT_TYPE_INFO, TOKEN_IMPORT]:
1324
+ if name in [
1325
+ TOKEN_EMIT_IO_TS,
1326
+ TOKEN_EMIT_TYPE_INFO,
1327
+ TOKEN_IMPORT,
1328
+ TOKEN_EMIT_TYPE_INFO_PYTHON,
1329
+ ]:
1165
1330
  continue
1166
1331
 
1167
1332
  builder.push_where(name)
@@ -1205,7 +1370,13 @@ class NameDataPair:
1205
1370
 
1206
1371
 
1207
1372
  class SpecBuilder:
1208
- def __init__(self, *, api_endpoints: dict[str, str], top_namespace: str) -> None:
1373
+ def __init__(
1374
+ self,
1375
+ *,
1376
+ api_endpoints: dict[EndpointKey, APIEndpointInfo],
1377
+ top_namespace: str,
1378
+ cross_output_paths: CrossOutputPaths | None,
1379
+ ) -> None:
1209
1380
  self.top_namespace = top_namespace
1210
1381
  self.where: list[str] = []
1211
1382
  self.namespaces = {}
@@ -1215,6 +1386,7 @@ class SpecBuilder:
1215
1386
  self.examples: dict[str, list[SpecEndpointExample]] = defaultdict(list)
1216
1387
  self.guides: dict[SpecGuideKey, list[SpecGuide]] = defaultdict(list)
1217
1388
  self.api_endpoints = api_endpoints
1389
+ self.cross_output_paths = cross_output_paths
1218
1390
  base_namespace = SpecNamespace(name=base_namespace_name)
1219
1391
  for base_type in BaseTypeName:
1220
1392
  defn = SpecTypeDefnObject(base_namespace, base_type, is_base=True)
@@ -1232,9 +1404,13 @@ class SpecBuilder:
1232
1404
  self.emit_id_source_enums: set[SpecTypeDefnStringEnum] = set()
1233
1405
 
1234
1406
  this_dir = os.path.dirname(os.path.realpath(__file__))
1235
- with open(f"{this_dir}/parts/base.py.prepart") as py_base_part:
1407
+ with open(
1408
+ f"{this_dir}/parts/base.py.prepart", encoding="utf-8"
1409
+ ) as py_base_part:
1236
1410
  self.preparts["python"][base_namespace_name] = py_base_part.read()
1237
- with open(f"{this_dir}/parts/base.ts.prepart") as ts_base_part:
1411
+ with open(
1412
+ f"{this_dir}/parts/base.ts.prepart", encoding="utf-8"
1413
+ ) as ts_base_part:
1238
1414
  self.preparts["typescript"][base_namespace_name] = ts_base_part.read()
1239
1415
 
1240
1416
  base_namespace.types["ObjectId"] = SpecTypeDefnObject(
@@ -1303,7 +1479,7 @@ class SpecBuilder:
1303
1479
  self,
1304
1480
  path: util.ParsedTypePath,
1305
1481
  namespace: SpecNamespace,
1306
- scope: Optional[SpecTypeDefn] = None,
1482
+ scope: SpecTypeDefn | None = None,
1307
1483
  top: bool = False,
1308
1484
  ) -> SpecType:
1309
1485
  """
@@ -1361,8 +1537,10 @@ class SpecBuilder:
1361
1537
  if len(path) == 2:
1362
1538
  if isinstance(defn_type, SpecTypeDefnStringEnum):
1363
1539
  assert path[1].parameters is None
1540
+ statement = f"$import: [{defn_type.namespace.name}]"
1364
1541
  self.ensure(
1365
- path[1].name in defn_type.values, f"missing-enum-value: {path}"
1542
+ path[1].name in defn_type.values,
1543
+ f"missing-enum-value: {path} have you specified the dependency in an import statement: {statement}",
1366
1544
  )
1367
1545
  return SpecTypeLiteralWrapper(
1368
1546
  value=path[1].name,
@@ -1384,7 +1562,7 @@ class SpecBuilder:
1384
1562
  )
1385
1563
 
1386
1564
  def parse_type(
1387
- self, namespace: SpecNamespace, spec: str, scope: Optional[SpecTypeDefn] = None
1565
+ self, namespace: SpecNamespace, spec: str, scope: SpecTypeDefn | None = None
1388
1566
  ) -> SpecType:
1389
1567
  self.push_where(spec)
1390
1568
  parsed_type = util.parse_type_str(spec)
@@ -0,0 +1,9 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(kw_only=True, frozen=True)
5
+ class CrossOutputPaths:
6
+ python_types_output: str
7
+ typescript_types_output: str
8
+ typescript_routes_output_by_endpoint: dict[str, str]
9
+ typespec_files_input: list[str]