UncountablePythonSDK 0.0.8__py3-none-any.whl → 0.0.92__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 (312) hide show
  1. UncountablePythonSDK-0.0.92.dist-info/METADATA +61 -0
  2. UncountablePythonSDK-0.0.92.dist-info/RECORD +301 -0
  3. {UncountablePythonSDK-0.0.8.dist-info → UncountablePythonSDK-0.0.92.dist-info}/WHEEL +1 -1
  4. {UncountablePythonSDK-0.0.8.dist-info → UncountablePythonSDK-0.0.92.dist-info}/top_level.txt +1 -1
  5. docs/.gitignore +1 -0
  6. docs/conf.py +57 -0
  7. docs/index.md +13 -0
  8. docs/justfile +12 -0
  9. docs/quickstart.md +19 -0
  10. docs/requirements.txt +7 -0
  11. docs/static/favicons/android-chrome-192x192.png +0 -0
  12. docs/static/favicons/android-chrome-512x512.png +0 -0
  13. docs/static/favicons/apple-touch-icon.png +0 -0
  14. docs/static/favicons/browserconfig.xml +9 -0
  15. docs/static/favicons/favicon-16x16.png +0 -0
  16. docs/static/favicons/favicon-32x32.png +0 -0
  17. docs/static/favicons/manifest.json +18 -0
  18. docs/static/favicons/mstile-150x150.png +0 -0
  19. docs/static/favicons/safari-pinned-tab.svg +32 -0
  20. docs/static/logo_blue.png +0 -0
  21. examples/async_batch.py +35 -0
  22. examples/create_entity.py +22 -17
  23. examples/download_files.py +26 -0
  24. examples/edit_recipe_inputs.py +50 -0
  25. examples/integration-server/jobs/materials_auto/example_cron.py +18 -0
  26. examples/integration-server/jobs/materials_auto/example_wh.py +15 -0
  27. examples/integration-server/jobs/materials_auto/profile.yaml +43 -0
  28. examples/integration-server/pyproject.toml +224 -0
  29. examples/invoke_uploader.py +26 -0
  30. examples/set_recipe_metadata_file.py +40 -0
  31. examples/set_recipe_output_file_sdk.py +26 -0
  32. examples/upload_files.py +18 -0
  33. pkgs/argument_parser/__init__.py +5 -0
  34. pkgs/argument_parser/_is_enum.py +1 -6
  35. pkgs/argument_parser/argument_parser.py +232 -76
  36. pkgs/argument_parser/case_convert.py +4 -3
  37. pkgs/filesystem_utils/__init__.py +20 -0
  38. pkgs/filesystem_utils/_blob_session.py +137 -0
  39. pkgs/filesystem_utils/_gdrive_session.py +309 -0
  40. pkgs/filesystem_utils/_local_session.py +69 -0
  41. pkgs/filesystem_utils/_s3_session.py +117 -0
  42. pkgs/filesystem_utils/_sftp_session.py +147 -0
  43. pkgs/filesystem_utils/file_type_utils.py +91 -0
  44. pkgs/filesystem_utils/filesystem_session.py +39 -0
  45. pkgs/py.typed +0 -0
  46. pkgs/serialization/__init__.py +8 -1
  47. pkgs/serialization/annotation.py +64 -0
  48. pkgs/serialization/missing_sentry.py +1 -1
  49. pkgs/serialization/opaque_key.py +1 -1
  50. pkgs/serialization/serial_alias.py +47 -0
  51. pkgs/serialization/serial_class.py +65 -50
  52. pkgs/serialization/serial_generic.py +16 -0
  53. pkgs/serialization/serial_union.py +84 -0
  54. pkgs/serialization/yaml.py +57 -0
  55. pkgs/serialization_util/__init__.py +7 -7
  56. pkgs/serialization_util/_get_type_for_serialization.py +1 -3
  57. pkgs/serialization_util/convert_to_snakecase.py +27 -0
  58. pkgs/serialization_util/dataclasses.py +14 -0
  59. pkgs/serialization_util/serialization_helpers.py +116 -74
  60. pkgs/strenum_compat/strenum_compat.py +1 -9
  61. pkgs/type_spec/actions_registry/__init__.py +0 -0
  62. pkgs/type_spec/actions_registry/__main__.py +126 -0
  63. pkgs/type_spec/actions_registry/emit_typescript.py +182 -0
  64. pkgs/type_spec/builder.py +475 -89
  65. pkgs/type_spec/config.py +24 -19
  66. pkgs/type_spec/emit_io_ts.py +5 -2
  67. pkgs/type_spec/emit_open_api.py +266 -32
  68. pkgs/type_spec/emit_open_api_util.py +32 -13
  69. pkgs/type_spec/emit_python.py +599 -151
  70. pkgs/type_spec/emit_typescript.py +74 -273
  71. pkgs/type_spec/emit_typescript_util.py +239 -5
  72. pkgs/type_spec/load_types.py +55 -10
  73. pkgs/type_spec/open_api_util.py +30 -41
  74. pkgs/type_spec/parts/base.py.prepart +4 -3
  75. pkgs/type_spec/type_info/emit_type_info.py +178 -16
  76. pkgs/type_spec/util.py +11 -11
  77. pkgs/type_spec/value_spec/__main__.py +3 -3
  78. pkgs/type_spec/value_spec/convert_type.py +8 -1
  79. pkgs/type_spec/value_spec/emit_python.py +13 -4
  80. uncountable/__init__.py +1 -2
  81. uncountable/core/__init__.py +12 -2
  82. uncountable/core/async_batch.py +37 -0
  83. uncountable/core/client.py +293 -43
  84. uncountable/core/environment.py +41 -0
  85. uncountable/core/file_upload.py +135 -0
  86. uncountable/core/types.py +17 -0
  87. uncountable/integration/__init__.py +0 -0
  88. uncountable/integration/cli.py +49 -0
  89. uncountable/integration/construct_client.py +51 -0
  90. uncountable/integration/cron.py +29 -0
  91. uncountable/integration/db/__init__.py +0 -0
  92. uncountable/integration/db/connect.py +18 -0
  93. uncountable/integration/db/session.py +25 -0
  94. uncountable/integration/entrypoint.py +13 -0
  95. uncountable/integration/executors/__init__.py +0 -0
  96. uncountable/integration/executors/executors.py +148 -0
  97. uncountable/integration/executors/generic_upload_executor.py +284 -0
  98. uncountable/integration/executors/script_executor.py +25 -0
  99. uncountable/integration/job.py +87 -0
  100. uncountable/integration/queue_runner/__init__.py +0 -0
  101. uncountable/integration/queue_runner/command_server/__init__.py +24 -0
  102. uncountable/integration/queue_runner/command_server/command_client.py +68 -0
  103. uncountable/integration/queue_runner/command_server/command_server.py +64 -0
  104. uncountable/integration/queue_runner/command_server/protocol/__init__.py +0 -0
  105. uncountable/integration/queue_runner/command_server/protocol/command_server.proto +22 -0
  106. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +40 -0
  107. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +38 -0
  108. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +129 -0
  109. uncountable/integration/queue_runner/command_server/types.py +52 -0
  110. uncountable/integration/queue_runner/datastore/__init__.py +3 -0
  111. uncountable/integration/queue_runner/datastore/datastore_sqlite.py +93 -0
  112. uncountable/integration/queue_runner/datastore/interface.py +19 -0
  113. uncountable/integration/queue_runner/datastore/model.py +17 -0
  114. uncountable/integration/queue_runner/job_scheduler.py +163 -0
  115. uncountable/integration/queue_runner/queue_runner.py +26 -0
  116. uncountable/integration/queue_runner/types.py +7 -0
  117. uncountable/integration/queue_runner/worker.py +119 -0
  118. uncountable/integration/scan_profiles.py +67 -0
  119. uncountable/integration/scheduler.py +150 -0
  120. uncountable/integration/secret_retrieval/__init__.py +3 -0
  121. uncountable/integration/secret_retrieval/retrieve_secret.py +93 -0
  122. uncountable/integration/server.py +117 -0
  123. uncountable/integration/telemetry.py +209 -0
  124. uncountable/integration/webhook_server/entrypoint.py +170 -0
  125. uncountable/types/__init__.py +136 -20
  126. uncountable/types/api/batch/execute_batch.py +15 -7
  127. uncountable/types/api/batch/execute_batch_load_async.py +42 -0
  128. uncountable/types/api/chemical/__init__.py +1 -0
  129. uncountable/types/api/chemical/convert_chemical_formats.py +63 -0
  130. uncountable/types/api/entity/create_entities.py +23 -11
  131. uncountable/types/api/entity/create_entity.py +21 -12
  132. uncountable/types/api/entity/get_entities_data.py +18 -8
  133. uncountable/types/api/entity/grant_entity_permissions.py +48 -0
  134. uncountable/types/api/entity/list_entities.py +27 -12
  135. uncountable/types/api/entity/lock_entity.py +45 -0
  136. uncountable/types/api/entity/resolve_entity_ids.py +17 -7
  137. uncountable/types/api/entity/set_entity_field_values.py +44 -0
  138. uncountable/types/api/entity/set_values.py +14 -7
  139. uncountable/types/api/entity/transition_entity_phase.py +80 -0
  140. uncountable/types/api/entity/unlock_entity.py +44 -0
  141. uncountable/types/api/equipment/__init__.py +1 -0
  142. uncountable/types/api/equipment/associate_equipment_input.py +44 -0
  143. uncountable/types/api/field_options/__init__.py +1 -0
  144. uncountable/types/api/field_options/upsert_field_options.py +55 -0
  145. uncountable/types/api/files/__init__.py +1 -0
  146. uncountable/types/api/files/download_file.py +77 -0
  147. uncountable/types/api/id_source/__init__.py +1 -0
  148. uncountable/types/api/id_source/list_id_source.py +56 -0
  149. uncountable/types/api/id_source/match_id_source.py +54 -0
  150. uncountable/types/api/input_groups/get_input_group_names.py +16 -6
  151. uncountable/types/api/inputs/create_inputs.py +24 -11
  152. uncountable/types/api/inputs/get_input_data.py +32 -13
  153. uncountable/types/api/inputs/get_input_names.py +18 -8
  154. uncountable/types/api/inputs/get_inputs_data.py +29 -10
  155. uncountable/types/api/inputs/set_input_attribute_values.py +16 -9
  156. uncountable/types/api/inputs/set_input_category.py +44 -0
  157. uncountable/types/api/inputs/set_input_subcategories.py +45 -0
  158. uncountable/types/api/inputs/set_intermediate_type.py +50 -0
  159. uncountable/types/api/material_families/__init__.py +1 -0
  160. uncountable/types/api/material_families/update_entity_material_families.py +48 -0
  161. uncountable/types/api/outputs/get_output_data.py +32 -16
  162. uncountable/types/api/outputs/get_output_names.py +18 -8
  163. uncountable/types/api/outputs/resolve_output_conditions.py +23 -10
  164. uncountable/types/api/permissions/__init__.py +1 -0
  165. uncountable/types/api/permissions/set_core_permissions.py +105 -0
  166. uncountable/types/api/project/get_projects.py +17 -7
  167. uncountable/types/api/project/get_projects_data.py +21 -11
  168. uncountable/types/api/recipe_links/__init__.py +1 -0
  169. uncountable/types/api/recipe_links/create_recipe_link.py +46 -0
  170. uncountable/types/api/recipe_links/remove_recipe_link.py +45 -0
  171. uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +18 -8
  172. uncountable/types/api/recipes/add_recipe_to_project.py +42 -0
  173. uncountable/types/api/recipes/archive_recipes.py +42 -0
  174. uncountable/types/api/recipes/associate_recipe_as_input.py +44 -0
  175. uncountable/types/api/recipes/associate_recipe_as_lot.py +43 -0
  176. uncountable/types/api/recipes/clear_recipe_outputs.py +42 -0
  177. uncountable/types/api/recipes/create_recipe.py +51 -0
  178. uncountable/types/api/recipes/create_recipes.py +25 -12
  179. uncountable/types/api/recipes/disassociate_recipe_as_input.py +42 -0
  180. uncountable/types/api/recipes/edit_recipe_inputs.py +283 -0
  181. uncountable/types/api/recipes/get_column_calculation_values.py +58 -0
  182. uncountable/types/api/recipes/get_curve.py +15 -7
  183. uncountable/types/api/recipes/get_recipe_calculations.py +17 -10
  184. uncountable/types/api/recipes/get_recipe_links.py +13 -6
  185. uncountable/types/api/recipes/get_recipe_names.py +16 -6
  186. uncountable/types/api/recipes/get_recipe_output_metadata.py +14 -7
  187. uncountable/types/api/recipes/get_recipes_data.py +63 -38
  188. uncountable/types/api/recipes/lock_recipes.py +63 -0
  189. uncountable/types/api/recipes/remove_recipe_from_project.py +42 -0
  190. uncountable/types/api/recipes/set_recipe_inputs.py +19 -10
  191. uncountable/types/api/recipes/set_recipe_metadata.py +43 -0
  192. uncountable/types/api/recipes/set_recipe_output_annotations.py +115 -0
  193. uncountable/types/api/recipes/set_recipe_output_file.py +56 -0
  194. uncountable/types/api/recipes/set_recipe_outputs.py +26 -12
  195. uncountable/types/api/recipes/set_recipe_tags.py +109 -0
  196. uncountable/types/api/recipes/unarchive_recipes.py +41 -0
  197. uncountable/types/api/recipes/unlock_recipes.py +50 -0
  198. uncountable/types/api/triggers/__init__.py +1 -0
  199. uncountable/types/api/triggers/run_trigger.py +43 -0
  200. uncountable/types/api/uploader/__init__.py +1 -0
  201. uncountable/types/api/uploader/invoke_uploader.py +47 -0
  202. uncountable/types/async_batch.py +13 -0
  203. uncountable/types/async_batch_processor.py +384 -0
  204. uncountable/types/async_batch_t.py +97 -0
  205. uncountable/types/async_jobs.py +9 -0
  206. uncountable/types/async_jobs_t.py +53 -0
  207. uncountable/types/auth_retrieval.py +12 -0
  208. uncountable/types/auth_retrieval_t.py +75 -0
  209. uncountable/types/base.py +5 -78
  210. uncountable/types/base_t.py +85 -0
  211. uncountable/types/calculations.py +3 -18
  212. uncountable/types/calculations_t.py +27 -0
  213. uncountable/types/chemical_structure.py +8 -0
  214. uncountable/types/chemical_structure_t.py +28 -0
  215. uncountable/types/client_base.py +1093 -54
  216. uncountable/types/client_config.py +8 -0
  217. uncountable/types/client_config_t.py +26 -0
  218. uncountable/types/curves.py +5 -42
  219. uncountable/types/curves_t.py +51 -0
  220. uncountable/types/entity.py +8 -269
  221. uncountable/types/entity_t.py +393 -0
  222. uncountable/types/experiment_groups.py +3 -18
  223. uncountable/types/experiment_groups_t.py +27 -0
  224. uncountable/types/field_values.py +17 -60
  225. uncountable/types/field_values_t.py +204 -0
  226. uncountable/types/fields.py +3 -19
  227. uncountable/types/fields_t.py +28 -0
  228. uncountable/types/generic_upload.py +15 -0
  229. uncountable/types/generic_upload_t.py +119 -0
  230. uncountable/types/id_source.py +12 -0
  231. uncountable/types/id_source_t.py +68 -0
  232. uncountable/types/identifier.py +11 -0
  233. uncountable/types/identifier_t.py +63 -0
  234. uncountable/types/input_attributes.py +3 -24
  235. uncountable/types/input_attributes_t.py +30 -0
  236. uncountable/types/inputs.py +6 -56
  237. uncountable/types/inputs_t.py +83 -0
  238. uncountable/types/integration_server.py +9 -0
  239. uncountable/types/integration_server_t.py +42 -0
  240. uncountable/types/job_definition.py +27 -0
  241. uncountable/types/job_definition_t.py +260 -0
  242. uncountable/types/outputs.py +3 -21
  243. uncountable/types/outputs_t.py +30 -0
  244. uncountable/types/overrides.py +10 -0
  245. uncountable/types/overrides_t.py +49 -0
  246. uncountable/types/permissions.py +8 -0
  247. uncountable/types/permissions_t.py +46 -0
  248. uncountable/types/phases.py +3 -18
  249. uncountable/types/phases_t.py +27 -0
  250. uncountable/types/post_base.py +8 -0
  251. uncountable/types/post_base_t.py +30 -0
  252. uncountable/types/queued_job.py +16 -0
  253. uncountable/types/queued_job_t.py +123 -0
  254. uncountable/types/recipe_identifiers.py +12 -0
  255. uncountable/types/recipe_identifiers_t.py +76 -0
  256. uncountable/types/recipe_inputs.py +9 -0
  257. uncountable/types/recipe_inputs_t.py +30 -0
  258. uncountable/types/recipe_links.py +4 -45
  259. uncountable/types/recipe_links_t.py +54 -0
  260. uncountable/types/recipe_metadata.py +5 -45
  261. uncountable/types/recipe_metadata_t.py +58 -0
  262. uncountable/types/recipe_output_metadata.py +3 -19
  263. uncountable/types/recipe_output_metadata_t.py +28 -0
  264. uncountable/types/recipe_tags.py +3 -18
  265. uncountable/types/recipe_tags_t.py +27 -0
  266. uncountable/types/recipe_workflow_steps.py +14 -0
  267. uncountable/types/recipe_workflow_steps_t.py +95 -0
  268. uncountable/types/recipes.py +8 -0
  269. uncountable/types/recipes_t.py +25 -0
  270. uncountable/types/response.py +3 -20
  271. uncountable/types/response_t.py +26 -0
  272. uncountable/types/secret_retrieval.py +12 -0
  273. uncountable/types/secret_retrieval_t.py +75 -0
  274. uncountable/types/units.py +3 -18
  275. uncountable/types/units_t.py +27 -0
  276. uncountable/types/users.py +3 -19
  277. uncountable/types/users_t.py +28 -0
  278. uncountable/types/webhook_job.py +9 -0
  279. uncountable/types/webhook_job_t.py +37 -0
  280. uncountable/types/workflows.py +4 -27
  281. uncountable/types/workflows_t.py +39 -0
  282. UncountablePythonSDK-0.0.8.dist-info/METADATA +0 -27
  283. UncountablePythonSDK-0.0.8.dist-info/RECORD +0 -134
  284. examples/recipe-import/importer.py +0 -39
  285. type_spec/external/api/batch/execute_batch.yaml +0 -56
  286. type_spec/external/api/entity/create_entities.yaml +0 -33
  287. type_spec/external/api/entity/create_entity.yaml +0 -39
  288. type_spec/external/api/entity/get_entities_data.yaml +0 -29
  289. type_spec/external/api/entity/list_entities.yaml +0 -52
  290. type_spec/external/api/entity/resolve_entity_ids.yaml +0 -29
  291. type_spec/external/api/entity/set_values.yaml +0 -18
  292. type_spec/external/api/input_groups/get_input_group_names.yaml +0 -29
  293. type_spec/external/api/inputs/create_inputs.yaml +0 -48
  294. type_spec/external/api/inputs/get_input_data.yaml +0 -95
  295. type_spec/external/api/inputs/get_input_names.yaml +0 -38
  296. type_spec/external/api/inputs/get_inputs_data.yaml +0 -82
  297. type_spec/external/api/inputs/set_input_attribute_values.yaml +0 -33
  298. type_spec/external/api/outputs/get_output_data.yaml +0 -92
  299. type_spec/external/api/outputs/get_output_names.yaml +0 -35
  300. type_spec/external/api/outputs/resolve_output_conditions.yaml +0 -50
  301. type_spec/external/api/project/get_projects.yaml +0 -42
  302. type_spec/external/api/project/get_projects_data.yaml +0 -50
  303. type_spec/external/api/recipe_metadata/get_recipe_metadata_data.yaml +0 -41
  304. type_spec/external/api/recipes/create_recipes.yaml +0 -47
  305. type_spec/external/api/recipes/get_curve.yaml +0 -18
  306. type_spec/external/api/recipes/get_recipe_calculations.yaml +0 -39
  307. type_spec/external/api/recipes/get_recipe_links.yaml +0 -26
  308. type_spec/external/api/recipes/get_recipe_names.yaml +0 -29
  309. type_spec/external/api/recipes/get_recipe_output_metadata.yaml +0 -36
  310. type_spec/external/api/recipes/get_recipes_data.yaml +0 -238
  311. type_spec/external/api/recipes/set_recipe_inputs.yaml +0 -36
  312. type_spec/external/api/recipes/set_recipe_outputs.yaml +0 -52
