UncountablePythonSDK 0.0.82__py3-none-any.whl → 0.0.132__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of UncountablePythonSDK might be problematic. Click here for more details.

Files changed (298) hide show
  1. docs/conf.py +54 -7
  2. docs/index.md +107 -4
  3. docs/integration_examples/create_ingredient.md +43 -0
  4. docs/integration_examples/create_output.md +56 -0
  5. docs/integration_examples/index.md +6 -0
  6. docs/justfile +2 -2
  7. docs/requirements.txt +6 -4
  8. examples/basic_auth.py +7 -0
  9. examples/create_ingredient_sdk.py +34 -0
  10. examples/download_files.py +26 -0
  11. examples/integration-server/jobs/materials_auto/concurrent_cron.py +11 -0
  12. examples/integration-server/jobs/materials_auto/example_cron.py +3 -0
  13. examples/integration-server/jobs/materials_auto/example_http.py +47 -0
  14. examples/integration-server/jobs/materials_auto/example_instrument.py +100 -0
  15. examples/integration-server/jobs/materials_auto/example_parse.py +140 -0
  16. examples/integration-server/jobs/materials_auto/example_predictions.py +61 -0
  17. examples/integration-server/jobs/materials_auto/example_runsheet_wh.py +39 -0
  18. examples/integration-server/jobs/materials_auto/example_wh.py +17 -9
  19. examples/integration-server/jobs/materials_auto/profile.yaml +61 -0
  20. examples/integration-server/pyproject.toml +10 -10
  21. examples/oauth.py +7 -0
  22. examples/set_recipe_metadata_file.py +1 -1
  23. examples/upload_files.py +1 -2
  24. pkgs/argument_parser/__init__.py +8 -0
  25. pkgs/argument_parser/_is_namedtuple.py +3 -0
  26. pkgs/argument_parser/argument_parser.py +196 -63
  27. pkgs/filesystem_utils/__init__.py +1 -0
  28. pkgs/filesystem_utils/_blob_session.py +144 -0
  29. pkgs/filesystem_utils/_gdrive_session.py +5 -5
  30. pkgs/filesystem_utils/_s3_session.py +2 -1
  31. pkgs/filesystem_utils/_sftp_session.py +6 -3
  32. pkgs/filesystem_utils/file_type_utils.py +30 -10
  33. pkgs/serialization/__init__.py +7 -2
  34. pkgs/serialization/annotation.py +64 -0
  35. pkgs/serialization/missing_sentry.py +1 -1
  36. pkgs/serialization/opaque_key.py +1 -1
  37. pkgs/serialization/serial_alias.py +47 -0
  38. pkgs/serialization/serial_class.py +40 -48
  39. pkgs/serialization/serial_generic.py +16 -0
  40. pkgs/serialization/serial_union.py +16 -16
  41. pkgs/serialization_util/__init__.py +6 -0
  42. pkgs/serialization_util/dataclasses.py +14 -0
  43. pkgs/serialization_util/serialization_helpers.py +15 -5
  44. pkgs/type_spec/actions_registry/__main__.py +0 -4
  45. pkgs/type_spec/actions_registry/emit_typescript.py +2 -4
  46. pkgs/type_spec/builder.py +248 -70
  47. pkgs/type_spec/builder_types.py +9 -0
  48. pkgs/type_spec/config.py +40 -7
  49. pkgs/type_spec/cross_output_links.py +99 -0
  50. pkgs/type_spec/emit_open_api.py +121 -34
  51. pkgs/type_spec/emit_open_api_util.py +5 -5
  52. pkgs/type_spec/emit_python.py +277 -86
  53. pkgs/type_spec/emit_typescript.py +102 -29
  54. pkgs/type_spec/emit_typescript_util.py +66 -10
  55. pkgs/type_spec/load_types.py +16 -3
  56. pkgs/type_spec/non_discriminated_union_exceptions.py +14 -0
  57. pkgs/type_spec/open_api_util.py +29 -4
  58. pkgs/type_spec/parts/base.py.prepart +11 -8
  59. pkgs/type_spec/parts/base.ts.prepart +4 -0
  60. pkgs/type_spec/type_info/__main__.py +3 -1
  61. pkgs/type_spec/type_info/emit_type_info.py +115 -22
  62. pkgs/type_spec/ui_entry_actions/__init__.py +4 -0
  63. pkgs/type_spec/ui_entry_actions/generate_ui_entry_actions.py +308 -0
  64. pkgs/type_spec/util.py +3 -3
  65. pkgs/type_spec/value_spec/__main__.py +26 -9
  66. pkgs/type_spec/value_spec/convert_type.py +18 -0
  67. pkgs/type_spec/value_spec/emit_python.py +13 -3
  68. pkgs/type_spec/value_spec/types.py +1 -1
  69. uncountable/core/async_batch.py +1 -1
  70. uncountable/core/client.py +133 -34
  71. uncountable/core/environment.py +3 -3
  72. uncountable/core/file_upload.py +39 -15
  73. uncountable/integration/cli.py +116 -23
  74. uncountable/integration/construct_client.py +3 -3
  75. uncountable/integration/executors/executors.py +12 -2
  76. uncountable/integration/executors/generic_upload_executor.py +66 -14
  77. uncountable/integration/http_server/__init__.py +5 -0
  78. uncountable/integration/http_server/types.py +69 -0
  79. uncountable/integration/job.py +192 -7
  80. uncountable/integration/queue_runner/command_server/__init__.py +4 -0
  81. uncountable/integration/queue_runner/command_server/command_client.py +65 -0
  82. uncountable/integration/queue_runner/command_server/command_server.py +83 -5
  83. uncountable/integration/queue_runner/command_server/constants.py +4 -0
  84. uncountable/integration/queue_runner/command_server/protocol/command_server.proto +36 -0
  85. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +28 -11
  86. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +77 -1
  87. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +135 -0
  88. uncountable/integration/queue_runner/command_server/types.py +25 -2
  89. uncountable/integration/queue_runner/datastore/datastore_sqlite.py +168 -11
  90. uncountable/integration/queue_runner/datastore/interface.py +10 -0
  91. uncountable/integration/queue_runner/datastore/model.py +8 -1
  92. uncountable/integration/queue_runner/job_scheduler.py +63 -23
  93. uncountable/integration/queue_runner/queue_runner.py +10 -2
  94. uncountable/integration/queue_runner/worker.py +22 -17
  95. uncountable/integration/scan_profiles.py +1 -1
  96. uncountable/integration/scheduler.py +74 -25
  97. uncountable/integration/secret_retrieval/retrieve_secret.py +1 -1
  98. uncountable/integration/server.py +42 -12
  99. uncountable/integration/telemetry.py +63 -10
  100. uncountable/integration/webhook_server/entrypoint.py +39 -112
  101. uncountable/types/__init__.py +58 -1
  102. uncountable/types/api/batch/execute_batch.py +5 -6
  103. uncountable/types/api/batch/execute_batch_load_async.py +2 -3
  104. uncountable/types/api/chemical/convert_chemical_formats.py +10 -5
  105. uncountable/types/api/condition_parameters/__init__.py +1 -0
  106. uncountable/types/api/condition_parameters/upsert_condition_match.py +72 -0
  107. uncountable/types/api/entity/create_entities.py +7 -7
  108. uncountable/types/api/entity/create_entity.py +8 -8
  109. uncountable/types/api/entity/create_or_update_entity.py +48 -0
  110. uncountable/types/api/entity/export_entities.py +59 -0
  111. uncountable/types/api/entity/get_entities_data.py +3 -4
  112. uncountable/types/api/entity/grant_entity_permissions.py +6 -6
  113. uncountable/types/api/entity/list_aggregate.py +79 -0
  114. uncountable/types/api/entity/list_entities.py +34 -10
  115. uncountable/types/api/entity/lock_entity.py +4 -4
  116. uncountable/types/api/entity/lookup_entity.py +116 -0
  117. uncountable/types/api/entity/resolve_entity_ids.py +5 -6
  118. uncountable/types/api/entity/set_entity_field_values.py +44 -0
  119. uncountable/types/api/entity/set_values.py +3 -3
  120. uncountable/types/api/entity/transition_entity_phase.py +14 -7
  121. uncountable/types/api/entity/unlock_entity.py +3 -3
  122. uncountable/types/api/equipment/associate_equipment_input.py +2 -3
  123. uncountable/types/api/field_options/upsert_field_options.py +7 -7
  124. uncountable/types/api/files/__init__.py +1 -0
  125. uncountable/types/api/files/download_file.py +77 -0
  126. uncountable/types/api/id_source/list_id_source.py +6 -7
  127. uncountable/types/api/id_source/match_id_source.py +4 -5
  128. uncountable/types/api/input_groups/get_input_group_names.py +3 -4
  129. uncountable/types/api/inputs/create_inputs.py +10 -9
  130. uncountable/types/api/inputs/get_input_data.py +11 -12
  131. uncountable/types/api/inputs/get_input_names.py +6 -7
  132. uncountable/types/api/inputs/get_inputs_data.py +6 -7
  133. uncountable/types/api/inputs/set_input_attribute_values.py +5 -6
  134. uncountable/types/api/inputs/set_input_category.py +5 -5
  135. uncountable/types/api/inputs/set_input_subcategories.py +3 -3
  136. uncountable/types/api/inputs/set_intermediate_type.py +4 -4
  137. uncountable/types/api/integrations/__init__.py +1 -0
  138. uncountable/types/api/integrations/publish_realtime_data.py +41 -0
  139. uncountable/types/api/integrations/push_notification.py +49 -0
  140. uncountable/types/api/integrations/register_sockets_token.py +41 -0
  141. uncountable/types/api/listing/__init__.py +1 -0
  142. uncountable/types/api/listing/fetch_listing.py +58 -0
  143. uncountable/types/api/material_families/update_entity_material_families.py +3 -4
  144. uncountable/types/api/notebooks/__init__.py +1 -0
  145. uncountable/types/api/notebooks/add_notebook_content.py +119 -0
  146. uncountable/types/api/outputs/get_output_data.py +12 -13
  147. uncountable/types/api/outputs/get_output_names.py +5 -6
  148. uncountable/types/api/outputs/get_output_organization.py +173 -0
  149. uncountable/types/api/outputs/resolve_output_conditions.py +7 -8
  150. uncountable/types/api/permissions/set_core_permissions.py +16 -10
  151. uncountable/types/api/project/get_projects.py +6 -7
  152. uncountable/types/api/project/get_projects_data.py +7 -8
  153. uncountable/types/api/recipe_links/create_recipe_link.py +5 -5
  154. uncountable/types/api/recipe_links/remove_recipe_link.py +4 -4
  155. uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +6 -7
  156. uncountable/types/api/recipes/add_recipe_to_project.py +3 -3
  157. uncountable/types/api/recipes/add_time_series_data.py +64 -0
  158. uncountable/types/api/recipes/archive_recipes.py +4 -4
  159. uncountable/types/api/recipes/associate_recipe_as_input.py +5 -5
  160. uncountable/types/api/recipes/associate_recipe_as_lot.py +3 -3
  161. uncountable/types/api/recipes/clear_recipe_outputs.py +3 -3
  162. uncountable/types/api/recipes/create_mix_order.py +44 -0
  163. uncountable/types/api/recipes/create_recipe.py +8 -9
  164. uncountable/types/api/recipes/create_recipes.py +8 -9
  165. uncountable/types/api/recipes/disassociate_recipe_as_input.py +3 -3
  166. uncountable/types/api/recipes/edit_recipe_inputs.py +101 -24
  167. uncountable/types/api/recipes/get_column_calculation_values.py +4 -5
  168. uncountable/types/api/recipes/get_curve.py +4 -5
  169. uncountable/types/api/recipes/get_recipe_calculations.py +6 -7
  170. uncountable/types/api/recipes/get_recipe_links.py +3 -4
  171. uncountable/types/api/recipes/get_recipe_names.py +3 -4
  172. uncountable/types/api/recipes/get_recipe_output_metadata.py +5 -6
  173. uncountable/types/api/recipes/get_recipes_data.py +62 -34
  174. uncountable/types/api/recipes/lock_recipes.py +9 -8
  175. uncountable/types/api/recipes/remove_recipe_from_project.py +3 -3
  176. uncountable/types/api/recipes/set_recipe_inputs.py +9 -10
  177. uncountable/types/api/recipes/set_recipe_metadata.py +3 -3
  178. uncountable/types/api/recipes/set_recipe_output_annotations.py +11 -12
  179. uncountable/types/api/recipes/set_recipe_output_file.py +5 -6
  180. uncountable/types/api/recipes/set_recipe_outputs.py +24 -13
  181. uncountable/types/api/recipes/set_recipe_tags.py +14 -9
  182. uncountable/types/api/recipes/set_recipe_total.py +59 -0
  183. uncountable/types/api/recipes/unarchive_recipes.py +3 -3
  184. uncountable/types/api/recipes/unlock_recipes.py +7 -6
  185. uncountable/types/api/runsheet/__init__.py +1 -0
  186. uncountable/types/api/runsheet/complete_async_upload.py +41 -0
  187. uncountable/types/api/triggers/run_trigger.py +4 -4
  188. uncountable/types/api/uploader/complete_async_parse.py +46 -0
  189. uncountable/types/api/uploader/invoke_uploader.py +4 -5
  190. uncountable/types/api/user/__init__.py +1 -0
  191. uncountable/types/api/user/get_current_user_info.py +40 -0
  192. uncountable/types/async_batch.py +1 -1
  193. uncountable/types/async_batch_processor.py +506 -23
  194. uncountable/types/async_batch_t.py +35 -8
  195. uncountable/types/async_jobs.py +0 -1
  196. uncountable/types/async_jobs_t.py +1 -2
  197. uncountable/types/auth_retrieval.py +0 -1
  198. uncountable/types/auth_retrieval_t.py +6 -6
  199. uncountable/types/base.py +0 -1
  200. uncountable/types/base_t.py +11 -9
  201. uncountable/types/calculations.py +0 -1
  202. uncountable/types/calculations_t.py +1 -2
  203. uncountable/types/chemical_structure.py +0 -1
  204. uncountable/types/chemical_structure_t.py +5 -5
  205. uncountable/types/client_base.py +614 -69
  206. uncountable/types/client_config.py +1 -1
  207. uncountable/types/client_config_t.py +13 -3
  208. uncountable/types/curves.py +0 -1
  209. uncountable/types/curves_t.py +6 -7
  210. uncountable/types/data.py +12 -0
  211. uncountable/types/data_t.py +103 -0
  212. uncountable/types/entity.py +1 -1
  213. uncountable/types/entity_t.py +90 -10
  214. uncountable/types/experiment_groups.py +0 -1
  215. uncountable/types/experiment_groups_t.py +1 -2
  216. uncountable/types/exports.py +8 -0
  217. uncountable/types/exports_t.py +34 -0
  218. uncountable/types/field_values.py +19 -1
  219. uncountable/types/field_values_t.py +242 -9
  220. uncountable/types/fields.py +0 -1
  221. uncountable/types/fields_t.py +1 -2
  222. uncountable/types/generic_upload.py +0 -1
  223. uncountable/types/generic_upload_t.py +14 -14
  224. uncountable/types/id_source.py +0 -1
  225. uncountable/types/id_source_t.py +13 -7
  226. uncountable/types/identifier.py +0 -1
  227. uncountable/types/identifier_t.py +10 -5
  228. uncountable/types/input_attributes.py +0 -1
  229. uncountable/types/input_attributes_t.py +3 -4
  230. uncountable/types/inputs.py +0 -1
  231. uncountable/types/inputs_t.py +3 -4
  232. uncountable/types/integration_server.py +0 -1
  233. uncountable/types/integration_server_t.py +13 -4
  234. uncountable/types/integration_session.py +10 -0
  235. uncountable/types/integration_session_t.py +60 -0
  236. uncountable/types/integrations.py +10 -0
  237. uncountable/types/integrations_t.py +62 -0
  238. uncountable/types/job_definition.py +2 -1
  239. uncountable/types/job_definition_t.py +57 -32
  240. uncountable/types/listing.py +9 -0
  241. uncountable/types/listing_t.py +51 -0
  242. uncountable/types/notices.py +8 -0
  243. uncountable/types/notices_t.py +37 -0
  244. uncountable/types/notifications.py +11 -0
  245. uncountable/types/notifications_t.py +74 -0
  246. uncountable/types/outputs.py +0 -1
  247. uncountable/types/outputs_t.py +2 -3
  248. uncountable/types/overrides.py +0 -1
  249. uncountable/types/overrides_t.py +10 -4
  250. uncountable/types/permissions.py +0 -1
  251. uncountable/types/permissions_t.py +1 -2
  252. uncountable/types/phases.py +0 -1
  253. uncountable/types/phases_t.py +1 -2
  254. uncountable/types/post_base.py +0 -1
  255. uncountable/types/post_base_t.py +1 -2
  256. uncountable/types/queued_job.py +2 -1
  257. uncountable/types/queued_job_t.py +29 -12
  258. uncountable/types/recipe_identifiers.py +0 -1
  259. uncountable/types/recipe_identifiers_t.py +18 -8
  260. uncountable/types/recipe_inputs.py +0 -1
  261. uncountable/types/recipe_inputs_t.py +1 -2
  262. uncountable/types/recipe_links.py +0 -1
  263. uncountable/types/recipe_links_t.py +3 -4
  264. uncountable/types/recipe_metadata.py +0 -1
  265. uncountable/types/recipe_metadata_t.py +9 -10
  266. uncountable/types/recipe_output_metadata.py +0 -1
  267. uncountable/types/recipe_output_metadata_t.py +1 -2
  268. uncountable/types/recipe_tags.py +0 -1
  269. uncountable/types/recipe_tags_t.py +1 -2
  270. uncountable/types/recipe_workflow_steps.py +0 -1
  271. uncountable/types/recipe_workflow_steps_t.py +7 -7
  272. uncountable/types/recipes.py +0 -1
  273. uncountable/types/recipes_t.py +2 -2
  274. uncountable/types/response.py +0 -1
  275. uncountable/types/response_t.py +2 -2
  276. uncountable/types/secret_retrieval.py +0 -1
  277. uncountable/types/secret_retrieval_t.py +7 -7
  278. uncountable/types/sockets.py +20 -0
  279. uncountable/types/sockets_t.py +169 -0
  280. uncountable/types/structured_filters.py +25 -0
  281. uncountable/types/structured_filters_t.py +248 -0
  282. uncountable/types/units.py +0 -1
  283. uncountable/types/units_t.py +1 -2
  284. uncountable/types/uploader.py +24 -0
  285. uncountable/types/uploader_t.py +222 -0
  286. uncountable/types/users.py +0 -1
  287. uncountable/types/users_t.py +1 -2
  288. uncountable/types/webhook_job.py +1 -1
  289. uncountable/types/webhook_job_t.py +14 -3
  290. uncountable/types/workflows.py +0 -1
  291. uncountable/types/workflows_t.py +3 -4
  292. uncountablepythonsdk-0.0.132.dist-info/METADATA +64 -0
  293. uncountablepythonsdk-0.0.132.dist-info/RECORD +363 -0
  294. {UncountablePythonSDK-0.0.82.dist-info → uncountablepythonsdk-0.0.132.dist-info}/WHEEL +1 -1
  295. UncountablePythonSDK-0.0.82.dist-info/METADATA +0 -60
  296. UncountablePythonSDK-0.0.82.dist-info/RECORD +0 -292
  297. docs/quickstart.md +0 -19
  298. {UncountablePythonSDK-0.0.82.dist-info → uncountablepythonsdk-0.0.132.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,15 @@
1
1
  import os
2
2
  from dataclasses import dataclass
3
3
  from io import BytesIO
4
- from typing import Optional, Union
4
+ from typing import Union
5
5
 
6
6
  import paramiko
7
+ from azure.core.credentials import (
8
+ AzureNamedKeyCredential,
9
+ AzureSasCredential,
10
+ TokenCredential,
11
+ )
12
+ from azure.storage.blob import ContainerProperties
7
13
 
8
14
 
9
15
  @dataclass
@@ -11,9 +17,9 @@ class FileObjectData:
11
17
  file_data: bytes
12
18
  file_IO: BytesIO
13
19
  filename: str
14
- filepath: Optional[str] = None
15
- mime_type: Optional[str] = None
16
- metadata: Optional[dict[str, str]] = None
20
+ filepath: str | None = None
21
+ mime_type: str | None = None
22
+ metadata: dict[str, str] | None = None
17
23
 
18
24
 
19
25
  @dataclass
@@ -33,7 +39,7 @@ class FileSystemFileReference:
33
39
  class RemoteObjectReference:
34
40
  file_id: str
35
41
  mime_type: str
36
- filename: Optional[str] = None
42
+ filename: str | None = None
37
43
 
38
44
  @property
39
45
  def is_dir(self) -> bool:
@@ -57,7 +63,7 @@ class FileSystemSFTPConfig:
57
63
  pem_path: str | None
58
64
  pem_key: paramiko.RSAKey | None = None
59
65
  password: str | None = None
60
- valid_extensions: Optional[tuple[str]] = None
66
+ valid_extensions: tuple[str] | None = None
61
67
  recursive: bool = True
62
68
 
63
69
 
@@ -65,7 +71,21 @@ class FileSystemSFTPConfig:
65
71
  class FileSystemS3Config:
66
72
  endpoint_url: str
67
73
  bucket_name: str
68
- region_name: Optional[str]
69
- access_key_id: Optional[str]
70
- secret_access_key: Optional[str]
71
- session_token: Optional[str]
74
+ region_name: str | None
75
+ access_key_id: str | None
76
+ secret_access_key: str | None
77
+ session_token: str | None
78
+
79
+
80
+ @dataclass(kw_only=True)
81
+ class FileSystemBlobConfig:
82
+ account_url: str
83
+ credential: (
84
+ str
85
+ | dict[str, str]
86
+ | AzureNamedKeyCredential
87
+ | AzureSasCredential
88
+ | TokenCredential
89
+ | None
90
+ )
91
+ container: ContainerProperties | str
@@ -1,12 +1,17 @@
1
- # flake8:noqa
1
+ from .annotation import unwrap_annotated as unwrap_annotated
2
2
  from .missing_sentry import MISSING_SENTRY as MISSING_SENTRY
3
3
  from .missing_sentry import MissingSentryType as MissingSentryType
4
4
  from .missing_sentry import MissingType as MissingType
5
5
  from .missing_sentry import coalesce_missing_sentry as coalesce_missing_sentry
6
6
  from .opaque_key import OpaqueKey as OpaqueKey
7
+ from .serial_alias import SerialAliasInspector as SerialAliasInspector
8
+ from .serial_alias import get_serial_alias_data as get_serial_alias_data
9
+ from .serial_alias import serial_alias_annotation as serial_alias_annotation
10
+ from .serial_class import SerialClassDataInspector as SerialClassDataInspector
7
11
  from .serial_class import get_serial_class_data as get_serial_class_data
8
12
  from .serial_class import get_serial_string_enum_data as get_serial_string_enum_data
9
13
  from .serial_class import serial_class as serial_class
10
14
  from .serial_class import serial_string_enum as serial_string_enum
11
- from .serial_union import serial_union_annotation as serial_union_annotation
15
+ from .serial_generic import get_serial_data as get_serial_data
12
16
  from .serial_union import get_serial_union_data as get_serial_union_data
17
+ from .serial_union import serial_union_annotation as serial_union_annotation
@@ -0,0 +1,64 @@
1
+ import dataclasses
2
+ import typing
3
+
4
+ T = typing.TypeVar("T")
5
+
6
+
7
+ @dataclasses.dataclass(kw_only=True, frozen=True, eq=True)
8
+ class SerialBase:
9
+ named_type_path: str | None = None
10
+ # Indicates this type is allowed in dynamic lookups, such as via a named_type_path
11
+ # This isn't meant to be limting, but to catalog all the types where we need it
12
+ is_dynamic_allowed: bool = False
13
+ # Tracks if this data was provided as a decorator to the type.
14
+ # This is used to track "proper types" which are appropriate
15
+ # for serialization and/or dynamic discovery
16
+ from_decorator: bool = False
17
+
18
+
19
+ def get_serial_annotation(parsed_type: type[T]) -> SerialBase | None:
20
+ if not hasattr(parsed_type, "__metadata__"):
21
+ return None
22
+ metadata = parsed_type.__metadata__ # type:ignore[attr-defined]
23
+ if not isinstance(metadata, tuple) or len(metadata) != 1:
24
+ return None
25
+ serial = metadata[0]
26
+ if not isinstance(serial, SerialBase):
27
+ return None
28
+ return serial
29
+
30
+
31
+ class SerialInspector(typing.Generic[T]):
32
+ def __init__(self, parsed_type: type[T], serial_base: SerialBase) -> None:
33
+ self._parsed_type = parsed_type
34
+ self._serial_base = serial_base
35
+
36
+ @property
37
+ def named_type_path(self) -> str | None:
38
+ return self._serial_base.named_type_path
39
+
40
+ @property
41
+ def from_decorator(self) -> bool:
42
+ return self._serial_base.from_decorator
43
+
44
+ @property
45
+ def is_field_proper(self) -> bool:
46
+ return (
47
+ self._serial_base.from_decorator
48
+ and self._serial_base.named_type_path is not None
49
+ )
50
+
51
+ @property
52
+ def is_dynamic_allowed(self) -> bool:
53
+ return self._serial_base.is_dynamic_allowed
54
+
55
+
56
+ def unwrap_annotated(parsed_type: type[T]) -> type[T]:
57
+ """
58
+ If the type is an annotated type then return the origin of it.
59
+ Otherwise return the original type.
60
+ """
61
+ if typing.get_origin(parsed_type) is typing.Annotated:
62
+ # It's unclear if there's anyway to type this correctly
63
+ return parsed_type.__origin__ # type:ignore[attr-defined, no-any-return]
64
+ return parsed_type
@@ -26,5 +26,5 @@ MISSING_SENTRY = MissingSentryType()
26
26
  MissingType = Union[MissingSentryType, ClassT]
27
27
 
28
28
 
29
- def coalesce_missing_sentry(value: MissingType[ClassT]) -> Optional[ClassT]:
29
+ def coalesce_missing_sentry(value: MissingType[ClassT]) -> ClassT | None:
30
30
  return None if isinstance(value, MissingSentryType) else value
@@ -1,4 +1,4 @@
1
1
  # Blocks a string key value from being interpreted for case conversion
2
- class OpaqueKey(str):
2
+ class OpaqueKey(str): # noqa: FURB189
3
3
  def __new__(cls, key: str) -> "OpaqueKey":
4
4
  return str.__new__(cls, key)
@@ -0,0 +1,47 @@
1
+ import dataclasses
2
+ import typing
3
+
4
+ from .annotation import SerialBase, SerialInspector, get_serial_annotation
5
+
6
+ T = typing.TypeVar("T")
7
+
8
+
9
+ @dataclasses.dataclass(kw_only=True, frozen=True, eq=True)
10
+ class _SerialAlias(SerialBase):
11
+ """
12
+ This class is to be kept private, to provide flexibility in registration/lookup.
13
+ Places that need the data should access it via help classes/methods.
14
+ """
15
+
16
+
17
+ def serial_alias_annotation(
18
+ *,
19
+ named_type_path: str | None = None,
20
+ is_dynamic_allowed: bool = False,
21
+ ) -> _SerialAlias:
22
+ return _SerialAlias(
23
+ named_type_path=named_type_path,
24
+ from_decorator=True,
25
+ is_dynamic_allowed=is_dynamic_allowed,
26
+ )
27
+
28
+
29
+ def _get_serial_alias(parsed_type: type[T]) -> _SerialAlias | None:
30
+ serial = get_serial_annotation(parsed_type)
31
+ if not isinstance(serial, _SerialAlias):
32
+ return None
33
+ return serial
34
+
35
+
36
+ class SerialAliasInspector(SerialInspector[T]):
37
+ def __init__(self, parsed_type: type[T], serial_alias: _SerialAlias) -> None:
38
+ super().__init__(parsed_type, serial_alias)
39
+ self._serial_alias = serial_alias
40
+
41
+
42
+ def get_serial_alias_data(parsed_type: type[T]) -> SerialAliasInspector[T] | None:
43
+ serial = _get_serial_alias(parsed_type)
44
+ if serial is None:
45
+ return None
46
+
47
+ return SerialAliasInspector(parsed_type, serial)
@@ -3,22 +3,20 @@ from __future__ import annotations
3
3
  import dataclasses
4
4
  from collections.abc import Callable
5
5
  from enum import StrEnum
6
- from typing import Any, Optional, TypeVar, cast
6
+ from typing import Any, TypeVar, cast
7
7
 
8
- _ClassT = TypeVar("_ClassT")
8
+ from .annotation import SerialBase, SerialInspector
9
9
 
10
+ ClassT = TypeVar("ClassT")
10
11
 
11
- @dataclasses.dataclass
12
- class _SerialClassData:
12
+
13
+ @dataclasses.dataclass(kw_only=True, frozen=True, eq=True)
14
+ class _SerialClassData(SerialBase):
13
15
  unconverted_keys: set[str] = dataclasses.field(default_factory=set)
14
16
  unconverted_values: set[str] = dataclasses.field(default_factory=set)
15
17
  to_string_values: set[str] = dataclasses.field(default_factory=set)
16
18
  parse_require: set[str] = dataclasses.field(default_factory=set)
17
- named_type_path: Optional[str] = None
18
- # Tracks if this data was provided as a decorator to the type.
19
- # This is used to track "proper types" which are appropriate
20
- # for serialization and/or dynamic discovery
21
- from_decorator: bool = False
19
+ named_type_path: str | None = None
22
20
 
23
21
 
24
22
  EMPTY_SERIAL_CLASS_DATA = _SerialClassData()
@@ -26,12 +24,13 @@ EMPTY_SERIAL_CLASS_DATA = _SerialClassData()
26
24
 
27
25
  def serial_class(
28
26
  *,
29
- unconverted_keys: Optional[set[str]] = None,
30
- unconverted_values: Optional[set[str]] = None,
31
- to_string_values: Optional[set[str]] = None,
32
- parse_require: Optional[set[str]] = None,
33
- named_type_path: Optional[str] = None,
34
- ) -> Callable[[_ClassT], _ClassT]:
27
+ unconverted_keys: set[str] | None = None,
28
+ unconverted_values: set[str] | None = None,
29
+ to_string_values: set[str] | None = None,
30
+ parse_require: set[str] | None = None,
31
+ named_type_path: str | None = None,
32
+ is_dynamic_allowed: bool = False,
33
+ ) -> Callable[[ClassT], ClassT]:
35
34
  """
36
35
  An additional decorator to a dataclass that specifies serialization options.
37
36
 
@@ -55,7 +54,7 @@ def serial_class(
55
54
  The type_spec type-path to this type. This applies only to named types.
56
55
  """
