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
@@ -2,18 +2,19 @@ import dataclasses
2
2
  import io
3
3
  import os
4
4
  from decimal import Decimal
5
- from typing import Any, Optional
5
+ from typing import Any
6
6
 
7
7
  from . import builder, util
8
+ from .builder import EndpointEmitType, EndpointSpecificPath, base_namespace_name
8
9
  from .config import PythonConfig
10
+ from .cross_output_links import get_path_links
11
+ from .emit_open_api_util import EmitOpenAPIStabilityLevel
9
12
 
10
13
  INDENT = " "
11
14
  LINE_BREAK = "\n"
12
15
  MODIFY_NOTICE = "# DO NOT MODIFY -- This file is generated by type_spec\n"
13
16
  # Turn excess line length warning and turn off ruff formatting
14
- LINT_HEADER = (
15
- "# flake8: noqa: F821\n# ruff: noqa: E402 Q003\n# fmt: off\n# isort: skip_file\n"
16
- )
17
+ LINT_HEADER = "# ruff: noqa: E402 Q003\n# fmt: off\n# isort: skip_file\n"
17
18
  LINT_FOOTER = "# fmt: on\n"
18
19
  ROUTE_NOTICE = """# Routes are generated from $endpoint specifications in the
19
20
  # type_spec API YAML files. Refer to the section on endpoints in the type_spec/README"""
@@ -34,10 +35,15 @@ QUEUED_BATCH_REQUEST_STYPE = builder.SpecTypeDefnObject(
34
35
  namespace=ASYNC_BATCH_TYPE_NAMESPACE, name="QueuedAsyncBatchRequest"
35
36
  )
36
37
 
38
+ CLIENT_CONFIG_TYPE_NAMESPACE = builder.SpecNamespace(name="client_config")
39
+ REQUEST_OPTIONS_STYPE = builder.SpecTypeDefnObject(
40
+ namespace=CLIENT_CONFIG_TYPE_NAMESPACE, name="RequestOptions"
41
+ )
42
+
37
43
 
38
44
  @dataclasses.dataclass(kw_only=True)
39
45
  class TrackingContext:
40
- namespace: Optional[builder.SpecNamespace] = None
46
+ namespace: builder.SpecNamespace | None = None
41
47
  namespaces: set[builder.SpecNamespace] = dataclasses.field(default_factory=set)
42
48
  names: set[str] = dataclasses.field(default_factory=set)
43
49
 
@@ -45,6 +51,7 @@ class TrackingContext:
45
51
  use_serial_string_enum: bool = False
46
52
  use_dataclass: bool = False
47
53
  use_serial_union: bool = False
54
+ use_serial_alias: bool = False
48
55
  use_missing: bool = False
49
56
  use_opaque_key: bool = False
50
57
 
@@ -115,26 +122,36 @@ def _check_type_match(stype: builder.SpecType, value: Any) -> bool:
115
122
  raise Exception("invalid type", stype, value)
116
123
 
117
124
 
118
- def _emit_value(ctx: TrackingContext, stype: builder.SpecType, value: Any) -> str:
125
+ def _emit_value(
126
+ ctx: TrackingContext, stype: builder.SpecType, value: Any, indent: int = 0
127
+ ) -> str:
119
128
  literal = builder.unwrap_literal_type(stype)
120
129
  if literal is not None:
121
130
  return _emit_value(ctx, literal.value_type, literal.value)
122
131
 
123
132
  if stype.is_base_type(builder.BaseTypeName.s_string):
124
- assert isinstance(value, str)
133
+ assert isinstance(value, str), (
134
+ f"Expected str value for {stype.name} but got {value}"
135
+ )
125
136
  return util.encode_common_string(value)
126
137
  elif stype.is_base_type(builder.BaseTypeName.s_integer):
127
- assert isinstance(value, int)
138
+ assert isinstance(value, int), (
139
+ f"Expected int value for {stype.name} but got {value}"
140
+ )
128
141
  return str(value)
129
142
  elif stype.is_base_type(builder.BaseTypeName.s_boolean):
130
- assert isinstance(value, bool)
143
+ assert isinstance(value, bool), (
144
+ f"Expected bool value for {stype.name} but got {value}"
145
+ )
131
146
  return "True" if value else "False"
132
147
  elif stype.is_base_type(builder.BaseTypeName.s_decimal) or stype.is_base_type(
133
148
  builder.BaseTypeName.s_lossy_decimal
134
149
  ):
135
150
  # Note that decimal requires the `!decimal 123.12` style notation in the YAML
136
151
  # file since PyYaml parses numbers as float, unfortuantely
137
- assert isinstance(value, (Decimal, int))
152
+ assert isinstance(value, (Decimal, int)), (
153
+ f"Expected decimal value for {stype.name} but got {value} (type: {type(value)})"
154
+ )
138
155
  if isinstance(value, int):
139
156
  # skip quotes for integers
140
157
  return f"Decimal({value})"
@@ -149,14 +166,14 @@ def _emit_value(ctx: TrackingContext, stype: builder.SpecType, value: Any) -> st
149
166
  key_type = stype.parameters[0]
150
167
  value_type = stype.parameters[1]