pkgs/type_spec/config.py CHANGED
@@ -1,9 +1,9 @@
1
1
  import os
2
+ from collections.abc import Callable, Mapping
2
3
  from dataclasses import dataclass
3
- from decimal import Decimal
4
- from typing import Callable, Mapping, Self, Type, TypeVar
4
+ from typing import Self, TypeVar
5
5
 
6
- import yaml
6
+ from pkgs.serialization import yaml
7
7
 
8
8
  ConfigValueType = str | None | Mapping[str, str | None] | list[str]
9
9
 
@@ -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)
@@ -55,6 +63,7 @@ class PythonConfig(BaseLanguageConfig):
55
63
  emit_api_argument_lookup: bool = (
56
64
  False # emit a lookup for api endpoint path to argument type.
57
65
  )
66
+ emit_async_batch_processor: bool = False # emit the async batch wrapping functions
58
67
  emit_client_class: bool = False # emit the base class for the api client
59
68
  all_named_type_exports: bool = False # emit __all__ for all named type exports
60
69
  sdk_endpoints_only: bool = False # only emit is_sdk endpoints
@@ -85,6 +94,7 @@ class OpenAPIConfig(BaseLanguageConfig):
85
94
 
86
95
  @dataclass(kw_only=True)
87
96
  class Config:
