UncountablePythonSDK 0.0.83__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 +3 -5
  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.83.dist-info → uncountablepythonsdk-0.0.132.dist-info}/WHEEL +1 -1
  295. UncountablePythonSDK-0.0.83.dist-info/METADATA +0 -60
  296. UncountablePythonSDK-0.0.83.dist-info/RECORD +0 -292
  297. docs/quickstart.md +0 -19
  298. {UncountablePythonSDK-0.0.83.dist-info → uncountablepythonsdk-0.0.132.dist-info}/top_level.txt +0 -0
pkgs/type_spec/config.py CHANGED
@@ -4,6 +4,7 @@ from dataclasses import dataclass
4
4
  from typing import Self, TypeVar
5
5
 
6
6
  from pkgs.serialization import yaml
7
+ from pkgs.type_spec.builder import APIEndpointInfo, EndpointKey
7
8
 
8
9
  ConfigValueType = str | None | Mapping[str, str | None] | list[str]
9
10
 
@@ -19,6 +20,22 @@ def _parse_string_lookup(
19
20
  }
20
21
 
21
22
 
23
+ VT = TypeVar("VT")
24
+
25
+
26
+ def _parse_data_lookup(
27
+ key: str,
28
+ raw_value: ConfigValueType,
29
+ conv_func: type[VT],
30
+ ) -> dict[str, VT]:
31
+ assert isinstance(raw_value, dict), f"{key} must be key/values"
32
+ return {
33
+ k: conv_func(**v)
34
+ for k, v in raw_value.items()
35
+ if v is not None and isinstance(v, dict)
36
+ }
37
+
38
+
22
39
  @dataclass(kw_only=True)
23
40
  class BaseLanguageConfig:
24
41
  types_output: (
@@ -31,18 +48,28 @@ class BaseLanguageConfig:
31
48
 
32
49
  @dataclass(kw_only=True)
33
50
  class TypeScriptConfig(BaseLanguageConfig):
34
- routes_output: str # folder for generate route files will be located.
51
+ endpoint_to_routes_output: dict[
52
+ EndpointKey, str
53
+ ] # folder for generate route files will be located.
35
54
  type_info_output: str # folder for generated type info files
36
55
  id_source_output: str | None = None # folder for emitted id source maps.
56
+ endpoint_to_frontend_app_type: dict[
57
+ str, str
58
+ ] # map from api_endpoint to frontend app type
37
59
 
38
60
  def __post_init__(self: Self) -> None:
39
- self.routes_output = self.routes_output
61
+ self.endpoint_to_routes_output = self.endpoint_to_routes_output
40
62
  self.type_info_output = os.path.abspath(self.type_info_output)
41
63
  self.id_source_output = (
42
64
  os.path.abspath(self.id_source_output)
43
65
  if self.id_source_output is not None
44
66
  else None
45
67
  )
68
+ self.endpoint_to_frontend_app_type = _parse_string_lookup(
69
+ "typescript_endpoint_to_frontend_app_type",
70
+ self.endpoint_to_frontend_app_type,
71
+ lambda x: x,
72
+ )
46
73
 
47
74
 
48
75
  @dataclass(kw_only=True)
@@ -59,6 +86,7 @@ class PythonConfig(BaseLanguageConfig):
59
86
  emit_client_class: bool = False # emit the base class for the api client
60
87
  all_named_type_exports: bool = False # emit __all__ for all named type exports
61
88
  sdk_endpoints_only: bool = False # only emit is_sdk endpoints
89
+ type_info_output: str | None = None # folder for generated type info files
62
90
 
63
91
  def __post_init__(self: Self) -> None:
64
92
  self.routes_output = _parse_string_lookup(
@@ -70,6 +98,9 @@ class PythonConfig(BaseLanguageConfig):
70
98
  else None
71
99
  )
72
100
 
101
+ if self.type_info_output is not None:
102
+ self.type_info_output = os.path.abspath(self.type_info_output)
103
+
73
104
 
74
105
  @dataclass(kw_only=True)
75
106
  class OpenAPIConfig(BaseLanguageConfig):
@@ -88,17 +119,17 @@ class OpenAPIConfig(BaseLanguageConfig):
88
119
  class Config:
89
120
  top_namespace: str
90
121
  type_spec_types: list[str] # folders containing the yaml type spec definitions
91
- api_endpoint: dict[str, str]
122
+ api_endpoint: dict[str, APIEndpointInfo]
92
123
  # languages
93
124
  typescript: TypeScriptConfig | None
94
125
  python: PythonConfig
95
126
  open_api: OpenAPIConfig | None
96
127
 
97
128
 
98
- _T = TypeVar("_T")
129
+ T = TypeVar("T")
99
130
 
100
131
 
101
- def _parse_language(config_class: type[_T], raw_value: ConfigValueType) -> _T:
132
+ def _parse_language(config_class: type[T], raw_value: ConfigValueType) -> T:
102
133
  assert isinstance(raw_value, dict), "expecting language config to have key/values."
103
134
  return config_class(**raw_value)
104
135
 
@@ -113,8 +144,10 @@ def parse_yaml_config(config_file: str) -> Config:
113
144
  )