151
168
  return (
152
- "{\n "
153
- + ",\n ".join(
169
+ f"{{\n{INDENT * (indent + 1)}"
170
+ + f",\n{INDENT * (indent + 1)}".join(
154
171
  _emit_value(ctx, key_type, dkey)
155
172
  + ": "
156
- + _emit_value(ctx, value_type, dvalue)
173
+ + _emit_value(ctx, value_type, dvalue, indent=indent + 1)
157
174
  for dkey, dvalue in value.items()
158
175
  )
159
- + "\n}"
176
+ + f"\n{INDENT * indent}}}"
160
177
  )
161
178
 
162
179
  if stype.defn_type.is_base_type(builder.BaseTypeName.s_optional):
@@ -182,6 +199,34 @@ def _emit_value(ctx: TrackingContext, stype: builder.SpecType, value: Any) -> st
182
199
  return f"{refer_to(ctx, stype)}.{_resolve_enum_name(value, stype.name_case)}"
183
200
  elif isinstance(stype, builder.SpecTypeDefnAlias):
184
201
  return _emit_value(ctx, stype.alias, value)
202
+ elif isinstance(stype, builder.SpecTypeDefnObject):
203
+ assert isinstance(value, dict), (
204
+ f"Expected dict value for {stype.name} but got {value}"
205
+ )
206
+ if not stype.is_hashable:
207
+ raise Exception("invalid constant object type, non-hashable", value, stype)
208
+ obj_out = f"{refer_to(ctx, stype)}("
209
+ emitted_fields: set[str] = set()
210
+ for prop_name, prop in (stype.properties or {}).items():
211
+ if prop_name not in value:
212
+ continue
213
+ else:
214
+ value_to_emit = value[prop_name]
215
+ emitted_fields.add(prop_name)
216
+ py_name = python_field_name(prop.name, prop.name_case)
217
+ obj_out += f"\n{INDENT * (indent + 1)}{py_name}={_emit_value(ctx, prop.spec_type, value_to_emit, indent=indent + 1)},"
218
+ whitespace = f"\n{INDENT * indent}" if len(emitted_fields) > 0 else ""
219
+ obj_out += f"{whitespace})"
220
+
221
+ if emitted_fields != set(value.keys()):
222
+ raise Exception(
223
+ "invalid object type, extra fields found:",
224
+ value,
225
+ stype,
226
+ set(value.keys()) - emitted_fields,
227
+ )
228
+
229
+ return obj_out
185
230
 
186
231
  raise Exception("invalid constant type", value, stype)
187
232
 
@@ -219,12 +264,14 @@ def _emit_types_imports(*, out: io.StringIO, ctx: Context) -> None:
219
264
  out.write("import datetime # noqa: F401\n")
220
265
  out.write("from decimal import Decimal # noqa: F401\n")
221
266
  if ctx.use_enum:
222
- out.write("from pkgs.strenum_compat import StrEnum\n")
267
+ out.write("from enum import StrEnum\n")
223
268
  if ctx.use_dataclass:
224
269
  out.write("import dataclasses\n")
225
270
  out.write("from pkgs.serialization import serial_class\n")
226
271
  if ctx.use_serial_union:
227
272
  out.write("from pkgs.serialization import serial_union_annotation\n")
273
+ if ctx.use_serial_alias:
274
+ out.write("from pkgs.serialization import serial_alias_annotation\n")
228
275
  if ctx.use_serial_string_enum:
229
276
  out.write("from pkgs.serialization import serial_string_enum\n")
230
277
  if ctx.use_missing:
@@ -249,7 +296,7 @@ def _emit_types(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
249
296
  ):
250
297
  if (
251
298
  namespace.endpoint is not None
252
- and not namespace.endpoint.is_sdk
299
+ and namespace.endpoint.is_sdk == EndpointEmitType.EMIT_NOTHING
253
300
  and config.sdk_endpoints_only is True
254
301
  ):
255
302
  continue
@@ -340,16 +387,44 @@ def _emit_types(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
340
387
 
341
388
  ENDPOINT_METHOD = "ENDPOINT_METHOD"
342
389
  ENDPOINT_PATH = "ENDPOINT_PATH"
390
+ # will be removed in Q1 2025 when ENDPOINT_PATH is made api_endpoint-agnostic
391
+ # is used when the API call has multiple endpoints for the one endpoint that isn't equal to the top_namespace
392
+ ENDPOINT_PATH_ALTERNATE = "ENDPOINT_PATH_ALTERNATE"
393
+
394
+
395
+ def _get_epf_root(endpoint_specific_path: EndpointSpecificPath) -> str:
396
+ return endpoint_specific_path.root
343
397
 
344
398
 
345
399
  def _emit_namespace(ctx: Context, namespace: builder.SpecNamespace) -> None:
346
400
  endpoint = namespace.endpoint
347
401
  if endpoint is not None:
402
+ path_links = get_path_links(
403
+ ctx.builder.cross_output_paths,
404
+ namespace,
405
+ current_path_type="Python",
406
+ endpoint=endpoint,
407
+ )
408
+ if path_links != "":
409
+ ctx.out.write("\n")
410
+ ctx.out.write(path_links)
411
+
348
412
  ctx.out.write("\n")
349
413
  ctx.out.write(f'{ENDPOINT_METHOD} = "{endpoint.method.upper()}"\n')
350
- ctx.out.write(
351
- f'{ENDPOINT_PATH} = "{endpoint.path_root}/{endpoint.path_dirname}/{endpoint.path_basename}"\n'
352
- )
414
+ for endpoint_specific_path in sorted(
415
+ endpoint.path_per_api_endpoint.values(), key=_get_epf_root
416
+ ):
417
+ endpoint_path_name = ENDPOINT_PATH
418
+
419
+ if (
420
+ len(endpoint.path_per_api_endpoint.keys()) > 1
421
+ and endpoint_specific_path.root != ctx.builder.top_namespace
422
+ ):
423
+ endpoint_path_name = ENDPOINT_PATH_ALTERNATE
424
+ ctx.names.add(ENDPOINT_PATH_ALTERNATE)
425
+ ctx.out.write(
426
+ f'{endpoint_path_name} = "{endpoint_specific_path.path_root}/{endpoint_specific_path.path_dirname}/{endpoint_specific_path.path_basename}"\n'
427
+ )
353
428
 
354
429
  ctx.names.add(ENDPOINT_METHOD)
355
430
  ctx.names.add(ENDPOINT_PATH)
@@ -439,8 +514,23 @@ def _emit_endpoint_invocation_function_signature(
439
514
  else []
440
515
  ) + (extra_params if extra_params is not None else [])
441
516
 
442
- assert endpoint.function is not None
443
- function_name = endpoint.function.split(".")[-1]
517
+ request_options_property = builder.SpecProperty(
518
+ name="_request_options",
519
+ label="_request_options",
520
+ spec_type=REQUEST_OPTIONS_STYPE,
521
+ extant=builder.PropertyExtant.optional,
522
+ convert_value=builder.PropertyConvertValue.auto,
523
+ name_case=builder.NameCase.convert,
524
+ default=None,
525
+ has_default=True,
526
+ desc=None,
527
+ )
528
+ all_arguments.append(request_options_property)
529
+
530
+ # All endpoints share a function name
531
+ function = endpoint.path_per_api_endpoint[endpoint.default_endpoint_key].function
532
+ assert function is not None
533
+ function_name = function.split(".")[-1]
444
534
  ctx.out.write(
445
535
  f"""
446
536
  def {function_name}(
@@ -486,7 +576,10 @@ def _emit_async_batch_invocation_function(
486
576
  endpoint = namespace.endpoint
487
577
  if endpoint is None:
488
578
  return
489
- if endpoint.async_batch_path is None or not endpoint.is_sdk:
579
+ if (
580
+ endpoint.async_batch_path is None
581
+ or endpoint.is_sdk != EndpointEmitType.EMIT_ENDPOINT
582
+ ):
490
583
  return
491
584
 
492
585
  ctx.out.write("\n")
@@ -568,7 +661,10 @@ def _emit_endpoint_invocation_function(
568
661
  endpoint = namespace.endpoint
569
662
  if endpoint is None:
570
663
  return
571
- if not endpoint.is_sdk or endpoint.is_beta:
664
+ if (
665
+ endpoint.is_sdk != EndpointEmitType.EMIT_ENDPOINT
666
+ or endpoint.stability_level == EmitOpenAPIStabilityLevel.draft
667
+ ):
572
668
  return
573
669
 
574
670
  ctx.out.write("\n")
@@ -601,6 +697,7 @@ def _emit_endpoint_invocation_function(
601
697
  method={refer_to(ctx=ctx, stype=endpoint_method_stype)},
602
698
  endpoint={refer_to(ctx=ctx, stype=endpoint_path_stype)},
603
699
  args=args,
700
+ request_options=_request_options,
604
701
  )
605
702
  return self.do_request(api_request=api_request, return_type={refer_to(ctx=ctx, stype=data_type)})"""
606
703
  )
@@ -695,39 +792,43 @@ def _emit_properties(
695
792
  if len(properties) > 0:
696
793
 
697
794
  def write_field(prop: builder.SpecProperty) -> None:
795
+ stype = prop.spec_type
698
796
  if prop.name_case == builder.NameCase.preserve:
699
797
  unconverted_keys.add(prop.name)
700
798
  py_name = python_field_name(prop.name, prop.name_case)
701
799
 
702
800
  if prop.convert_value == builder.PropertyConvertValue.no_convert:
703
801
  unconverted_values.add(py_name)
704
- elif not prop.spec_type.is_value_converted():
802
+ elif not stype.is_value_converted():
705
803
  assert prop.convert_value == builder.PropertyConvertValue.auto
706
804
  unconverted_values.add(py_name)
707
- if prop.spec_type.is_value_to_string():
805
+ if stype.is_value_to_string():
708
806
  to_string_values.add(py_name)
709
807
 
710
808
  if prop.parse_require:
711
809
  parse_require.add(py_name)
712
810
 
713
- ref_type = refer_to(ctx, prop.spec_type)
811
+ ref_type = refer_to(ctx, stype)
714
812
  default = None
715
813
  if prop.extant == builder.PropertyExtant.missing:
716
814
  ref_type = f"MissingType[{ref_type}]"
717
815
  default = "MISSING_SENTRY"
718
816
  ctx.use_missing = True
719
817
  elif prop.extant == builder.PropertyExtant.optional:
720
- ref_type = f"typing.Optional[{ref_type}]"
818
+ if isinstance(
819
+ stype, builder.SpecTypeInstance
820
+ ) and stype.defn_type.is_base_type(builder.BaseTypeName.s_optional):
821
+ pass # base type already adds the None union
822
+ elif ref_type == "None":
823
+ pass # no need to add a None union to a none type
824
+ else:
825
+ ref_type = f"{ref_type} | None"
721
826
  default = "None"
722
827
  elif prop.has_default:
723
- default = _emit_value(ctx, prop.spec_type, prop.default)
828
+ default = _emit_value(ctx, stype, prop.default)
724
829
  if (
725
- isinstance(prop.spec_type, builder.SpecTypeInstance)
726
- and (
727
- prop.spec_type.defn_type.is_base_type(
728
- builder.BaseTypeName.s_list
729
- )
730
- )
830
+ isinstance(stype, builder.SpecTypeInstance)
831
+ and (stype.defn_type.is_base_type(builder.BaseTypeName.s_list))
731
832
  and default == "[]"
732
833
  ):
733
834
  default = "dataclasses.field(default_factory=list)"
@@ -779,7 +880,17 @@ def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
779
880
  return
780
881
 
781
882
  if isinstance(stype, builder.SpecTypeDefnAlias):
782
- ctx.out.write(f"{stype.name} = {refer_to(ctx, stype.alias)}\n")
883
+ ctx.use_serial_alias = True
884
+ ctx.out.write(f"{stype.name} = typing.Annotated[\n")
885
+ ctx.out.write(f"{INDENT}{refer_to(ctx, stype.alias)},\n")
886
+ ctx.out.write(f"{INDENT}serial_alias_annotation(\n")
887
+ ctx.out.write(
888
+ f"{INDENT}named_type_path={util.encode_common_string(_named_type_path(ctx, stype))},\n"
889
+ )
890
+ if stype.is_dynamic_allowed():
891
+ ctx.out.write(f"{INDENT}is_dynamic_allowed=True,\n")
892
+ ctx.out.write(f"{INDENT}),\n")
893
+ ctx.out.write("]\n")
783
894
  return
784
895
 
785
896
  if isinstance(stype, builder.SpecTypeDefnUnion):
@@ -790,6 +901,8 @@ def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
790
901
  ctx.out.write(
791
902
  f"{INDENT}named_type_path={util.encode_common_string(_named_type_path(ctx, stype))},\n"
792
903
  )
904
+ if stype.is_dynamic_allowed():
905
+ ctx.out.write(f"{INDENT}is_dynamic_allowed=True,\n")
793
906
  if stype.discriminator is not None:
794
907
  ctx.out.write(
795
908
  f"{INDENT * 2}discriminator={util.encode_common_string(stype.discriminator)},\n"
@@ -818,7 +931,7 @@ def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
818
931
  if not stype.base.is_base:
819
932
  base_class = f"({refer_to(ctx, stype.base)})"
820
933
  elif len(generics) > 0:
821
- base_class = f"(typing.Generic[{', '.join(generics)}])"
934
+ base_class = f"[{', '.join(generics)}]"
822
935
  class_out.write(f"class {stype.name}{base_class}:\n")
823
936
 
824
937
  emitted_properties_metadata = _emit_type_properties(
@@ -836,6 +949,8 @@ def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
836
949
  ctx.out.write(
837
950
  f"{INDENT}named_type_path={util.encode_common_string(_named_type_path(ctx, stype))},\n"
838
951
  )
952
+ if stype.is_dynamic_allowed():
953
+ ctx.out.write(f"{INDENT}is_dynamic_allowed=True,\n")
839
954
 
840
955
  def write_values(key: str, values: set[str]) -> None:
841
956
  if len(values) == 0:
@@ -852,13 +967,19 @@ def _emit_type(ctx: Context, stype: builder.SpecType) -> None:
852
967
 
853
968
  # Emit dataclass decorator
854
969
  dataclass = "@dataclasses.dataclass"
855
- dc_args = []
970
+ refer_to(
971
+ ctx,
972
+ builder.SpecTypeDefnAlias(
973
+ namespace=ctx.builder.namespaces[base_namespace_name], name="ENABLE_SLOTS"
974
+ ),
975
+ )
976
+ dc_args = ["slots=base_t.ENABLE_SLOTS"]
856
977
  if stype.is_kw_only():
857
978
  dc_args.append("kw_only=True")
858
979
  if stype.is_hashable:
859
980
  dc_args.extend(["frozen=True", "eq=True"])
860
981
  if len(dc_args) > 0:
861
- dataclass += f"({', '.join(dc_args)})"
982
+ dataclass += f"({', '.join(dc_args)}) # type: ignore[literal-required]"
862
983
 
863
984
  ctx.out.write(f"{dataclass}\n")
864
985
  ctx.out.write(class_out.getvalue())
@@ -903,13 +1024,22 @@ base_name_map = {
903
1024
 
904
1025
  def refer_to(ctx: TrackingContext, stype: builder.SpecType) -> str:
905
1026
  if isinstance(stype, builder.SpecTypeInstance):
906
- params = ", ".join([refer_to(ctx, p) for p in stype.parameters])
1027
+ params = [refer_to(ctx, p) for p in stype.parameters]
1028
+
1029
+ if stype.defn_type.is_base_type(builder.BaseTypeName.s_union):
1030
+ if len(stype.parameters) == 1:
1031
+ return f"typing.Union[{params[0]}]"
1032
+ return " | ".join(params)
907
1033
 
908
1034
  if stype.defn_type.is_base_type(builder.BaseTypeName.s_readonly_array):
909
- assert len(stype.parameters) == 1, "Read Only Array takes one parameter"
910
- params = f"{params}, ..."
1035
+ assert len(params) == 1, "Read Only Array takes one parameter"
1036
+ return f"tuple[{params[0]}, ...]"
911
1037
 
912
- return f"{refer_to(ctx, stype.defn_type)}[{params}]"
1038
+ if stype.defn_type.is_base_type(builder.BaseTypeName.s_optional):
1039
+ assert len(params) == 1, "Optional only takes one parameter"
1040
+ return f"{params[0]} | None"
1041
+
1042
+ return f"{refer_to(ctx, stype.defn_type)}[{', '.join(params)}]"
913
1043
 
914
1044
  if isinstance(stype, builder.SpecTypeLiteralWrapper):
915
1045
  return _emit_value(ctx, stype.value_type, stype.value)
@@ -935,23 +1065,21 @@ def refer_to(ctx: TrackingContext, stype: builder.SpecType) -> str:
935
1065
  SpecEndpoint = builder.SpecEndpoint
936
1066
 
937
1067
 
938
- def _route_identifier(endpoint: builder.SpecEndpoint) -> tuple[str, str, str]:
939
- return (endpoint.path_dirname, endpoint.path_basename, endpoint.method)
940
-
941
-
942
1068
  def _emit_routes(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
943
1069
  for endpoint_root in builder.api_endpoints:
944
1070
  endpoints: list[SpecEndpoint] = []
945
1071
  output = config.routes_output.get(endpoint_root)
946
1072
  if output is None:
947
1073
  continue
1074
+ last_endpoint: SpecEndpoint | None = None
948
1075
  for namespace in builder.namespaces.values():
949
1076
  endpoint = namespace.endpoint
1077
+ last_endpoint = endpoint
950
1078
  if endpoint is None:
951
1079
  continue
952
- if endpoint.root != endpoint_root:
1080
+ if endpoint_root not in endpoint.path_per_api_endpoint:
953
1081
  continue
954
- if endpoint.function is None:
1082
+ if endpoint.path_per_api_endpoint[endpoint_root].function is None:
955
1083
  continue
956
1084
 
957
1085
  endpoints.append(endpoint)
@@ -964,6 +1092,15 @@ def _emit_routes(*, builder: builder.SpecBuilder, config: PythonConfig) -> None:
964
1092
  from main.site.framework.types import StaticRouteType
965
1093
  """
966
1094
  )
1095
+
1096
+ def _route_identifier(endpoint: SpecEndpoint) -> tuple[str, str, str]:
1097
+ endpoint_specific_path = endpoint.path_per_api_endpoint[endpoint_root]
1098
+ return (
1099
+ endpoint_specific_path.path_dirname,
1100
+ endpoint_specific_path.path_basename,
1101
+ endpoint.method,
1102
+ )
1103
+
967
1104
  sorted_endpoints = sorted(endpoints, key=_route_identifier)
968
1105
 
969
1106
  assert len(endpoints) == len(set(map(_route_identifier, endpoints))), (
@@ -972,17 +1109,21 @@ from main.site.framework.types import StaticRouteType
972
1109
 
973
1110
  path_set = set()
974
1111
  for endpoint in sorted_endpoints:
975
- assert endpoint.function
976
- func_bits = endpoint.function.split(".")
1112
+ last_endpoint = endpoint
1113
+ endpoint_function_path = endpoint.path_per_api_endpoint[endpoint_root]
1114
+ assert endpoint_function_path.function
1115
+ func_bits = endpoint_function_path.function.split(".")
977
1116
  path = ".".join(func_bits[:-1])
978
1117
  if path in path_set:
979
1118
  continue
980
1119
  path_set.add(path)
981
1120
  static_out.write(f"import {path}\n")
982
1121
 
1122
+ assert last_endpoint is not None
1123
+
983
1124
  static_out.write(
984
1125
  f"""
985
- ROUTE_PREFIX = "/{endpoint.path_root}"
1126
+ ROUTE_PREFIX = "/{last_endpoint.path_per_api_endpoint[endpoint_root].path_root}"
986
1127
 
987
1128
  ROUTES: list[StaticRouteType] = [
988
1129
  """
@@ -996,20 +1137,21 @@ ROUTES: list[StaticRouteType] = [
996
1137
 
997
1138
  from main.site.framework.types import DynamicRouteType
998
1139
 
999
- ROUTE_PREFIX = "/{endpoint.path_root}"
1140
+ ROUTE_PREFIX = "/{last_endpoint.path_per_api_endpoint[endpoint_root].path_root}"
1000
1141
 
1001
1142
  ROUTES: list[DynamicRouteType] = [
1002
1143
  """
1003
1144
  )
1004
1145
 
1005
1146
  for endpoint in sorted_endpoints:
1147
+ endpoint_function_path = endpoint.path_per_api_endpoint[endpoint_root]
1006
1148
  dynamic_out.write(
1007
- f'{INDENT}("{endpoint.path_dirname}/{endpoint.path_basename}", "{endpoint.function}", ["{endpoint.method.upper()}"]),\n'
1149
+ f'{INDENT}("{endpoint_function_path.path_dirname}/{endpoint_function_path.path_basename}", "{endpoint_function_path.function}", ["{endpoint.method.upper()}"]),\n'
1008
1150
  )
1009
1151
 
1010
- assert endpoint.function
1152
+ assert endpoint_function_path.function
1011
1153
  static_out.write(
1012
- f'{INDENT}("{endpoint.path_dirname}/{endpoint.path_basename}", {endpoint.function}, ["{endpoint.method.upper()}"]),\n'
1154
+ f'{INDENT}("{endpoint_function_path.path_dirname}/{endpoint_function_path.path_basename}", {endpoint_function_path.function}, ["{endpoint.method.upper()}"]),\n'
1013
1155
  )
1014
1156
 
1015
1157
  dynamic_out.write(f"{MODIFY_NOTICE}]\n")
@@ -1025,15 +1167,21 @@ def _emit_namespace_imports(
1025
1167
  *,
1026
1168
  out: io.StringIO,
1027
1169
  namespaces: set[builder.SpecNamespace],
1028
- from_namespace: Optional[builder.SpecNamespace],
1170
+ from_namespace: builder.SpecNamespace | None,
1029
1171
  config: PythonConfig,
1172
+ skip_non_sdk: bool = False,
1030
1173
  ) -> None:
1031
1174
  for ns in sorted(
1032
1175
  namespaces,
1033
1176
  key=lambda name: _resolve_namespace_name(name),
1034
1177
  ):
1178
+ if (
1179
+ skip_non_sdk
1180
+ and ns.endpoint is not None
1181
+ and ns.endpoint.is_sdk != EndpointEmitType.EMIT_ENDPOINT
1182
+ ):
1183
+ continue
1035
1184
  resolved = _resolve_namespace_name(ns)
1036
- ref = _resolve_namespace_ref(ns)
1037
1185
  if ns.endpoint is not None:
1038
1186
  import_alias = "_".join(ns.path[2:]) + "_t"
1039
1187
  out.write(
@@ -1057,8 +1205,8 @@ def _emit_id_source(*, builder: builder.SpecBuilder, config: PythonConfig) -> No
1057
1205
  return None
1058
1206
  enum_out = io.StringIO()
1059
1207
  enum_out.write(f"{LINT_HEADER}{MODIFY_NOTICE}\n")
1060
- enum_out.write("from typing import Literal, Union\n")
1061
- enum_out.write("from pkgs.strenum_compat import StrEnum\n")
1208
+ enum_out.write("import typing\n")
1209
+ enum_out.write("from enum import StrEnum\n")
1062
1210
 
1063
1211
  ctx = TrackingContext()
1064
1212
  # In this context the propername
@@ -1074,11 +1222,11 @@ def _emit_id_source(*, builder: builder.SpecBuilder, config: PythonConfig) -> No
1074
1222
  known_keys = []
1075
1223
  enum_out.write("\nENUM_MAP: dict[str, type[StrEnum]] = {\n")
1076
1224
  for key in sorted(named_enums.keys()):
1077
- enum_out.write(f'"{key}": {named_enums[key]},\n')
1078
- known_keys.append(f'Literal["{key}"]')
1225
+ enum_out.write(f'{INDENT}"{key}": {named_enums[key]},\n')
1226
+ known_keys.append(f'"{key}"')
1079
1227
  enum_out.write(f"}}\n{MODIFY_NOTICE}\n")
1080
1228
 
1081
- enum_out.write(f"\nKnownEnumsType = Union[\n{INDENT}")
1229
+ enum_out.write(f"\nKnownEnumsType = typing.Literal[\n{INDENT}")
1082
1230
  enum_out.write(f",\n{INDENT}".join(known_keys))
1083
1231
  enum_out.write(f"\n]\n{MODIFY_NOTICE}\n")
1084
1232
 
@@ -1095,21 +1243,36 @@ def _emit_api_stubs(*, builder: builder.SpecBuilder, config: PythonConfig) -> No
1095
1243
 
1096
1244
  if endpoint is None:
1097
1245
  continue
1098
- if endpoint.root != endpoint_root:
1246
+ if endpoint_root not in endpoint.path_per_api_endpoint:
1099
1247
  continue
1100
- if endpoint.function is None:
1248
+
1249
+ endpoint_function = endpoint.path_per_api_endpoint[endpoint_root].function
1250
+ if endpoint_function is None:
1101
1251
  continue
1102
1252
 
1103
- module_dir, file_name, func_name = endpoint.function.rsplit(".", 2)
1253
+ module_dir, file_name, _func_name = endpoint_function.rsplit(".", 2)
1104
1254
  module_path = os.path.abspath(module_dir.replace(".", "/"))
1105
1255
  api_stub_file = f"{module_path}/{file_name}.py"
1106
1256
  if os.path.isfile(api_stub_file):
1107
1257
  continue
1108
- _create_api_stub(api_stub_file, file_name, endpoint, config)
1258
+ _create_api_stub(
1259
+ api_stub_file=api_stub_file,
1260
+ file_name=file_name,
1261
+ endpoint=endpoint,
1262
+ config=config,
1263
+ endpoint_root=endpoint_root,
1264
+ top_namespace=builder.top_namespace,
1265
+ )
1109
1266
 
1110
1267
 
1111
1268
  def _create_api_stub(
1112
- api_stub_file: str, file_name: str, endpoint: SpecEndpoint, config: PythonConfig
1269
+ *,
1270
+ api_stub_file: str,
1271
+ file_name: str,
1272
+ endpoint: SpecEndpoint,
1273
+ config: PythonConfig,
1274
+ endpoint_root: str,
1275
+ top_namespace: str,
1113
1276
  ) -> None:
1114
1277
  assert (
1115
1278
  endpoint.method == builder.RouteMethod.post
@@ -1117,7 +1280,13 @@ def _create_api_stub(
1117
1280
  or endpoint.method == builder.RouteMethod.delete
1118
1281
  or endpoint.method == builder.RouteMethod.patch
1119
1282
  )
1120
- api_out = _create_api_function(file_name, endpoint, config)
1283
+ api_out = _create_api_function(
1284
+ file_name=file_name,
1285
+ endpoint=endpoint,
1286
+ config=config,
1287
+ endpoint_root=endpoint_root,
1288
+ top_namespace=top_namespace,
1289
+ )
1121
1290
  util.rewrite_file(api_stub_file, api_out.getvalue())
1122
1291
 
1123
1292
 
@@ -1126,15 +1295,22 @@ WRAP_ARGS_END = "\n"
1126
1295
 
1127
1296
 
1128
1297
  def _create_api_function(
1129
- file_name: str, endpoint: SpecEndpoint, config: PythonConfig
1298
+ *,
1299
+ file_name: str,
1300
+ endpoint: SpecEndpoint,
1301
+ config: PythonConfig,
1302
+ endpoint_root: str,
1303
+ top_namespace: str,
1130
1304
  ) -> io.StringIO:
1305
+ endpoint_specific_path = endpoint.path_per_api_endpoint[endpoint_root]
1306
+ assert endpoint_specific_path is not None
1131
1307
  api_out = io.StringIO()
1132
1308
  python_api_type_root = f"{config.types_package}.api"
1133
- dot_dirname = endpoint.path_dirname.replace("/", ".")
1309
+ dot_dirname = endpoint_specific_path.path_dirname.replace("/", ".")
1134
1310
  api_import = (
1135
1311
  f"{python_api_type_root}.{dot_dirname}.{file_name}"
1136
1312
  if dot_dirname != ""
1137
- else f"{python_api_type_root}.{endpoint.path_basename}"
1313
+ else f"{python_api_type_root}.{endpoint_specific_path.path_basename}"
1138
1314
  )
1139
1315
 
1140
1316
  if endpoint.method == builder.RouteMethod.post:
@@ -1146,7 +1322,20 @@ def _create_api_function(
1146
1322
  elif endpoint.method == builder.RouteMethod.patch:
1147
1323
  validated_method = "validated_patch"
1148
1324
 
1149
- ruff_requires_wrap = len(endpoint.path_basename) > 14
1325
+ ruff_requires_wrap = len(endpoint_specific_path.path_basename) > 14
1326
+
1327
+ account_type = (
1328
+ endpoint_specific_path.root
1329
+ if endpoint_specific_path.root not in ["external", "portal"]
1330
+ else "materials"
1331
+ )
1332
+
1333
+ endpoint_path_name = (
1334
+ ENDPOINT_PATH_ALTERNATE
1335
+ if len(endpoint.path_per_api_endpoint.keys()) > 1
1336
+ and endpoint_specific_path.root != top_namespace
1337
+ else ENDPOINT_PATH
1338
+ )
1150
1339
 
1151
1340
  api_out.write(
1152
1341
  f"""import {api_import} as api
@@ -1154,8 +1343,8 @@ from main.db.session import Session, SessionMaker
1154
1343
  from main.site.decorators import APIError, APIResponse, {validated_method}
1155
1344
 
1156
1345
 
1157
- @{validated_method}(api.ENDPOINT_PATH, "{endpoint.root}", api.Arguments)
1158
- def {endpoint.path_basename}({WRAP_ARGS_START if ruff_requires_wrap else ""}args: api.Arguments, client_sm: SessionMaker{WRAP_ARGS_END if ruff_requires_wrap else ""}) -> APIResponse[api.Data]:
1346
+ @{validated_method}(api.{endpoint_path_name}, "{account_type}", api.Arguments)
1347
+ def {endpoint_specific_path.path_basename}({WRAP_ARGS_START if ruff_requires_wrap else ""}args: api.Arguments, client_sm: SessionMaker{WRAP_ARGS_END if ruff_requires_wrap else ""}) -> APIResponse[api.Data]:
1159
1348
  with Session(client_sm) as session:
1160
1349
  # return APIResponse(data=api.Data())
1161
1350
  pass
@@ -1177,7 +1366,7 @@ def _emit_api_argument_lookup(
1177
1366
  for endpoint_root in builder.api_endpoints:
1178
1367
  routes_output = config.routes_output[endpoint_root]
1179
1368
 
1180
- imports = ["import typing", "import dataclasses"]
1369
+ imports = ["import dataclasses"]
1181
1370
  mappings = []
1182
1371
  for namespace in sorted(
1183
1372
  builder.namespaces.values(),
@@ -1187,11 +1376,11 @@ def _emit_api_argument_lookup(
1187
1376
 
1188
1377
  if endpoint is None:
1189
1378
  continue
1190
- if endpoint.root != endpoint_root:
1379
+ if endpoint_root not in endpoint.path_per_api_endpoint:
1191
1380
  continue
1192
- if endpoint.function is None:
1381
+ if endpoint.path_per_api_endpoint[endpoint_root].function is None:
1193
1382
  continue
1194
- if "Arguments" not in namespace.types:
1383
+ if "Arguments" not in namespace.types or "Data" not in namespace.types:
1195
1384
  continue
1196
1385
 
1197
1386
  import_alias = "_".join(namespace.path[1:])
@@ -1215,7 +1404,7 @@ def _emit_api_argument_lookup(
1215
1404
  mapping += f"{INDENT}{INDENT}route_group={route_group},\n"
1216
1405
  mapping += f"{INDENT}{INDENT}account_type={account_type},\n"
1217
1406
  mapping += f"{INDENT}{INDENT}route={import_alias}.ENDPOINT_PATH,\n"
1218
- mapping += f'{INDENT}{INDENT}handler="{endpoint.function}",\n'
1407
+ mapping += f'{INDENT}{INDENT}handler="{endpoint.path_per_api_endpoint[endpoint_root].function}",\n'
1219
1408
  mapping += f"{INDENT}{INDENT}method={import_alias}.ENDPOINT_METHOD,\n"
1220
1409
  mapping += f"{INDENT})"
1221
1410
  mappings.append(mapping)
@@ -1226,9 +1415,6 @@ def _emit_api_argument_lookup(
1226
1415
  argument_lookup_out.write(
1227
1416
  f"""{LINE_BREAK.join(imports)}
1228
1417
 
1229
- AT = typing.TypeVar("AT")
1230
- DT = typing.TypeVar("DT")
1231
-
1232
1418
 
1233
1419
  @dataclasses.dataclass(kw_only=True, frozen=True)
1234
1420
  class ApiEndpointKey:
@@ -1237,7 +1423,7 @@ class ApiEndpointKey:
1237
1423
 
1238
1424
 
1239
1425
  @dataclasses.dataclass(kw_only=True)
1240
- class ApiEndpointSpec(typing.Generic[AT, DT]):
1426
+ class ApiEndpointSpec[AT, DT]:
1241
1427
  route: str
1242
1428
  arguments_type: type[AT]
1243
1429
  data_type: type[DT]
@@ -1267,10 +1453,10 @@ CLIENT_CLASS_IMPORTS = [
1267
1453
  "import dataclasses",
1268
1454
  ]
1269
1455
  ASYNC_BATCH_PROCESSOR_FILENAME = "async_batch_processor"
1270
- ASYNC_BATCH_PROCESSOR_IMPORTS = [
1456
+ ASYNC_BATCH_PROCESSOR_BASE_IMPORTS = [
1271
1457
  "import uuid",
1272
1458
  "from abc import ABC, abstractmethod",
1273
- "from pkgs.serialization_util.serialization_helpers import serialize_for_api",
1459
+ "from pkgs.serialization_util import serialize_for_api",
1274
1460
  ]
1275
1461
 
1276
1462
 
@@ -1305,8 +1491,11 @@ def _emit_async_batch_processor(
1305
1491
  config=config,
1306
1492
  )
1307
1493
 
1494
+ imports = ASYNC_BATCH_PROCESSOR_BASE_IMPORTS.copy()
1495
+ if ctx.use_dataclass:
1496
+ imports.append("import dataclasses")
1308
1497
  async_batch_processor_out.write(
1309
- f"""{LINE_BREAK.join(ASYNC_BATCH_PROCESSOR_IMPORTS)}
1498
+ f"""{LINE_BREAK.join(imports)}
1310
1499
 
1311
1500
 
1312
1501
  class AsyncBatchProcessorBase(ABC):
@@ -1355,6 +1544,7 @@ def _emit_client_class(
1355
1544
  namespaces=ctx.namespaces,
1356
1545
  from_namespace=None,
1357
1546
  config=config,
1547
+ skip_non_sdk=True,
1358
1548
  )
1359
1549
 
1360
1550
  client_base_out.write(
@@ -1368,6 +1558,7 @@ class APIRequest:
1368
1558
  method: str
1369
1559
  endpoint: str
1370
1560
  args: typing.Any
1561
+ request_options: {refer_to(ctx=ctx, stype=REQUEST_OPTIONS_STYPE)} | None = None
1371
1562
 
1372
1563
 
1373
1564
  class ClientMethods(ABC):