97
+ top_namespace: str
88
98
  type_spec_types: list[str] # folders containing the yaml type spec definitions
89
99
  api_endpoint: dict[str, str]
90
100
  # languages
@@ -96,30 +106,19 @@ class Config:
96
106
  _T = TypeVar("_T")
97
107
 
98
108
 
99
- def _parse_language(config_class: Type[_T], raw_value: ConfigValueType) -> _T:
109
+ def _parse_language(config_class: type[_T], raw_value: ConfigValueType) -> _T:
100
110
  assert isinstance(raw_value, dict), "expecting language config to have key/values."
101
111
  return config_class(**raw_value)
102
112
 
103
113
 
104
- def _decimal_constructor(loader, node): # type:ignore
105
- value = loader.construct_scalar(node)
106
- return Decimal(value)
107
-
108
-
109
- # A semi-acceptable patch to force a number to be parsed as a decimal, since pyyaml
110
- # parses them as lossy floats otherwise. Though a bit ugly, at least this way we have
111
- # support for decimal constants
112
- yaml.SafeLoader.add_constructor("!decimal", _decimal_constructor)
113
-
114
-
115
114
  def parse_yaml_config(config_file: str) -> Config:
116
115
  with open(config_file, encoding="utf-8") as input:
117
116
  raw_config: dict[str, ConfigValueType] = yaml.safe_load(input)