114
145
  type_spec_types = [os.path.abspath(folder) for folder in raw_type_spec_types]
115
146
 
116
- api_endpoint = _parse_string_lookup(
117
- "api_endpoint", raw_config.get("api_endpoint", {}), lambda x: x
147
+ api_endpoint = _parse_data_lookup(
148
+ "api_endpoint",
149
+ raw_config.get("api_endpoint", {}),
150
+ APIEndpointInfo,
118
151
  )
119
152
 
120
153
  raw_typescript = raw_config.get("typescript")
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from . import builder
6
+ from .builder_types import CrossOutputPaths
7
+
8
+
9
+ def get_python_stub_file_path(
10
+ function_name: str | None,
11
+ ) -> str | None:
12
+ if function_name is None:
13
+ return None
14
+ module_dir, file_name, _func_name = function_name.rsplit(".", 2)
15
+ module_path = os.path.relpath(module_dir.replace(".", "/"))
16
+ api_stub_file = f"{module_path}/{file_name}.py"
17
+ return api_stub_file
18
+
19
+
20
+ def get_python_api_file_path(
21
+ cross_output_paths: CrossOutputPaths,
22
+ namespace: builder.SpecNamespace,
23
+ ) -> str:
24
+ return f"{cross_output_paths.python_types_output}/{'/'.join(namespace.path)}{'' if len(namespace.path) > 1 else '_t'}.py"
25
+
26
+
27
+ def get_typescript_api_file_path(
28
+ cross_output_paths: CrossOutputPaths,
29
+ namespace: builder.SpecNamespace,
30
+ endpoint_key: builder.EndpointKey,
31
+ ) -> str:
32
+ return f"{cross_output_paths.typescript_routes_output_by_endpoint[endpoint_key]}/{'/'.join(namespace.path)}.tsx"
33
+
34
+
35
+ def get_yaml_api_file_path(
36
+ cross_output_paths: CrossOutputPaths,
37
+ namespace: builder.SpecNamespace,
38
+ ) -> str:
39
+ abs_path = next(
40
+ (
41
+ path
42
+ for path in cross_output_paths.typespec_files_input
43
+ if (
44
+ namespace.endpoint is None
45
+ or namespace.endpoint.default_endpoint_key in path
46
+ )
47
+ ),
48
+ cross_output_paths.typespec_files_input[0],
49
+ )
50
+ return f"{os.path.relpath(abs_path)}/{'/'.join(namespace.path)}.yaml"
51
+
52
+
53
+ def get_return_to_root_path(path: str) -> str:
54
+ return "../" * (path.count("/"))
55
+
56
+
57
+ def get_path_links(
58
+ cross_output_paths: CrossOutputPaths | None,
59
+ namespace: builder.SpecNamespace,
60
+ *,
61
+ current_path_type: str,
62
+ endpoint: builder.SpecEndpoint,
63
+ ) -> str:
64
+ if cross_output_paths is None:
65
+ return ""
66
+
67
+ api_paths = {
68
+ "Python": get_python_api_file_path(cross_output_paths, namespace),
69
+ "TypeScript": get_typescript_api_file_path(
70
+ cross_output_paths, namespace, endpoint.default_endpoint_key
71
+ ),
72
+ "YAML": get_yaml_api_file_path(cross_output_paths, namespace),
73
+ }
74
+
75
+ assert current_path_type in api_paths
76
+
77
+ comment_prefix = "#"
78
+ if current_path_type == "TypeScript":
79
+ comment_prefix = "//"
80
+
81
+ return_to_root_path = get_return_to_root_path(api_paths[current_path_type])
82
+ del api_paths[current_path_type]
83
+
84
+ paths_string = ""
85
+ for path_name, path in api_paths.items():
86
+ paths_string += (
87
+ f"{comment_prefix} {path_name}: file://./{return_to_root_path}{path}\n"
88
+ )
89
+
90
+ if namespace.endpoint is not None:
91
+ for (
92
+ endpoint_key,
93
+ path_specific_endpoint,
94
+ ) in namespace.endpoint.path_per_api_endpoint.items():
95
+ path_from_root = get_python_stub_file_path(path_specific_endpoint.function)
96
+ if path_from_root is None:
97
+ continue
98
+ paths_string += f"{comment_prefix} Implementation for {endpoint_key}: file://./{return_to_root_path}{path_from_root}\n"
99
+ return paths_string
@@ -7,10 +7,11 @@ WORK-IN-PROGRESS, DON'T USE!
7
7
  import dataclasses
8
8
  import json
9
9
  import re
10
- from typing import Collection, cast
10
+ from enum import StrEnum
11
+ from typing import Collection, assert_never, cast
11
12
 
12
13
  from pkgs.serialization import yaml
13
- from pkgs.serialization_util.serialization_helpers import serialize_for_api
14
+ from pkgs.serialization_util import serialize_for_api
14
15
 
15
16
  from . import builder, util
16
17
  from .builder import EndpointGuideKey, RootGuideKey
@@ -62,6 +63,10 @@ base_name_map = {
62
63
  }
63
64
 
64
65
 
66
+ class OpenAPIDefaultBehavior(StrEnum):
67
+ OPTIONAL_WITH_DEFAULT = "optional_with_default"
68
+
69
+
65
70
  def _rewrite_with_notice(
66
71
  file_path: str, file_content: str, *, notice: str = MODIFY_NOTICE
67
72
  ) -> bool:
@@ -126,7 +131,11 @@ def emit_open_api(builder: builder.SpecBuilder, *, config: OpenAPIConfig) -> Non
126
131
  for namespace in sorted(builder.namespaces.values(), key=lambda ns: ns.name):
127
132
  ctx = EmitOpenAPIContext(namespace=namespace)
128
133
 
129
- if ctx.namespace.endpoint is not None and ctx.namespace.endpoint.is_beta:
134
+ if (
135
+ ctx.namespace.endpoint is not None
136
+ and ctx.namespace.endpoint.stability_level
137
+ == EmitOpenAPIStabilityLevel.draft
138
+ ):
130
139
  continue
131
140
 
132
141
  if ctx.namespace.name == "base":
@@ -252,18 +261,35 @@ def _emit_endpoint_parameters(
252
261
  } | _emit_endpoint_parameter_examples(examples)
253
262
 
254
263
 
255
- def _emit_is_beta(is_beta: bool) -> DictApiSchema:
256
- if is_beta:
257
- return {"x-beta": True}
258
- return {}
264
+ def _emit_endpoint_deprecated(deprecated: bool) -> DictApiSchema:
265
+ return {"deprecated": True} if deprecated else {}
259
266
 
260
267
 
261
268
  def _emit_stability_level(
262
269
  stability_level: EmitOpenAPIStabilityLevel | None,
263
270
  ) -> DictApiSchema:
264
- if stability_level is not None:
265
- return {"x-stability-level": str(stability_level)}
266
- return {}
271
+ stability_info: dict[str, ApiSchema] = {}
272
+ resolved_stability_level = (
273
+ stability_level
274
+ if stability_level is not None
275
+ else EmitOpenAPIStabilityLevel.stable
276
+ )
277
+ stability_info["x-stability-level"] = str(resolved_stability_level)
278
+ match resolved_stability_level:
279
+ case EmitOpenAPIStabilityLevel.draft:
280
+ stability_info["x-beta"] = True
281
+ case EmitOpenAPIStabilityLevel.beta:
282
+ stability_info["x-badges"] = [
283
+ {
284
+ "name": "Beta",
285
+ "color": "DarkOrange",
286
+ }
287
+ ]
288
+ case EmitOpenAPIStabilityLevel.stable:
289
+ pass
290
+ case _:
291
+ assert_never(stability_level)
292
+ return stability_info
267
293
 
268
294
 
269
295
  def _emit_endpoint_request_body(
@@ -313,18 +339,57 @@ def _emit_endpoint_response_examples(
313
339
  return {"examples": response_examples}
314
340
 
315
341
 
342
+ def _create_warning_banner(api_type: str, message: str) -> str:
343
+ return (
344
+ f'<div style="background-color: #fff3cd; border: 1px solid #ffeaa7; '
345
+ f'border-radius: 4px; padding: 12px; margin-bottom: 16px;">'
346
+ f"<strong>⚠️ {api_type} API:</strong> {message}"
347
+ f"</div>"
348
+ )
349
+
350
+
351
+ def _get_stability_warning(
352
+ stability_level: EmitOpenAPIStabilityLevel | None,
353
+ ) -> str:
354
+ resolved_stability_level = (
355
+ stability_level
356
+ if stability_level is not None
357
+ else EmitOpenAPIStabilityLevel.stable
358
+ )
359
+
360
+ match resolved_stability_level:
361
+ case EmitOpenAPIStabilityLevel.draft:
362
+ return _create_warning_banner(
363
+ "Draft",
364
+ "This endpoint is in draft status and may change significantly. Not recommended for production use.",
365
+ )
366
+ case EmitOpenAPIStabilityLevel.beta:
367
+ return _create_warning_banner(
368
+ "Beta",
369
+ "This endpoint is in beta and its required parameters may change. Use with caution in production environments.",
370
+ )
371
+ case EmitOpenAPIStabilityLevel.stable:
372
+ return ""
373
+
374
+
316
375
  def _emit_endpoint_description(
317
- description: str, guides: list[EmitOpenAPIGuide]
376
+ description: str,
377
+ guides: list[EmitOpenAPIGuide],
378
+ stability_level: EmitOpenAPIStabilityLevel | None = None,
318
379
  ) -> dict[str, str]:
380
+ stability_warning = _get_stability_warning(stability_level)
381
+
319
382
  full_guides = "<br/>".join([
320
383
  _write_guide_as_html(guide, is_open=False)
321
384
  for guide in sorted(guides, key=lambda g: g.ref_name)
322
385
  ])
323
- return {
324
- "description": description
325
- if len(guides) == 0
326
- else f"{description}<br/>{full_guides}"
327
- }
386
+
387
+ full_description_parts = [
388
+ part for part in [stability_warning, description, full_guides] if part
389
+ ]
390
+ full_description = "<br/>".join(full_description_parts)
391
+
392
+ return {"description": full_description}
328
393
 
329
394
 
330
395
  def _emit_namespace(
@@ -359,8 +424,10 @@ def _emit_namespace(
359
424
  "tags": endpoint.tags,
360
425
  "summary": endpoint.summary,
361
426
  }
362
- | _emit_endpoint_description(endpoint.description, ctx.endpoint.guides)
363
- | _emit_is_beta(endpoint.is_beta)
427
+ | _emit_endpoint_deprecated(endpoint.deprecated)
428
+ | _emit_endpoint_description(
429
+ endpoint.description, ctx.endpoint.guides, endpoint.stability_level
430
+ )
364
431
  | _emit_stability_level(endpoint.stability_level)
365
432
  | _emit_endpoint_parameters(endpoint, argument_type, ctx.endpoint.examples)
366
433
  | _emit_endpoint_request_body(
@@ -421,6 +488,18 @@ def _emit_namespace(
421
488
  _rewrite_with_notice(path, yaml.dumps(oa_namespace, sort_keys=False))
422
489
 
423
490
 
491
+ def _get_openapi_default_behavior(
492
+ prop: builder.SpecProperty,
493
+ ) -> OpenAPIDefaultBehavior | None:
494
+ if prop.ext_info is None or prop.ext_info.get("open_api") is None:
495
+ return None
496
+ value_passed = prop.ext_info["open_api"].get("default_required_behavior")
497
+ if value_passed is None:
498
+ return None
499
+ assert isinstance(value_passed, str)
500
+ return OpenAPIDefaultBehavior(value_passed)
501
+
502
+
424
503
  def _emit_type(
425
504
  ctx: EmitOpenAPIContext,
426
505
  stype: builder.SpecType,
@@ -446,8 +525,18 @@ def _emit_type(
446
525
  return
447
526
 
448
527
  if isinstance(stype, builder.SpecTypeDefnUnion):
449
- ctx.types[stype.name] = open_api_type(
450
- ctx, stype.get_backing_type(), config=config
528
+ converted_discriminator_map: dict[str, OpenAPIRefType] = dict()
529
+ if stype.discriminator_map is not None:
530
+ for discriminator_value, base_type in stype.discriminator_map.items():
531
+ converted_base_type = open_api_type(ctx, base_type, config=config)
532
+ assert isinstance(converted_base_type, OpenAPIRefType)
533
+ converted_discriminator_map[discriminator_value] = converted_base_type
534
+ ctx.types[stype.name] = OpenAPIUnionType(
535
+ [open_api_type(ctx, p, config=config) for p in stype.types],
536
+ discriminator=stype.discriminator,
537
+ discriminator_map=converted_discriminator_map
538
+ if stype.discriminator_map is not None
539
+ else None,
451
540
  )
452
541
  return
453
542
 
@@ -484,6 +573,16 @@ def _emit_type(
484
573
  # arguments, thus treat like extant==missing
485
574
  # IMPROVE: if we can decide they are meant as output instead, then
486
575
  # they should be marked as required
576
+ openapi_default_beahvior = _get_openapi_default_behavior(prop)
577
+ match openapi_default_beahvior:
578
+ case None:
579
+ pass
580
+ case OpenAPIDefaultBehavior.OPTIONAL_WITH_DEFAULT:
581
+ ref_type.nullable = True
582
+ assert prop.default is not None, (
583
+ "optional_with_default requires default"
584
+ )
585
+ ref_type.default = prop.default
487
586
  properties[prop_name] = ref_type
488
587
  elif prop.extant == builder.PropertyExtant.missing:
489
588
  # Unlike optional below, missing does not imply null is possible. They
@@ -511,18 +610,6 @@ def _emit_type(
511
610
  ctx.types[stype.name] = final_type
512
611
 
513
612
 
514
- def _emit_constant(ctx: EmitOpenAPIContext, sconst: builder.SpecConstant) -> None:
515
- if sconst.value_type.is_base_type(builder.BaseTypeName.s_string):
516
- value = util.encode_common_string(cast(str, sconst.value))
517
- elif sconst.value_type.is_base_type(builder.BaseTypeName.s_integer):
518
- value = str(sconst.value)
519
- else:
520
- raise Exception("invalid constant type", sconst.name)
521
-
522
- const_name = sconst.name.upper()
523
- print("_emit_constant", value, const_name)
524
-
525
-
526
613
  def _emit_endpoint(
527
614
  gctx: EmitOpenAPIGlobalContext,
528
615
  ctx: EmitOpenAPIContext,
@@ -563,7 +650,7 @@ def _emit_endpoint(
563
650
  ep = namespace.endpoint
564
651
  gctx.paths.append(
565
652
  EmitOpenAPIPath(
566
- path=f"/{ep.path_root}/{ep.path_dirname}/{ep.path_basename}",
653
+ path=f"/{ep.resolved_path}",
567
654
  ref=ref_path,
568
655
  )
569
656
  )
@@ -579,7 +666,7 @@ def _emit_endpoint(
579
666
  tags=[tag_name],
580
667
  summary=f"{'/'.join(namespace.path[path_cutoff:])}",
581
668
  description=description,
582
- is_beta=namespace.endpoint.is_beta,
669
+ deprecated=namespace.endpoint.deprecated,
583
670
  stability_level=namespace.endpoint.stability_level,
584
671
  examples=[
585
672
  EmitOpenAPIEndpointExample(
@@ -7,16 +7,16 @@ 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
14
14
 
15
15
  MODIFY_NOTICE = "# DO NOT MODIFY -- This file is generated by type_spec"
16
16
 
17
- type GlobalContextInfo = dict[str, str | dict[str, str]]
18
- type TagGroupToNamedTags = dict[str, str | list[str]]
19
- type TagPathsToRef = dict[str, dict[str, str]]
17
+ GlobalContextInfo = dict[str, str | dict[str, str]]
18
+ TagGroupToNamedTags = dict[str, str | list[str]]
19
+ TagPathsToRef = dict[str, dict[str, str]]
20
20
 
21
21
 
22
22
  @dataclass
@@ -82,7 +82,7 @@ class EmitOpenAPIEndpoint:
82
82
  tags: list[str]
83
83
  summary: str
84
84
  description: str
85
- is_beta: bool
85
+ deprecated: bool
86
86
  stability_level: EmitOpenAPIStabilityLevel | None
87
87
  examples: list[EmitOpenAPIEndpointExample]
88
88
  guides: list[EmitOpenAPIGuide]