57
56
 
58
- def decorate(orig_class: _ClassT) -> _ClassT:
57
+ def decorate(orig_class: ClassT) -> ClassT:
59
58
  cast(Any, orig_class).__unc_serial_data = _SerialClassData(
60
59
  unconverted_keys=unconverted_keys or set(),
61
60
  unconverted_values=unconverted_values or set(),
@@ -63,17 +62,20 @@ def serial_class(
63
62
  parse_require=parse_require or set(),
64
63
  named_type_path=named_type_path,
65
64
  from_decorator=True,
65
+ is_dynamic_allowed=is_dynamic_allowed,
66
66
  )
67
67
  return orig_class
68
68
 
69
69
  return decorate
70
70
 
71
71
 
72
- class SerialClassDataInspector:
72
+ class SerialClassDataInspector(SerialInspector[ClassT]):
73
73
  def __init__(
74
74
  self,
75
+ parsed_type: type[ClassT],
75
76
  current: _SerialClassData,
76
77
  ) -> None:
78
+ super().__init__(parsed_type, current)
77
79
  self.current = current
78
80
 
79
81
  def has_unconverted_key(self, key: str) -> bool:
@@ -88,20 +90,8 @@ class SerialClassDataInspector:
88
90
  def has_parse_require(self, key: str) -> bool:
89
91
  return key in self.current.parse_require
90
92
 
91
- @property
92
- def from_decorator(self) -> bool:
93
- return self.current.from_decorator
94
-
95
- @property
96
- def named_type_path(self) -> Optional[str]:
97
- return self.current.named_type_path
98
-
99
- @property
100
- def is_field_proper(self) -> bool:
101
- return self.current.from_decorator and self.current.named_type_path is not None
102
93
 
103
-
104
- def _get_merged_serial_class_data(type_class: type[Any]) -> _SerialClassData | None:
94
+ def get_merged_serial_class_data(type_class: type[Any]) -> _SerialClassData | None:
105
95
  base_class_data = (
106
96
  cast(_SerialClassData, type_class.__unc_serial_data)
107
97
  if hasattr(type_class, "__unc_serial_data")
@@ -110,28 +100,30 @@ def _get_merged_serial_class_data(type_class: type[Any]) -> _SerialClassData | N
110
100
  if base_class_data is None:
111
101
  return None
112
102
 
103
+ # IMPROVE: We should cache this result on the type
113
104
  if type_class.__bases__ is not None:
114
105
  for base in type_class.__bases__:
115
- curr_base_class_data = _get_merged_serial_class_data(base)
106
+ curr_base_class_data = get_merged_serial_class_data(base)
116
107
  if curr_base_class_data is not None:
117
- if base_class_data is None:
118
- base_class_data = _SerialClassData()
119
- base_class_data.unconverted_keys |= (
120
- curr_base_class_data.unconverted_keys
121
- )
122
- base_class_data.unconverted_values |= (
123
- curr_base_class_data.unconverted_values
124
- )
125
- base_class_data.to_string_values |= (
126
- curr_base_class_data.to_string_values
108
+ base_class_data = dataclasses.replace(
109
+ base_class_data,
110
+ unconverted_keys=base_class_data.unconverted_keys
111
+ | curr_base_class_data.unconverted_keys,
112
+ unconverted_values=base_class_data.unconverted_values
113
+ | curr_base_class_data.unconverted_values,
114
+ to_string_values=base_class_data.to_string_values
115
+ | curr_base_class_data.to_string_values,
116
+ parse_require=base_class_data.parse_require
117
+ | curr_base_class_data.parse_require,
127
118
  )
128
- base_class_data.parse_require |= curr_base_class_data.parse_require
129
119
  return base_class_data
130
120
 
131
121
 
132
- def get_serial_class_data(type_class: type[Any]) -> SerialClassDataInspector:
122
+ def get_serial_class_data(
123
+ type_class: type[ClassT],
124
+ ) -> SerialClassDataInspector[ClassT]:
133
125
  return SerialClassDataInspector(
134
- _get_merged_serial_class_data(type_class) or EMPTY_SERIAL_CLASS_DATA
126
+ type_class, get_merged_serial_class_data(type_class) or EMPTY_SERIAL_CLASS_DATA
135
127
  )
136
128
 
137
129
 
@@ -142,13 +134,13 @@ class _SerialStringEnumData:
142
134
 
143
135
 
144
136
  def serial_string_enum(
145
- *, labels: Optional[dict[str, str]] = None, deprecated: Optional[set[str]] = None
146
- ) -> Callable[[_ClassT], _ClassT]:
137
+ *, labels: dict[str, str] | None = None, deprecated: set[str] | None = None
138
+ ) -> Callable[[ClassT], ClassT]:
147
139
  """
148
140
  A decorator for enums to provide serialization data, including labels.
149
141
  """
150
142
 
151
- def decorate(orig_class: _ClassT) -> _ClassT:
143
+ def decorate(orig_class: ClassT) -> ClassT:
152
144
  cast(Any, orig_class).__unc_serial_string_enum_data = _SerialStringEnumData(
153
145
  labels=labels or {}, deprecated=deprecated or set()
154
146
  )
@@ -161,7 +153,7 @@ class SerialStringEnumInspector:
161
153
  def __init__(self, current: _SerialStringEnumData) -> None:
162
154
  self.current = current
163
155
 
164
- def get_label(self, value: str) -> Optional[str]:
156
+ def get_label(self, value: str) -> str | None:
165
157
  return self.current.labels.get(value)
166
158
 
167
159
  def get_deprecated(self, value: str) -> bool:
@@ -0,0 +1,16 @@
1
+ import typing
2
+
3
+ from .annotation import SerialInspector, get_serial_annotation
4
+ from .serial_class import get_merged_serial_class_data
5
+
6
+ T = typing.TypeVar("T")
7
+
8
+
9
+ def get_serial_data(parsed_type: type[T]) -> SerialInspector[T] | None:
10
+ serial = get_serial_annotation(parsed_type)
11
+ if serial is None:
12
+ serial = get_merged_serial_class_data(parsed_type)
13
+
14
+ if serial is not None:
15
+ return SerialInspector(parsed_type, serial)
16
+ return None
@@ -1,6 +1,8 @@
1
1
  import dataclasses
2
2
  import typing
3
3
 
4
+ from .annotation import SerialBase, SerialInspector, get_serial_annotation
5
+
4
6
  T = typing.TypeVar("T")
5
7
 
6
8
 
@@ -17,7 +19,7 @@ class IdentityHashWrapper(typing.Generic[T]):
17
19
 
18
20
 
19
21
  @dataclasses.dataclass(kw_only=True, frozen=True, eq=True)
20
- class _SerialUnion:
22
+ class _SerialUnion(SerialBase):
21
23
  """
22
24
  This class is to be kept private, to provide flexibility in registration/lookup.
23
25
  Places that need the data should access it via help classes/methods.
@@ -25,16 +27,16 @@ class _SerialUnion:
25
27
 
26
28
  # If specified, indicates the Union has a discriminator which should be used to
27
29
  # determine which type to parse.
28
- discriminator: typing.Optional[str] = None
29
- discriminator_map: typing.Optional[IdentityHashWrapper[dict[str, type]]] = None
30
- named_type_path: typing.Optional[str] = None
30
+ discriminator: str | None = None
31
+ discriminator_map: IdentityHashWrapper[dict[str, type]] | None = None
31
32
 
32
33
 
33
34
  def serial_union_annotation(
34
35
  *,
35
- discriminator: typing.Optional[str] = None,
36
- discriminator_map: typing.Optional[dict[str, type]] = None,
37
- named_type_path: typing.Optional[str] = None,
36
+ discriminator: str | None = None,
37
+ discriminator_map: dict[str, type] | None = None,
38
+ named_type_path: str | None = None,
39
+ is_dynamic_allowed: bool = False,
38
40
  ) -> _SerialUnion:
39
41
  return _SerialUnion(
40
42
  discriminator=discriminator,
@@ -42,23 +44,21 @@ def serial_union_annotation(
42
44
  if discriminator_map is not None
43
45
  else None,
44
46
  named_type_path=named_type_path,
47
+ from_decorator=True,
48
+ is_dynamic_allowed=is_dynamic_allowed,
45
49
  )
46
50
 
47
51
 
48
52
  def _get_serial_union(parsed_type: type[T]) -> _SerialUnion | None:
49
- if not hasattr(parsed_type, "__metadata__"):
50
- return None
51
- metadata = parsed_type.__metadata__ # type:ignore[attr-defined]
52
- if not isinstance(metadata, tuple) or len(metadata) != 1:
53
- return None
54
- serial = metadata[0]
53
+ serial = get_serial_annotation(parsed_type)
55
54
  if not isinstance(serial, _SerialUnion):
56
55
  return None
57
56
  return serial
58
57
 
59
58
 
60
- class SerialClassInspector(typing.Generic[T]):
59
+ class SerialClassInspector(SerialInspector[T]):
61
60
  def __init__(self, parsed_type: type[T], serial_union: _SerialUnion) -> None:
61
+ super().__init__(parsed_type, serial_union)
62
62
  self._parsed_type = parsed_type
63
63
  self._serial_union = serial_union
64
64
 
@@ -66,11 +66,11 @@ class SerialClassInspector(typing.Generic[T]):
66
66
  return typing.get_args(self._parsed_type)[0] # type:ignore[no-any-return]
67
67
 
68
68
  @property
69
- def discriminator(self) -> typing.Optional[str]:
69
+ def discriminator(self) -> str | None:
70
70
  return self._serial_union.discriminator
71
71
 
72
72
  @property
73
- def discriminator_map(self) -> typing.Optional[dict[str, type]]:
73
+ def discriminator_map(self) -> dict[str, type] | None:
74
74
  if self._serial_union.discriminator_map is None:
75
75
  return None
76
76
  return self._serial_union.discriminator_map.inner
@@ -1,5 +1,8 @@
1
1
  from .convert_to_snakecase import convert_dict_to_snake_case
2
+ from .dataclasses import dict_fields as dict_fields
3
+ from .dataclasses import iterate_fields as iterate_fields
2
4
  from .serialization_helpers import (
5
+ JsonValue,
3
6
  serialize_for_api,
4
7
  serialize_for_storage,
5
8
  serialize_for_storage_dict,
@@ -10,4 +13,7 @@ __all__: list[str] = [
10
13
  "serialize_for_api",
11
14
  "serialize_for_storage",
12
15
  "serialize_for_storage_dict",
16
+ "iterate_fields",
17
+ "dict_fields",
18
+ "JsonValue",
13
19
  ]
@@ -0,0 +1,14 @@
1
+ from dataclasses import fields
2
+ from typing import TYPE_CHECKING, Any, Iterator
3
+
4
+ if TYPE_CHECKING:
5
+ from _typeshed import DataclassInstance
6
+
7
+
8
+ def iterate_fields(d: "DataclassInstance") -> Iterator[tuple[str, Any]]:
9
+ for field in fields(d):
10
+ yield field.name, getattr(d, field.name)
11
+
12
+
13
+ def dict_fields(d: "DataclassInstance") -> dict[str, Any]:
14
+ return dict(iterate_fields(d))
@@ -25,6 +25,7 @@ from pkgs.serialization import (
25
25
  )
26
26
 
27
27
  from ._get_type_for_serialization import SerializationType, get_serialization_type
28
+ from .dataclasses import iterate_fields
28
29
 
29
30
  # Inlined types which otherwise would import from types/base.py
30
31
  JsonScalar = Union[str, float, bool, Decimal, None, datetime.datetime, datetime.date]
@@ -77,8 +78,16 @@ def _serialize_dict(d: dict[str, Any]) -> dict[str, JsonValue]:
77
78
  return {k: serialize_for_storage(v) for k, v in d.items() if v != MISSING_SENTRY}
78
79
 
79
80
 
81
+ def _serialize_dataclass(d: Any) -> dict[str, JsonValue]:
82
+ return {
83
+ k: serialize_for_storage(v) for k, v in iterate_fields(d) if v != MISSING_SENTRY
84
+ }
85
+
86
+
80
87
  def _to_string_value(value: Any) -> str:
81
- assert isinstance(value, (Decimal, int))
88
+ assert isinstance(value, (Decimal, int)), (
89
+ f"Expecting decimal or int, received: {value} (type={type(value)})"
90
+ )
82
91
  return str(value)
83
92
 
84
93
 
@@ -105,7 +114,7 @@ def _get_dataclass_conversion_lookups(dataclass_type: Any) -> DataclassConversio
105
114
  if scd.has_to_string_value(key):
106
115
  value_conversion_functions[key] = _to_string_value
107
116
  elif scd.has_unconverted_value(key):
108
- value_conversion_functions[key] = identity
117
+ value_conversion_functions[key] = serialize_for_storage
109
118
  else:
110
119
  value_conversion_functions[key] = serialize_for_api
111
120
 
@@ -121,7 +130,7 @@ def _convert_dataclass(d: Any) -> dict[str, JsonValue]:
121
130
  conversions.key_conversions[k]: (
122
131
  conversions.value_conversion_functions[k](v) if v is not None else None
123
132
  )
124
- for k, v in d.__dict__.items()
133
+ for k, v in iterate_fields(d)
125
134
  if v != MISSING_SENTRY
126
135
  }
127
136
 
@@ -169,14 +178,15 @@ def serialize_for_api(obj: Any) -> JsonValue:
169
178
  serialization_type == SerializationType.UNKNOWN
170
179
  ): # performance optimization to not do function lookup
171
180
  return obj # type: ignore
172
- return _CONVERSION_SERIALIZATION_FUNCS[serialization_type](obj)
181
+ r = _CONVERSION_SERIALIZATION_FUNCS[serialization_type](obj)
182
+ return r
173
183
 
174
184
 
175
185
  _SERIALIZATION_FUNCS_DICT: dict[
176
186
  SerializationType, Callable[[Any], dict[str, JsonValue]]
177
187
  ] = {
178
188
  SerializationType.DICT: _serialize_dict,
179
- SerializationType.DATACLASS: lambda x: _serialize_dict(x.__dict__),
189
+ SerializationType.DATACLASS: _serialize_dataclass,
180
190
  }
181
191
 
182
192
 
@@ -6,7 +6,6 @@ import os
6
6
  import sys
7
7
  from collections import defaultdict
8
8
  from dataclasses import dataclass
9
- from typing import TypeVar
10
9
 
11
10
  from main.base.types import actions_registry_t
12
11
  from pkgs.type_spec import builder
@@ -22,9 +21,6 @@ key_short_description = "short_description"
22
21
  key_description = "description"
23
22
 
24
23
 
25
- TypeT = TypeVar("TypeT")
26
-
27
-
28
24
  class InvalidSpecException(Exception):
29
25
  pass
30
26
 
@@ -64,7 +64,7 @@ def _emit_imports(
64
64
  sorted([f"type {_action_namespace_name(item)}" for item in list(namespaces)])
65
65
  )
66
66
  out.write(
67
- f'import {{ ActionsRegistryT, {namespaces_to_import} }} from "unc_mat/types"\n\n'
67
+ f'import {{ ActionsRegistryT, {namespaces_to_import} }} from "unc_types"\n\n'
68
68
  )
69
69
  out.write(MODIFY_NOTICE)
70
70
  return out.getvalue()
@@ -111,9 +111,7 @@ def _get_argument_type(arguments: str | None) -> str | None:
111
111
  argument_parts = arguments.split(".")
112
112
  namespace = argument_parts[0]
113
113
  type_name = argument_parts[1]
114
- if namespace is not None and type_name is not None:
115
- return f"{_action_namespace_name(namespace)}.{type_name}"
116
- return None
114
+ return f"{_action_namespace_name(namespace)}.{type_name}"
117
115
 
118
116
 
119
117
  def _emit_action_definition(