118
117
 
119
118
  raw_type_spec_types = raw_config["type_spec_types"]
120
- assert isinstance(
121
- raw_type_spec_types, list
122
- ), "type_spec_types, must be a list of folders"
119
+ assert isinstance(raw_type_spec_types, list), (
120
+ "type_spec_types, must be a list of folders"
121
+ )
123
122
  type_spec_types = [os.path.abspath(folder) for folder in raw_type_spec_types]
124
123
 
125
124
  api_endpoint = _parse_string_lookup(
@@ -135,10 +134,16 @@ def parse_yaml_config(config_file: str) -> Config:
135
134
  python = _parse_language(PythonConfig, raw_config["python"])
136
135
  raw_open_api = raw_config.get("open_api")
137
136
  open_api = (
138
- _parse_language(OpenAPIConfig, raw_open_api) if raw_open_api is not None else None
137
+ _parse_language(OpenAPIConfig, raw_open_api)
138
+ if raw_open_api is not None
139
+ else None
139
140
  )
140
141
 
142
+ top_namespace = raw_config["top_namespace"]
143
+ assert isinstance(top_namespace, str)
144
+
141
145
  return Config(
146
+ top_namespace=top_namespace,
142
147
  type_spec_types=type_spec_types,
143
148
  api_endpoint=api_endpoint,
144
149
  typescript=typescript,
@@ -118,11 +118,14 @@ def refer_to_io_ts(
118
118
  stype: builder.SpecType,
119
119
  ) -> str:
120
120
  if isinstance(stype, builder.SpecTypeInstance):
121
- if stype.defn_type.name == builder.BaseTypeName.s_list:
121
+ if (
122
+ stype.defn_type.name == builder.BaseTypeName.s_list
123
+ or stype.defn_type.name == builder.BaseTypeName.s_readonly_array
124
+ ):
122
125
  spec = refer_to_io_ts(ctx, stype.parameters[0])
123
126
  return f"IO.array({spec})"
124
127
  if stype.defn_type.name == builder.BaseTypeName.s_union:
125
- return f'IO.union([{", ".join([refer_to_io_ts(ctx, p) for p in stype.parameters])}])'
128
+ return f"IO.union([{', '.join([refer_to_io_ts(ctx, p) for p in stype.parameters])}])"
126
129
  if stype.defn_type.name == builder.BaseTypeName.s_optional:
127
130
  return f"IO.optional({refer_to_io_ts(ctx, stype.parameters[0])})"
128
131
  if stype.defn_type.name == builder.BaseTypeName.s_tuple:
@@ -5,20 +5,26 @@ WORK-IN-PROGRESS, DON'T USE!
5
5
  """
6
6
 
7
7
  import dataclasses
8
+ import json
8
9
  import re
9
- from typing import cast
10
+ from typing import Collection, cast
10
11
 
11
- import yaml
12
+ from pkgs.serialization import yaml
13
+ from pkgs.serialization_util import serialize_for_api
12
14
 
13
15
  from . import builder, util
16
+ from .builder import EndpointGuideKey, RootGuideKey
14
17
  from .config import OpenAPIConfig
15
18
  from .emit_open_api_util import (
16
19
  MODIFY_NOTICE,
17
20
  EmitOpenAPIContext,
18
21
  EmitOpenAPIEndpoint,
22
+ EmitOpenAPIEndpointExample,
19
23
  EmitOpenAPIGlobalContext,
24
+ EmitOpenAPIGuide,
20
25
  EmitOpenAPIPath,
21
26
  EmitOpenAPIServer,
27
+ EmitOpenAPIStabilityLevel,
22
28
  EmitOpenAPITag,
23
29
  GlobalContextInfo,
24
30
  TagGroupToNamedTags,
@@ -59,7 +65,7 @@ base_name_map = {
59
65
  def _rewrite_with_notice(
60
66
  file_path: str, file_content: str, *, notice: str = MODIFY_NOTICE
61
67
  ) -> bool:
62
- pattern = re.compile("^\S", re.MULTILINE)
68
+ pattern = re.compile(r"^\S", re.MULTILINE)
63
69
 
64
70
  file_lines = file_content.split("\n")
65
71
  comment_lines = []
@@ -74,12 +80,26 @@ def _rewrite_with_notice(
74
80
  return util.rewrite_file(file_path, f"{notice}\n{modified_file_content}")
75
81
 
76
82
 
77
- def _open_api_info(config: OpenAPIConfig) -> GlobalContextInfo:
78
- description = config.description
83
+ def _write_guide_as_html(guide: EmitOpenAPIGuide, *, is_open: bool) -> str:
84
+ return f"""
85
+ <details id="{guide.ref_name}" {"open" if is_open else ""}>
86
+ <summary>{guide.title}</summary>
87
+ {guide.html_content}
88
+ </details>"""
89
+
90
+
91
+ def _open_api_info(
92
+ config: OpenAPIConfig, guides: list[EmitOpenAPIGuide]
93
+ ) -> GlobalContextInfo:
94
+ full_guides = "<br/>".join([
95
+ _write_guide_as_html(guide, is_open=True)
96
+ for guide in sorted(guides, key=lambda g: g.ref_name)
97
+ ])
98
+ full_description = f"{config.description}<br/>{full_guides}"
79
99
  info: GlobalContextInfo = dict()
80
100
  info["version"] = "1.0.0"
81
101
  info["title"] = "Uncountable API Documentation"
82
- info["description"] = description
102
+ info["description"] = full_description
83
103
  info["x-logo"] = {"url": "../static/images/logo_blue.png", "altText": "Logo"}
84
104
  return info
85
105
 
@@ -90,15 +110,25 @@ def _open_api_servers(config: OpenAPIConfig) -> list[EmitOpenAPIServer]:
90
110
 
91
111
 
92
112
  def emit_open_api(builder: builder.SpecBuilder, *, config: OpenAPIConfig) -> None:
113
+ root_guides = builder.guides.get(RootGuideKey(), [])
114
+ openapi_guides = [
115
+ EmitOpenAPIGuide(
116
+ ref_name=guide.ref_name, title=guide.title, html_content=guide.html_content
117
+ )
118
+ for guide in root_guides
119
+ ]
93
120
  gctx = EmitOpenAPIGlobalContext(
94
121
  version="3.0.0",
95
- info=_open_api_info(config),
122
+ info=_open_api_info(config, openapi_guides),
96
123
  servers=_open_api_servers(config),
97
124
  )
98
125
 
99
126
  for namespace in sorted(builder.namespaces.values(), key=lambda ns: ns.name):
100
127
  ctx = EmitOpenAPIContext(namespace=namespace)
101
128
 
129
+ if ctx.namespace.endpoint is not None and ctx.namespace.endpoint.is_beta:
130
+ continue
131
+
102
132
  if ctx.namespace.name == "base":
103
133
  # TODO: add additional base defintions here
104
134
  ctx.types["ObjectId"] = OpenAPIIntegerT()
@@ -109,6 +139,8 @@ def emit_open_api(builder: builder.SpecBuilder, *, config: OpenAPIConfig) -> Non
109
139
  ctx,
110
140
  namespace=namespace,
111
141
  config=config,
142
+ examples=builder.examples,
143
+ guides=builder.guides,
112
144
  )
113
145
 
114
146
  _rewrite_with_notice(
@@ -138,7 +170,161 @@ def _serialize_global_context(ctx: EmitOpenAPIGlobalContext) -> str:
138
170
  oa_paths[path.path] = {"$ref": path.ref}
139
171
  oa_root["paths"] = oa_paths
140
172
 
141
- return yaml.dump(oa_root, sort_keys=False)
173
+ return yaml.dumps(oa_root, sort_keys=False)
174
+
175
+
176
+ def _is_empty_object_type(typ: OpenAPIType) -> bool:
177
+ if not isinstance(typ, OpenAPIObjectType):
178
+ return False
179
+ return len(typ.properties) == 0
180
+
181
+
182
+ _QUERY_PARM_METHODS = ("get", "head", "options")
183
+ _REQUEST_BODY_METHODS = ("put", "post", "patch", "delete")
184
+
185
+ ApiSchema = dict[str, "ApiSchema"] | Collection["ApiSchema"] | str | bool
186
+ DictApiSchema = dict[str, ApiSchema]
187
+
188
+
189
+ def _emit_endpoint_argument_examples(
190
+ examples: list[EmitOpenAPIEndpointExample],
191
+ ) -> DictApiSchema:
192
+ if len(examples) == 0:
193
+ return {}
194
+
195
+ response_examples = {}
196
+ for example in examples:
197
+ response_examples[example.ref_name] = {
198
+ "summary": example.summary,
199
+ "description": example.description,
200
+ "value": example.arguments,
201
+ }
202
+ return {"examples": response_examples}
203
+
204
+
205
+ def _emit_endpoint_parameter_examples(
206
+ examples: list[EmitOpenAPIEndpointExample],
207
+ ) -> DictApiSchema:
208
+ if len(examples) == 0:
209
+ return {}
210
+
211
+ paramater_examples = []
212
+ comment_new_line = "\n// "
213
+ new_line = "\n"
214
+ for example in examples:
215
+ javascript_description = (
216
+ f"// {comment_new_line.join(example.description.split(new_line))}"
217
+ )
218
+ javascript_json_payload = f"{json.dumps(example.arguments, indent=2)}"
219
+ paramater_examples.append({
220
+ "lang": "JavaScript",
221
+ "label": f"Payload - {example.summary}",
222
+ "source": f"{javascript_description}\n{javascript_json_payload}",
223
+ })
224
+ return {"x-codeSamples": paramater_examples}
225
+
226
+
227
+ def _emit_endpoint_parameters(
228
+ endpoint: EmitOpenAPIEndpoint,
229
+ argument_type: OpenAPIType | None,
230
+ examples: list[EmitOpenAPIEndpointExample],
231
+ ) -> DictApiSchema:
232
+ if (
233
+ endpoint.method.lower() not in _QUERY_PARM_METHODS
234
+ or argument_type is None
235
+ or _is_empty_object_type(argument_type)
236
+ ):
237
+ return {}
238
+
239
+ return {
240
+ "parameters": [
241
+ {
242
+ "name": "data",
243
+ "required": True,
244
+ "in": "query",
245
+ "content": {
246
+ "application/json": {
247
+ "schema": {"$ref": "#/components/schema/Arguments"}
248
+ }
249
+ },
250
+ }
251
+ ]
252
+ } | _emit_endpoint_parameter_examples(examples)
253
+
254
+
255
+ def _emit_is_beta(is_beta: bool) -> DictApiSchema:
256
+ if is_beta:
257
+ return {"x-beta": True}
258
+ return {}
259
+
260
+
261
+ def _emit_stability_level(
262
+ stability_level: EmitOpenAPIStabilityLevel | None,
263
+ ) -> DictApiSchema:
264
+ if stability_level is not None:
265
+ return {"x-stability-level": str(stability_level)}
266
+ return {}
267
+
268
+
269
+ def _emit_endpoint_request_body(
270
+ endpoint: EmitOpenAPIEndpoint,
271
+ arguments_type: OpenAPIType | None,
272
+ examples: list[EmitOpenAPIEndpointExample],
273
+ ) -> DictApiSchema:
274
+ if (
275
+ endpoint.method.lower() not in _REQUEST_BODY_METHODS
276
+ or arguments_type is None
277
+ or _is_empty_object_type(arguments_type)
278
+ ):
279
+ return {}
280
+
281
+ return {
282
+ "requestBody": {
283
+ "content": {
284
+ "application/json": {
285
+ "schema": {
286
+ "type": "object",
287
+ "title": "Body",
288
+ "required": ["data"],
289
+ "properties": {
290
+ "data": {"$ref": "#/components/schema/Arguments"}
291
+ },
292
+ }
293
+ }
294
+ | _emit_endpoint_argument_examples(examples)
295
+ },
296
+ }
297
+ }
298
+
299
+
300
+ def _emit_endpoint_response_examples(
301
+ examples: list[EmitOpenAPIEndpointExample],
302
+ ) -> dict[str, dict[str, object]]:
303
+ if len(examples) == 0:
304
+ return {}
305
+
306
+ response_examples: dict[str, object] = {}
307
+ for example in examples:
308
+ response_examples[example.ref_name] = {
309
+ "summary": example.summary,
310
+ "description": example.description,
311
+ "value": example.data,
312
+ }
313
+ return {"examples": response_examples}
314
+
315
+
316
+ def _emit_endpoint_description(
317
+ description: str, guides: list[EmitOpenAPIGuide]
318
+ ) -> dict[str, str]:
319
+ full_guides = "<br/>".join([
320
+ _write_guide_as_html(guide, is_open=False)
321
+ for guide in sorted(guides, key=lambda g: g.ref_name)
322
+ ])
323
+ return {
324
+ "description": description
325
+ if len(guides) == 0
326
+ else f"{description}<br/>{full_guides}"
327
+ }
142
328
 
143
329
 
144
330
  def _emit_namespace(
@@ -147,35 +333,53 @@ def _emit_namespace(
147
333
  namespace: builder.SpecNamespace,
148
334
  *,
149
335
  config: OpenAPIConfig,
336
+ examples: dict[str, list[builder.SpecEndpointExample]],
337
+ guides: dict[builder.SpecGuideKey, list[builder.SpecGuide]],
150
338
  ) -> None:
151
339
  for stype in namespace.types.values():
152
340
  _emit_type(ctx, stype, config=config)
153
341
 
154
342
  if namespace.endpoint is not None:
155
- _emit_endpoint(gctx, ctx, namespace, namespace.endpoint)
343
+ endpoint_examples = examples.get(namespace.endpoint.resolved_path, [])
344
+ endpoint_guides = guides.get(
345
+ EndpointGuideKey(path=namespace.endpoint.resolved_path), []
346
+ )
347
+ _emit_endpoint(
348
+ gctx, ctx, namespace, namespace.endpoint, endpoint_examples, endpoint_guides
349
+ )
156
350
 
157
351
  oa_components: dict[str, object] = dict()
158
352
 
159
353
  if ctx.endpoint is not None:
160
354
  endpoint = ctx.endpoint
161
-
355
+ argument_type = ctx.types.get("Arguments")
162
356
  oa_endpoint = dict()
163
- oa_endpoint[endpoint.method] = {
164
- "tags": endpoint.tags,
165
- "summary": endpoint.summary,
166
- "description": endpoint.description,
167
- "parameters": {"$ref": "#/components/schema/Arguments"},
168
- "responses": {
169
- "200": {
170
- "description": "OK",
171
- "content": {
172
- "application/json": {
173
- "schema": {"$ref": "#/components/schema/Data"}
174
- }
175
- },
176
- }
177
- },
178
- }
357
+ oa_endpoint[endpoint.method] = (
358
+ {
359
+ "tags": endpoint.tags,
360
+ "summary": endpoint.summary,
361
+ }
362
+ | _emit_endpoint_description(endpoint.description, ctx.endpoint.guides)
363
+ | _emit_is_beta(endpoint.is_beta)
364
+ | _emit_stability_level(endpoint.stability_level)
365
+ | _emit_endpoint_parameters(endpoint, argument_type, ctx.endpoint.examples)
366
+ | _emit_endpoint_request_body(
367
+ endpoint, argument_type, ctx.endpoint.examples
368
+ )
369
+ | {
370
+ "responses": {
371
+ "200": {
372
+ "description": "OK",
373
+ "content": {
374
+ "application/json": {
375
+ "schema": {"$ref": "#/components/schema/Data"}
376
+ }
377
+ | _emit_endpoint_response_examples(ctx.endpoint.examples)
378
+ },
379
+ }
380
+ },
381
+ }
382
+ )
179
383
  oa_components["endpoint"] = oa_endpoint
180
384
 
181
385
  types = ctx.types
@@ -209,15 +413,12 @@ def _emit_namespace(
209
413
 
210
414
  oa_components["schema"] = cast(
211
415
  object,
212
- {
213
- name: (value.asdict() if name != "Arguments" else value.asarguments())
214
- for name, value in types.items()
215
- },
416
+ {name: value.asdict() for name, value in types.items()},
216
417
  )
217
418
 
218
419
  path = f"{config.types_output}/common/{'/'.join(namespace.path)}.yaml"
219
420
  oa_namespace = {"components": oa_components}
220
- _rewrite_with_notice(path, yaml.dump(oa_namespace, sort_keys=False))
421
+ _rewrite_with_notice(path, yaml.dumps(oa_namespace, sort_keys=False))
221
422
 
222
423
 
223
424
  def _emit_type(
@@ -244,6 +445,12 @@ def _emit_type(
244
445
  ctx.types[stype.name] = open_api_type(ctx, stype.alias, config=config)
245
446
  return
246
447
 
448
+ if isinstance(stype, builder.SpecTypeDefnUnion):
449
+ ctx.types[stype.name] = open_api_type(
450
+ ctx, stype.get_backing_type(), config=config
451
+ )
452
+ return
453
+
247
454
  if isinstance(stype, builder.SpecTypeDefnStringEnum):
248
455
  # TODO: check that these are always string enums
249
456
  # IMPROVE: reflect the enum names in the description
@@ -321,6 +528,8 @@ def _emit_endpoint(
321
528
  ctx: EmitOpenAPIContext,
322
529
  namespace: builder.SpecNamespace,
323
530
  endpoint: builder.SpecEndpoint,
531
+ endpoint_examples: list[builder.SpecEndpointExample],
532
+ endpoint_guides: list[builder.SpecGuide],
324
533
  ) -> None:
325
534
  assert namespace.endpoint is not None
326
535
  assert namespace.path[0] == "api"
@@ -364,11 +573,32 @@ def _emit_endpoint(
364
573
  description = f"**[External API-Endpoint]** <br/> {description}"
365
574
 
366
575
  path_cutoff = min(3, len(namespace.path) - 1)
576
+
367
577
  ctx.endpoint = EmitOpenAPIEndpoint(
368
578
  method=namespace.endpoint.method.lower(),
369
579
  tags=[tag_name],
370
580
  summary=f"{'/'.join(namespace.path[path_cutoff:])}",
371
581
  description=description,
582
+ is_beta=namespace.endpoint.is_beta,
583
+ stability_level=namespace.endpoint.stability_level,
584
+ examples=[
585
+ EmitOpenAPIEndpointExample(
586
+ ref_name=f"ex_{i}",
587
+ summary=example.summary,
588
+ description=example.description,
589
+ arguments=serialize_for_api(example.arguments),
590
+ data=serialize_for_api(example.data),
591
+ )
592
+ for i, example in enumerate(endpoint_examples)
593
+ ],
594
+ guides=[
595
+ EmitOpenAPIGuide(
596
+ ref_name=guide.ref_name,
597
+ title=guide.title,
598
+ html_content=guide.html_content,
599
+ )
600
+ for guide in endpoint_guides
601
+ ],
372
602
  )
373
603
 
374
604
 
@@ -436,6 +666,10 @@ def open_api_type(
436
666
  [open_api_type(ctx, p, config=config) for p in stype.parameters],
437
667
  description="TupleType",
438
668
  )
669
+ if stype.defn_type.name == builder.BaseTypeName.s_readonly_array:
670
+ return OpenAPIArrayType(
671
+ open_api_type(ctx, stype.parameters[0], config=config)
672
+ )
439
673
 
440
674
  # TODO: generics are not supported by OpenAPI
441
675
  # map to Free-Form Object and add description
@@ -460,5 +694,5 @@ def open_api_type(
460
694
  ctx.namespaces.add(stype.namespace)
461
695
  # external namespace resolution
462
696
  return OpenAPIRefType(
463
- source=f"{resolve_namespace_ref(ctx, stype.namespace, config=config)}/{stype.name}"
697
+ source=f"{resolve_namespace_ref(source_path=ctx.namespace.path, ref_path=stype.namespace.path, ref='/components/schema')}/{stype.name}"
464
698
  )
@@ -6,17 +6,17 @@ WORK-IN-PROGRESS, DON'T USE!
6
6
 
7
7
  from collections import defaultdict
8
8
  from dataclasses import dataclass, field
9
- from typing import TypeAlias
9
+
10
+ from pkgs.serialization_util import JsonValue
10
11
 
11
12
  from . import builder
12
- from .config import OpenAPIConfig
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
- GlobalContextInfo: TypeAlias = dict[str, str | dict[str, str]]
18
- TagGroupToNamedTags: TypeAlias = dict[str, str | list[str]]
19
- TagPathsToRef: TypeAlias = dict[str, dict[str, str]]
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]]
20
20
 
21
21
 
22
22
  @dataclass
@@ -44,6 +44,13 @@ class EmitOpenAPIServer:
44
44
  url: str
45
45
 
46
46
 
47
+ @dataclass(kw_only=True)
48
+ class EmitOpenAPIGuide:
49
+ ref_name: str
50
+ title: str
51
+ html_content: str
52
+
53
+
47
54
  @dataclass
48
55
  class EmitOpenAPIGlobalContext:
49
56
  version: str
@@ -57,12 +64,28 @@ class EmitOpenAPIGlobalContext:
57
64
  paths: list[EmitOpenAPIPath] = field(default_factory=list)
58
65
 
59
66
 
67
+ @dataclass(kw_only=True)
68
+ class EmitOpenAPIEndpointExample:
69
+ ref_name: str
70
+ summary: str
71
+ description: str
72
+ arguments: dict[str, JsonValue]
73
+ data: dict[str, JsonValue]
74
+
75
+
76
+ EmitOpenAPIStabilityLevel = builder.StabilityLevel
77
+
78
+
60
79
  @dataclass
61
80
  class EmitOpenAPIEndpoint:
62
81
  method: str
63
82
  tags: list[str]
64
83
  summary: str
65
84
  description: str
85
+ is_beta: bool
86
+ stability_level: EmitOpenAPIStabilityLevel | None
87
+ examples: list[EmitOpenAPIEndpointExample]
88
+ guides: list[EmitOpenAPIGuide]
66
89
 
67
90
 
68
91
  @dataclass
@@ -75,12 +98,8 @@ class EmitOpenAPIContext:
75
98
 
76
99
 
77
100
  def resolve_namespace_ref(
78
- ctx: EmitOpenAPIContext,
79
- namespace: builder.SpecNamespace,
80
- *,
81
- config: OpenAPIConfig,
101
+ *, source_path: list[str], ref_path: list[str], ref: str
82
102
  ) -> str:
83
- # TODO: Handle namespaces not in root directory
84
- if len(ctx.namespace.path) == 1:
85
- return f"{namespace.name}.yaml#/components/schema"
86
- return f"{config.static_url_path}/common/{namespace.name}.yaml#/components/schema"
103
+ to_root = "/".join(".." for _ in source_path)
104
+ location = "/".join(ref_path)
105
+ return f"{to_root}/common/{location}.yaml#{ref}"