UncountablePythonSDK 0.0.52__py3-none-any.whl → 0.0.131__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 (316) 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/async_batch.py +3 -3
  9. examples/basic_auth.py +7 -0
  10. examples/create_entity.py +3 -1
  11. examples/create_ingredient_sdk.py +34 -0
  12. examples/download_files.py +26 -0
  13. examples/edit_recipe_inputs.py +4 -2
  14. examples/integration-server/jobs/materials_auto/concurrent_cron.py +11 -0
  15. examples/integration-server/jobs/materials_auto/example_cron.py +21 -0
  16. examples/integration-server/jobs/materials_auto/example_http.py +47 -0
  17. examples/integration-server/jobs/materials_auto/example_instrument.py +100 -0
  18. examples/integration-server/jobs/materials_auto/example_parse.py +140 -0
  19. examples/integration-server/jobs/materials_auto/example_predictions.py +61 -0
  20. examples/integration-server/jobs/materials_auto/example_runsheet_wh.py +39 -0
  21. examples/integration-server/jobs/materials_auto/example_wh.py +23 -0
  22. examples/integration-server/jobs/materials_auto/profile.yaml +104 -0
  23. examples/integration-server/pyproject.toml +224 -0
  24. examples/invoke_uploader.py +4 -1
  25. examples/oauth.py +7 -0
  26. examples/set_recipe_metadata_file.py +40 -0
  27. examples/set_recipe_output_file_sdk.py +26 -0
  28. examples/upload_files.py +1 -2
  29. pkgs/argument_parser/__init__.py +9 -0
  30. pkgs/argument_parser/_is_namedtuple.py +3 -0
  31. pkgs/argument_parser/argument_parser.py +217 -70
  32. pkgs/filesystem_utils/__init__.py +1 -0
  33. pkgs/filesystem_utils/_blob_session.py +144 -0
  34. pkgs/filesystem_utils/_gdrive_session.py +10 -7
  35. pkgs/filesystem_utils/_s3_session.py +15 -13
  36. pkgs/filesystem_utils/_sftp_session.py +11 -7
  37. pkgs/filesystem_utils/file_type_utils.py +30 -10
  38. pkgs/py.typed +0 -0
  39. pkgs/serialization/__init__.py +7 -2
  40. pkgs/serialization/annotation.py +64 -0
  41. pkgs/serialization/missing_sentry.py +1 -1
  42. pkgs/serialization/opaque_key.py +1 -1
  43. pkgs/serialization/serial_alias.py +47 -0
  44. pkgs/serialization/serial_class.py +47 -26
  45. pkgs/serialization/serial_generic.py +16 -0
  46. pkgs/serialization/serial_union.py +17 -14
  47. pkgs/serialization/yaml.py +4 -1
  48. pkgs/serialization_util/__init__.py +6 -0
  49. pkgs/serialization_util/dataclasses.py +14 -0
  50. pkgs/serialization_util/serialization_helpers.py +15 -5
  51. pkgs/type_spec/actions_registry/__main__.py +0 -4
  52. pkgs/type_spec/actions_registry/emit_typescript.py +5 -5
  53. pkgs/type_spec/builder.py +354 -119
  54. pkgs/type_spec/builder_types.py +9 -0
  55. pkgs/type_spec/config.py +51 -11
  56. pkgs/type_spec/cross_output_links.py +99 -0
  57. pkgs/type_spec/emit_io_ts.py +1 -1
  58. pkgs/type_spec/emit_open_api.py +127 -36
  59. pkgs/type_spec/emit_open_api_util.py +5 -6
  60. pkgs/type_spec/emit_python.py +329 -121
  61. pkgs/type_spec/emit_typescript.py +117 -256
  62. pkgs/type_spec/emit_typescript_util.py +291 -2
  63. pkgs/type_spec/load_types.py +18 -4
  64. pkgs/type_spec/non_discriminated_union_exceptions.py +14 -0
  65. pkgs/type_spec/open_api_util.py +29 -4
  66. pkgs/type_spec/parts/base.py.prepart +13 -10
  67. pkgs/type_spec/parts/base.ts.prepart +4 -0
  68. pkgs/type_spec/type_info/__main__.py +3 -1
  69. pkgs/type_spec/type_info/emit_type_info.py +124 -29
  70. pkgs/type_spec/ui_entry_actions/__init__.py +4 -0
  71. pkgs/type_spec/ui_entry_actions/generate_ui_entry_actions.py +308 -0
  72. pkgs/type_spec/util.py +4 -4
  73. pkgs/type_spec/value_spec/__main__.py +26 -9
  74. pkgs/type_spec/value_spec/convert_type.py +21 -1
  75. pkgs/type_spec/value_spec/emit_python.py +25 -7
  76. pkgs/type_spec/value_spec/types.py +1 -1
  77. uncountable/core/async_batch.py +1 -1
  78. uncountable/core/client.py +142 -39
  79. uncountable/core/environment.py +41 -0
  80. uncountable/core/file_upload.py +52 -18
  81. uncountable/integration/cli.py +142 -0
  82. uncountable/integration/construct_client.py +8 -8
  83. uncountable/integration/cron.py +11 -37
  84. uncountable/integration/db/connect.py +12 -2
  85. uncountable/integration/db/session.py +25 -0
  86. uncountable/integration/entrypoint.py +8 -37
  87. uncountable/integration/executors/executors.py +125 -2
  88. uncountable/integration/executors/generic_upload_executor.py +87 -29
  89. uncountable/integration/executors/script_executor.py +3 -3
  90. uncountable/integration/http_server/__init__.py +5 -0
  91. uncountable/integration/http_server/types.py +69 -0
  92. uncountable/integration/job.py +242 -12
  93. uncountable/integration/queue_runner/__init__.py +0 -0
  94. uncountable/integration/queue_runner/command_server/__init__.py +28 -0
  95. uncountable/integration/queue_runner/command_server/command_client.py +133 -0
  96. uncountable/integration/queue_runner/command_server/command_server.py +142 -0
  97. uncountable/integration/queue_runner/command_server/constants.py +4 -0
  98. uncountable/integration/queue_runner/command_server/protocol/__init__.py +0 -0
  99. uncountable/integration/queue_runner/command_server/protocol/command_server.proto +58 -0
  100. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +57 -0
  101. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +114 -0
  102. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +264 -0
  103. uncountable/integration/queue_runner/command_server/types.py +75 -0
  104. uncountable/integration/queue_runner/datastore/__init__.py +3 -0
  105. uncountable/integration/queue_runner/datastore/datastore_sqlite.py +250 -0
  106. uncountable/integration/queue_runner/datastore/interface.py +29 -0
  107. uncountable/integration/queue_runner/datastore/model.py +24 -0
  108. uncountable/integration/queue_runner/job_scheduler.py +200 -0
  109. uncountable/integration/queue_runner/queue_runner.py +34 -0
  110. uncountable/integration/queue_runner/types.py +7 -0
  111. uncountable/integration/queue_runner/worker.py +116 -0
  112. uncountable/integration/scan_profiles.py +67 -0
  113. uncountable/integration/scheduler.py +199 -0
  114. uncountable/integration/secret_retrieval/retrieve_secret.py +26 -4
  115. uncountable/integration/server.py +94 -69
  116. uncountable/integration/telemetry.py +150 -34
  117. uncountable/integration/webhook_server/entrypoint.py +97 -0
  118. uncountable/types/__init__.py +78 -1
  119. uncountable/types/api/batch/execute_batch.py +13 -6
  120. uncountable/types/api/batch/execute_batch_load_async.py +9 -3
  121. uncountable/types/api/chemical/convert_chemical_formats.py +17 -5
  122. uncountable/types/api/condition_parameters/__init__.py +1 -0
  123. uncountable/types/api/condition_parameters/upsert_condition_match.py +72 -0
  124. uncountable/types/api/entity/create_entities.py +19 -7
  125. uncountable/types/api/entity/create_entity.py +17 -8
  126. uncountable/types/api/entity/create_or_update_entity.py +48 -0
  127. uncountable/types/api/entity/export_entities.py +59 -0
  128. uncountable/types/api/entity/get_entities_data.py +13 -4
  129. uncountable/types/api/entity/grant_entity_permissions.py +48 -0
  130. uncountable/types/api/entity/list_aggregate.py +79 -0
  131. uncountable/types/api/entity/list_entities.py +42 -10
  132. uncountable/types/api/entity/lock_entity.py +11 -4
  133. uncountable/types/api/entity/lookup_entity.py +116 -0
  134. uncountable/types/api/entity/resolve_entity_ids.py +15 -6
  135. uncountable/types/api/entity/set_entity_field_values.py +44 -0
  136. uncountable/types/api/entity/set_values.py +10 -3
  137. uncountable/types/api/entity/transition_entity_phase.py +22 -7
  138. uncountable/types/api/entity/unlock_entity.py +10 -3
  139. uncountable/types/api/equipment/associate_equipment_input.py +9 -3
  140. uncountable/types/api/field_options/upsert_field_options.py +17 -7
  141. uncountable/types/api/files/__init__.py +1 -0
  142. uncountable/types/api/files/download_file.py +77 -0
  143. uncountable/types/api/id_source/list_id_source.py +16 -7
  144. uncountable/types/api/id_source/match_id_source.py +14 -5
  145. uncountable/types/api/input_groups/get_input_group_names.py +13 -4
  146. uncountable/types/api/inputs/create_inputs.py +23 -9
  147. uncountable/types/api/inputs/get_input_data.py +30 -12
  148. uncountable/types/api/inputs/get_input_names.py +16 -7
  149. uncountable/types/api/inputs/get_inputs_data.py +25 -7
  150. uncountable/types/api/inputs/set_input_attribute_values.py +12 -6
  151. uncountable/types/api/inputs/set_input_category.py +12 -5
  152. uncountable/types/api/inputs/set_input_subcategories.py +10 -3
  153. uncountable/types/api/inputs/set_intermediate_type.py +11 -4
  154. uncountable/types/api/integrations/__init__.py +1 -0
  155. uncountable/types/api/integrations/publish_realtime_data.py +41 -0
  156. uncountable/types/api/integrations/push_notification.py +49 -0
  157. uncountable/types/api/integrations/register_sockets_token.py +41 -0
  158. uncountable/types/api/listing/__init__.py +1 -0
  159. uncountable/types/api/listing/fetch_listing.py +58 -0
  160. uncountable/types/api/material_families/update_entity_material_families.py +10 -4
  161. uncountable/types/api/notebooks/__init__.py +1 -0
  162. uncountable/types/api/notebooks/add_notebook_content.py +119 -0
  163. uncountable/types/api/outputs/get_output_data.py +28 -13
  164. uncountable/types/api/outputs/get_output_names.py +15 -6
  165. uncountable/types/api/outputs/get_output_organization.py +173 -0
  166. uncountable/types/api/outputs/resolve_output_conditions.py +20 -8
  167. uncountable/types/api/permissions/set_core_permissions.py +26 -10
  168. uncountable/types/api/project/get_projects.py +16 -7
  169. uncountable/types/api/project/get_projects_data.py +17 -8
  170. uncountable/types/api/recipe_links/create_recipe_link.py +12 -5
  171. uncountable/types/api/recipe_links/remove_recipe_link.py +11 -4
  172. uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +16 -7
  173. uncountable/types/api/recipes/add_recipe_to_project.py +10 -3
  174. uncountable/types/api/recipes/add_time_series_data.py +64 -0
  175. uncountable/types/api/recipes/archive_recipes.py +11 -4
  176. uncountable/types/api/recipes/associate_recipe_as_input.py +12 -5
  177. uncountable/types/api/recipes/associate_recipe_as_lot.py +10 -3
  178. uncountable/types/api/recipes/clear_recipe_outputs.py +42 -0
  179. uncountable/types/api/recipes/create_mix_order.py +44 -0
  180. uncountable/types/api/recipes/create_recipe.py +15 -9
  181. uncountable/types/api/recipes/create_recipes.py +21 -9
  182. uncountable/types/api/recipes/disassociate_recipe_as_input.py +10 -3
  183. uncountable/types/api/recipes/edit_recipe_inputs.py +134 -22
  184. uncountable/types/api/recipes/get_column_calculation_values.py +57 -0
  185. uncountable/types/api/recipes/get_curve.py +11 -5
  186. uncountable/types/api/recipes/get_recipe_calculations.py +13 -7
  187. uncountable/types/api/recipes/get_recipe_links.py +10 -4
  188. uncountable/types/api/recipes/get_recipe_names.py +13 -4
  189. uncountable/types/api/recipes/get_recipe_output_metadata.py +12 -6
  190. uncountable/types/api/recipes/get_recipes_data.py +87 -33
  191. uncountable/types/api/recipes/lock_recipes.py +19 -8
  192. uncountable/types/api/recipes/remove_recipe_from_project.py +10 -3
  193. uncountable/types/api/recipes/set_recipe_inputs.py +16 -10
  194. uncountable/types/api/recipes/set_recipe_metadata.py +10 -3
  195. uncountable/types/api/recipes/set_recipe_output_annotations.py +24 -12
  196. uncountable/types/api/recipes/set_recipe_output_file.py +55 -0
  197. uncountable/types/api/recipes/set_recipe_outputs.py +35 -12
  198. uncountable/types/api/recipes/set_recipe_tags.py +26 -9
  199. uncountable/types/api/recipes/set_recipe_total.py +59 -0
  200. uncountable/types/api/recipes/unarchive_recipes.py +10 -3
  201. uncountable/types/api/recipes/unlock_recipes.py +14 -6
  202. uncountable/types/api/runsheet/__init__.py +1 -0
  203. uncountable/types/api/runsheet/complete_async_upload.py +41 -0
  204. uncountable/types/api/triggers/run_trigger.py +11 -4
  205. uncountable/types/api/uploader/complete_async_parse.py +46 -0
  206. uncountable/types/api/uploader/invoke_uploader.py +13 -6
  207. uncountable/types/api/user/__init__.py +1 -0
  208. uncountable/types/api/user/get_current_user_info.py +40 -0
  209. uncountable/types/async_batch.py +2 -1
  210. uncountable/types/async_batch_processor.py +618 -18
  211. uncountable/types/async_batch_t.py +54 -7
  212. uncountable/types/async_jobs.py +8 -0
  213. uncountable/types/async_jobs_t.py +52 -0
  214. uncountable/types/auth_retrieval.py +11 -0
  215. uncountable/types/auth_retrieval_t.py +75 -0
  216. uncountable/types/base.py +0 -1
  217. uncountable/types/base_t.py +13 -11
  218. uncountable/types/calculations.py +0 -1
  219. uncountable/types/calculations_t.py +5 -2
  220. uncountable/types/chemical_structure.py +0 -1
  221. uncountable/types/chemical_structure_t.py +6 -5
  222. uncountable/types/client_base.py +751 -70
  223. uncountable/types/client_config.py +1 -1
  224. uncountable/types/client_config_t.py +17 -3
  225. uncountable/types/curves.py +0 -1
  226. uncountable/types/curves_t.py +10 -7
  227. uncountable/types/data.py +12 -0
  228. uncountable/types/data_t.py +103 -0
  229. uncountable/types/entity.py +4 -1
  230. uncountable/types/entity_t.py +125 -7
  231. uncountable/types/experiment_groups.py +0 -1
  232. uncountable/types/experiment_groups_t.py +5 -2
  233. uncountable/types/exports.py +8 -0
  234. uncountable/types/exports_t.py +34 -0
  235. uncountable/types/field_values.py +19 -1
  236. uncountable/types/field_values_t.py +246 -9
  237. uncountable/types/fields.py +0 -1
  238. uncountable/types/fields_t.py +5 -2
  239. uncountable/types/generic_upload.py +6 -1
  240. uncountable/types/generic_upload_t.py +88 -9
  241. uncountable/types/id_source.py +0 -1
  242. uncountable/types/id_source_t.py +26 -7
  243. uncountable/types/identifier.py +0 -1
  244. uncountable/types/identifier_t.py +13 -5
  245. uncountable/types/input_attributes.py +0 -1
  246. uncountable/types/input_attributes_t.py +4 -4
  247. uncountable/types/inputs.py +1 -1
  248. uncountable/types/inputs_t.py +24 -4
  249. uncountable/types/integration_server.py +8 -0
  250. uncountable/types/integration_server_t.py +46 -0
  251. uncountable/types/integration_session.py +10 -0
  252. uncountable/types/integration_session_t.py +60 -0
  253. uncountable/types/integrations.py +10 -0
  254. uncountable/types/integrations_t.py +62 -0
  255. uncountable/types/job_definition.py +4 -6
  256. uncountable/types/job_definition_t.py +96 -65
  257. uncountable/types/listing.py +9 -0
  258. uncountable/types/listing_t.py +51 -0
  259. uncountable/types/notices.py +8 -0
  260. uncountable/types/notices_t.py +37 -0
  261. uncountable/types/notifications.py +11 -0
  262. uncountable/types/notifications_t.py +74 -0
  263. uncountable/types/outputs.py +0 -1
  264. uncountable/types/outputs_t.py +6 -3
  265. uncountable/types/overrides.py +9 -0
  266. uncountable/types/overrides_t.py +49 -0
  267. uncountable/types/permissions.py +0 -1
  268. uncountable/types/permissions_t.py +1 -2
  269. uncountable/types/phases.py +0 -1
  270. uncountable/types/phases_t.py +5 -2
  271. uncountable/types/post_base.py +0 -1
  272. uncountable/types/post_base_t.py +1 -2
  273. uncountable/types/queued_job.py +17 -0
  274. uncountable/types/queued_job_t.py +140 -0
  275. uncountable/types/recipe_identifiers.py +0 -1
  276. uncountable/types/recipe_identifiers_t.py +21 -8
  277. uncountable/types/recipe_inputs.py +0 -1
  278. uncountable/types/recipe_inputs_t.py +1 -2
  279. uncountable/types/recipe_links.py +0 -1
  280. uncountable/types/recipe_links_t.py +7 -4
  281. uncountable/types/recipe_metadata.py +0 -1
  282. uncountable/types/recipe_metadata_t.py +14 -9
  283. uncountable/types/recipe_output_metadata.py +0 -1
  284. uncountable/types/recipe_output_metadata_t.py +5 -2
  285. uncountable/types/recipe_tags.py +0 -1
  286. uncountable/types/recipe_tags_t.py +5 -2
  287. uncountable/types/recipe_workflow_steps.py +0 -1
  288. uncountable/types/recipe_workflow_steps_t.py +14 -7
  289. uncountable/types/recipes.py +0 -1
  290. uncountable/types/recipes_t.py +6 -2
  291. uncountable/types/response.py +0 -1
  292. uncountable/types/response_t.py +3 -2
  293. uncountable/types/secret_retrieval.py +0 -1
  294. uncountable/types/secret_retrieval_t.py +13 -7
  295. uncountable/types/sockets.py +20 -0
  296. uncountable/types/sockets_t.py +169 -0
  297. uncountable/types/structured_filters.py +25 -0
  298. uncountable/types/structured_filters_t.py +248 -0
  299. uncountable/types/units.py +0 -1
  300. uncountable/types/units_t.py +5 -2
  301. uncountable/types/uploader.py +24 -0
  302. uncountable/types/uploader_t.py +222 -0
  303. uncountable/types/users.py +0 -1
  304. uncountable/types/users_t.py +5 -2
  305. uncountable/types/webhook_job.py +9 -0
  306. uncountable/types/webhook_job_t.py +48 -0
  307. uncountable/types/workflows.py +0 -1
  308. uncountable/types/workflows_t.py +10 -4
  309. uncountablepythonsdk-0.0.131.dist-info/METADATA +64 -0
  310. uncountablepythonsdk-0.0.131.dist-info/RECORD +363 -0
  311. {UncountablePythonSDK-0.0.52.dist-info → uncountablepythonsdk-0.0.131.dist-info}/WHEEL +1 -1
  312. UncountablePythonSDK-0.0.52.dist-info/METADATA +0 -56
  313. UncountablePythonSDK-0.0.52.dist-info/RECORD +0 -246
  314. docs/quickstart.md +0 -19
  315. uncountable/core/version.py +0 -11
  316. {UncountablePythonSDK-0.0.52.dist-info → uncountablepythonsdk-0.0.131.dist-info}/top_level.txt +0 -0
examples/upload_files.py CHANGED
@@ -12,8 +12,7 @@ client = Client(
12
12
  )
13
13
  uploaded = client.upload_files(
14
14
  file_uploads=[
15
- MediaFileUpload(path="/my/file/path.doc"),
16
- MediaFileUpload(path="/my/file/path2.doc"),
15
+ MediaFileUpload(path="Downloads/file"),
17
16
  ]
18
17
  )
19
18
  pprint(uploaded)
@@ -1,6 +1,15 @@
1
+ from ._is_enum import is_string_enum_class as is_string_enum_class
1
2
  from .argument_parser import CachedParser as CachedParser
3
+ from .argument_parser import ParserBase as ParserBase
4
+ from .argument_parser import ParserError as ParserError
5
+ from .argument_parser import ParserExtraFieldsError as ParserExtraFieldsError
6
+ from .argument_parser import ParserFunction as ParserFunction
2
7
  from .argument_parser import ParserOptions as ParserOptions
8
+ from .argument_parser import SourceEncoding as SourceEncoding
3
9
  from .argument_parser import build_parser as build_parser
10
+ from .argument_parser import is_missing as is_missing
11
+ from .argument_parser import is_optional as is_optional
12
+ from .argument_parser import is_union as is_union
4
13
  from .case_convert import camel_to_snake_case as camel_to_snake_case
5
14
  from .case_convert import kebab_to_pascal_case as kebab_to_pascal_case
6
15
  from .case_convert import snake_to_camel_case as snake_to_camel_case
@@ -5,6 +5,9 @@ def is_namedtuple_type(x: Any) -> bool:
5
5
  if not hasattr(x, "__annotations__"):
6
6
  return False
7
7
 
8
+ if not hasattr(x, "__bases__"):
9
+ return False
10
+
8
11
  b = x.__bases__
9
12
  if len(b) != 1 or b[0] is not tuple:
10
13
  return False
@@ -1,20 +1,25 @@
1
+ from __future__ import annotations
2
+
1
3
  import dataclasses
4
+ import datetime
5
+ import math
2
6
  import types
3
7
  import typing
8
+ from abc import ABC, abstractmethod
4
9
  from collections import defaultdict
5
- from dataclasses import MISSING, dataclass
6
- from datetime import date, datetime
10
+ from datetime import date
7
11
  from decimal import Decimal
12
+ from enum import Enum, auto
8
13
  from importlib import resources
9
14
 
10
15
  import dateutil.parser
16
+ import msgspec.yaml
11
17
 
12
18
  from pkgs.serialization import (
13
19
  MissingSentryType,
14
20
  OpaqueKey,
15
21
  get_serial_class_data,
16
22
  get_serial_union_data,
17
- yaml,
18
23
  )
19
24
 
20
25
  from ._is_enum import is_string_enum_class
@@ -26,13 +31,41 @@ ParserFunction = typing.Callable[[typing.Any], T]
26
31
  ParserCache = dict[type[typing.Any], ParserFunction[typing.Any]]
27
32
 
28
33
 
29
- @dataclass(frozen=True, eq=True)
34
+ class SourceEncoding(Enum):
35
+ API = auto()
36
+ STORAGE = auto()
37
+
38
+
39
+ @dataclasses.dataclass(frozen=True, eq=True)
30
40
  class ParserOptions:
31
- convert_to_snake_case: bool
41
+ encoding: SourceEncoding
32
42
  strict_property_parsing: bool = False
33
43
 
44
+ @staticmethod
45
+ def Api(*, strict_property_parsing: bool = False) -> ParserOptions:
46
+ return ParserOptions(
47
+ encoding=SourceEncoding.API, strict_property_parsing=strict_property_parsing
48
+ )
49
+
50
+ @staticmethod
51
+ def Storage(*, strict_property_parsing: bool = False) -> ParserOptions:
52
+ return ParserOptions(
53
+ encoding=SourceEncoding.STORAGE,
54
+ strict_property_parsing=strict_property_parsing,
55
+ )
56
+
57
+ @property
58
+ def from_camel_case(self) -> bool:
59
+ return self.encoding == SourceEncoding.API
60
+
61
+ @property
62
+ def allow_direct_type(self) -> bool:
63
+ """This allows parsing from a DB column without having to check whether it's
64
+ the native format of the type, a JSON column, or a string encoding."""
65
+ return self.encoding == SourceEncoding.STORAGE
66
+
34
67
 
35
- @dataclass(frozen=True)
68
+ @dataclasses.dataclass(frozen=True)
36
69
  class ParserContext:
37
70
  options: ParserOptions
38
71
  cache: ParserCache
@@ -51,20 +84,38 @@ class ParserExtraFieldsError(ParserError):
51
84
  return f"extra fields were provided: {', '.join(self.extra_fields)}"
52
85
 
53
86
 
87
+ def is_union(field_type: typing.Any) -> bool:
88
+ origin = typing.get_origin(field_type)
89
+ return origin is typing.Union or origin is types.UnionType
90
+
91
+
54
92
  def is_optional(field_type: typing.Any) -> bool:
55
- return typing.get_origin(field_type) is typing.Union and type(
56
- None
57
- ) in typing.get_args(field_type)
93
+ return is_union(field_type) and type(None) in typing.get_args(field_type)
58
94
 
59
95
 
60
96
  def is_missing(field_type: typing.Any) -> bool:
61
- origin = typing.get_origin(field_type)
62
- if origin is not typing.Union:
97
+ if not is_union(field_type):
63
98
  return False
64
99
  args = typing.get_args(field_type)
65
100
  return not (len(args) == 0 or args[0] is not MissingSentryType)
66
101
 
67
102
 
103
+ def _has_field_default(field: dataclasses.Field[typing.Any]) -> bool:
104
+ return (
105
+ field.default != dataclasses.MISSING
106
+ and not isinstance(field.default, MissingSentryType)
107
+ ) or field.default_factory != dataclasses.MISSING
108
+
109
+
110
+ def _get_field_default(
111
+ field: dataclasses.Field[typing.Any],
112
+ ) -> typing.Any:
113
+ if field.default != dataclasses.MISSING:
114
+ return field.default
115
+ assert field.default_factory != dataclasses.MISSING
116
+ return field.default_factory()
117
+
118
+
68
119
  def _invoke_tuple_parsers(
69
120
  tuple_type: type[T],
70
121
  arg_parsers: typing.Sequence[typing.Callable[[typing.Any], object]],
@@ -117,11 +168,39 @@ def _invoke_membership_parser(
117
168
  raise ValueError(f"Expected value from {expected_values} but got value {value}")
118
169
 
119
170
 
171
+ # Uses `is` to compare
172
+ def _build_identity_parser(
173
+ identity_value: T,
174
+ ) -> ParserFunction[T]:
175
+ def parse(value: typing.Any) -> T:
176
+ if value is identity_value:
177
+ return identity_value
178
+ raise ValueError(
179
+ f"Expected value {identity_value} (type: {type(identity_value)}) but got value {value} (type: {type(value)})"
180
+ )
181
+
182
+ return parse
183
+
184
+
185
+ NONE_IDENTITY_PARSER = _build_identity_parser(None)
186
+
187
+
120
188
  def _build_parser_discriminated_union(
121
- discriminator: str, discriminator_map: dict[str, ParserFunction[T]]
189
+ context: ParserContext,
190
+ discriminator_raw: str,
191
+ discriminator_map: dict[str, ParserFunction[T]],
122
192
  ) -> ParserFunction[T]:
193
+ discriminator = (
194
+ snake_to_camel_case(discriminator_raw)
195
+ if context.options.from_camel_case
196
+ else discriminator_raw
197
+ )
198
+
123
199
  def parse(value: typing.Any) -> typing.Any:
124
- discriminant = value.get(discriminator)
200
+ if context.options.allow_direct_type and dataclasses.is_dataclass(value):
201
+ discriminant = getattr(value, discriminator)
202
+ else:
203
+ discriminant = value.get(discriminator)
125
204
  if discriminant is None:
126
205
  raise ValueError("missing-union-discriminant")
127
206
  if not isinstance(discriminant, str):
@@ -137,20 +216,10 @@ def _build_parser_discriminated_union(
137
216
  def _build_parser_inner(
138
217
  parsed_type: type[T],
139
218
  context: ParserContext,
140
- *,
141
- convert_string_to_snake_case: bool = False,
142
219
  ) -> ParserFunction[T]:
143
220
  """
144
- convert_to_snake_case - internal flag
145
- if convert_to_snake_case is True, and parsed_type is str,
146
- then the generated parser will convert camel to snake case case
147
- should only be True for cases like dictionary keys
148
- should only be True if options.convert_to_snake_case is True
149
-
150
- NOTE: This argument makes caching at this level difficult, as the cache-map
151
- would need to vary based on this argument. For this reason only dataclasses
152
- are cached now, as they don't use the argument, and they're known to be safe.
153
- This is also enough to support some recursion.
221
+ IMPROVE: We can now cache at this level, to avoid producing redundant
222
+ internal parsers.
154
223
  """
155
224
 
156
225
  serial_union = get_serial_union_data(parsed_type)
@@ -162,6 +231,7 @@ def _build_parser_inner(
162
231
  parsed_type = serial_union.get_union_underlying()
163
232
  else:
164
233
  return _build_parser_discriminated_union(
234
+ context,
165
235
  discriminator,
166
236
  {
167
237
  key: _build_parser_inner(value, context)
@@ -170,7 +240,7 @@ def _build_parser_inner(
170
240
  )
171
241
 
172
242
  if dataclasses.is_dataclass(parsed_type):
173
- return _build_parser_dataclass(parsed_type, context) # type: ignore[arg-type]
243
+ return _build_parser_dataclass(parsed_type, context)
174
244
 
175
245
  # namedtuple support
176
246
  if is_namedtuple_type(parsed_type):
@@ -183,15 +253,17 @@ def _build_parser_inner(
183
253
  field_name: field_parser(
184
254
  value.get(
185
255
  snake_to_camel_case(field_name)
186
- if context.options.convert_to_snake_case
256
+ if context.options.from_camel_case
187
257
  else field_name
188
258
  )
189
259
  )
190
260
  for field_name, field_parser in field_parsers
191
261
  })
192
262
 
263
+ # IMPROVE: unclear why we need == here
193
264
  if parsed_type == type(None): # noqa: E721
194
- return lambda value: _invoke_membership_parser({None}, value) # type: ignore
265
+ # Need to convince type checker that parsed_type is type(None)
266
+ return typing.cast(ParserFunction[T], NONE_IDENTITY_PARSER)
195
267
 
196
268
  origin = typing.get_origin(parsed_type)
197
269
  if origin is tuple:
@@ -218,7 +290,7 @@ def _build_parser_inner(
218
290
  arg_parsers = [_build_parser_inner(arg, context) for arg in sorted_args]
219
291
  return lambda value: _invoke_fallback_parsers(parsed_type, arg_parsers, value)
220
292
 
221
- if parsed_type is typing.Any: # type: ignore[comparison-overlap]
293
+ if parsed_type is typing.Any:
222
294
  return lambda value: value
223
295
 
224
296
  if origin in (list, set):
@@ -244,45 +316,70 @@ def _build_parser_inner(
244
316
  args = typing.get_args(parsed_type)
245
317
  if len(args) != 2:
246
318
  raise ValueError("Dict types only support two arguments for now")
247
- k_parser = _build_parser_inner(
319
+ k_inner_parser = _build_parser_inner(
248
320
  args[0],
249
321
  context,
250
- convert_string_to_snake_case=context.options.convert_to_snake_case,
251
322
  )
323
+
324
+ def key_parser(value: typing.Any) -> object:
325
+ inner = k_inner_parser(value)
326
+ if (
327
+ isinstance(inner, str)
328
+ # enum keys and OpaqueData's would also have string value types,
329
+ # but their explicit type is not a string, thus shouldn't be converted
330
+ and args[0] is str
331
+ and context.options.from_camel_case
332
+ ):
333
+ return camel_to_snake_case(value)
334
+ return inner
335
+
252
336
  v_parser = _build_parser_inner(args[1], context)
253
- return lambda value: origin((k_parser(k), v_parser(v)) for k, v in value.items())
337
+ return lambda value: origin(
338
+ (key_parser(k), v_parser(v)) for k, v in value.items()
339
+ )
254
340
 
255
341
  if origin == typing.Literal:
256
342
  valid_values: set[T] = set(typing.get_args(parsed_type))
257
343
  return lambda value: _invoke_membership_parser(valid_values, value)
258
344
 
259
- if parsed_type is str and convert_string_to_snake_case:
260
- return lambda value: camel_to_snake_case(value) # type: ignore
261
-
262
345
  if parsed_type is int:
263
346
  # first parse ints to decimal to allow scientific notation and decimals
264
347
  # e.g. (1) 1e4 => 1000, (2) 3.0 => 3
265
348
 
266
349
  def parse_int(value: typing.Any) -> T:
267
350
  if isinstance(value, str):
268
- assert (
269
- "_" not in value
270
- ), "numbers with underscores not considered integers"
351
+ assert "_" not in value, (
352
+ "numbers with underscores not considered integers"
353
+ )
271
354
 
272
355
  dec_value = Decimal(value)
273
356
  int_value = int(dec_value)
274
- assert (
275
- int_value == dec_value
276
- ), f"value ({value}) cannot be parsed to int without discarding precision"
357
+ assert int_value == dec_value, (
358
+ f"value ({value}) cannot be parsed to int without discarding precision"
359
+ )
277
360
  return int_value # type: ignore
278
361
 
279
362
  return parse_int
280
363
 
281
- if parsed_type is datetime:
282
- return lambda value: dateutil.parser.isoparse(value) # type:ignore
364
+ if parsed_type is datetime.datetime:
365
+
366
+ def parse_datetime(value: typing.Any) -> T:
367
+ if context.options.allow_direct_type and isinstance(
368
+ value, datetime.datetime
369
+ ):
370
+ return value # type: ignore
371
+ return dateutil.parser.isoparse(value) # type:ignore
372
+
373
+ return parse_datetime
283
374
 
284
375
  if parsed_type is date:
285
- return lambda value: date.fromisoformat(value) # type:ignore
376
+
377
+ def parse_date(value: typing.Any) -> T:
378
+ if context.options.allow_direct_type and isinstance(value, date):
379
+ return value # type:ignore
380
+ return date.fromisoformat(value) # type:ignore
381
+
382
+ return parse_date
286
383
 
287
384
  # MyPy: It's unclear why `parsed_type in (str, OpaqueKey)` is flagged as invalid
288
385
  # Thus an or statement is used instead, which isn't flagged as invalid.
@@ -297,7 +394,18 @@ def _build_parser_inner(
297
394
 
298
395
  return parse_str
299
396
 
300
- if parsed_type in (float, dict, bool, Decimal) or is_string_enum_class(parsed_type):
397
+ if parsed_type in (float, Decimal):
398
+
399
+ def parse_as_numeric_type(value: typing.Any) -> T:
400
+ numeric_value: Decimal | float = parsed_type(value) # type: ignore
401
+ if math.isnan(numeric_value):
402
+ raise ValueError(f"Invalid numeric value: {numeric_value}")
403
+
404
+ return numeric_value # type: ignore
405
+
406
+ return parse_as_numeric_type
407
+
408
+ if parsed_type in (dict, bool) or is_string_enum_class(parsed_type):
301
409
  return lambda value: parsed_type(value) # type: ignore
302
410
 
303
411
  if parsed_type is MissingSentryType:
@@ -306,7 +414,17 @@ def _build_parser_inner(
306
414
  raise ValueError("Missing type cannot be parsed directly")
307
415
 
308
416
  return error
309
- raise ValueError(f"Unhandled type {parsed_type}")
417
+
418
+ # Check last for generic annotated types and process them unwrapped
419
+ # this must be last, since some of the expected types, like Unions,
420
+ # will also be annotated, but have a special form
421
+ if typing.get_origin(parsed_type) is typing.Annotated:
422
+ return _build_parser_inner(
423
+ parsed_type.__origin__, # type: ignore[attr-defined]
424
+ context,
425
+ )
426
+
427
+ raise ValueError(f"Unhandled type {parsed_type}/{origin}")
310
428
 
311
429
 
312
430
  def _build_parser_dataclass(
@@ -321,8 +439,7 @@ def _build_parser_dataclass(
321
439
  cur_parser = context.cache.get(parsed_type)
322
440
  if cur_parser is not None:
323
441
  return cur_parser
324
-
325
- type_hints = typing.get_type_hints(parsed_type)
442
+ type_hints = typing.get_type_hints(parsed_type, include_extras=True)
326
443
  dc_field_parsers: list[
327
444
  tuple[
328
445
  dataclasses.Field[typing.Any],
@@ -337,29 +454,31 @@ def _build_parser_dataclass(
337
454
  return (
338
455
  snake_to_camel_case(field_name)
339
456
  if (
340
- context.options.convert_to_snake_case
457
+ context.options.from_camel_case
341
458
  and not serial_class_data.has_unconverted_key(field_name)
342
459
  )
343
460
  else field_name
344
461
  )
345
462
 
346
463
  def parse(value: typing.Any) -> typing.Any:
464
+ # Use an exact type match to prevent base/derived class mismatches
465
+ if context.options.allow_direct_type and type(value) is parsed_type:
466
+ return value
467
+
347
468
  data: dict[typing.Any, typing.Any] = {}
348
469
  for field, field_type, field_parser in dc_field_parsers:
349
470
  field_raw_value = None
350
471
  try:
351
472
  field_raw_value = value.get(
352
473
  resolve_serialized_field_name(field_name=field.name),
353
- MISSING,
474
+ dataclasses.MISSING,
354
475
  )
355
476
  field_value: typing.Any
356
- if field_raw_value == MISSING:
477
+ if field_raw_value == dataclasses.MISSING:
357
478
  if serial_class_data.has_parse_require(field.name):
358
479
  raise ValueError("missing-required-field", field.name)
359
- if field.default != MISSING:
360
- field_value = field.default
361
- elif field.default_factory != MISSING:
362
- field_value = field.default_factory()
480
+ if _has_field_default(field):
481
+ field_value = _get_field_default(field)
363
482
  elif is_missing(field_type):
364
483
  field_value = MissingSentryType()
365
484
  elif is_optional(field_type):
@@ -371,6 +490,13 @@ def _build_parser_dataclass(
371
490
  field_value = False
372
491
  else:
373
492
  raise ValueError("missing-value-for-field", field.name)
493
+ elif (
494
+ field_raw_value is None
495
+ and not is_optional(field_type)
496
+ and _has_field_default(field)
497
+ and not serial_class_data.has_parse_require(field.name)
498
+ ):
499
+ field_value = _get_field_default(field)
374
500
  elif serial_class_data.has_unconverted_value(field.name):
375
501
  field_value = field_raw_value
376
502
  else:
@@ -438,15 +564,46 @@ def build_parser(
438
564
  return built_parser
439
565
 
440
566
 
441
- class CachedParser(typing.Generic[T]):
567
+ class ParserBase(ABC, typing.Generic[T]):
568
+ def parse_from_encoding(
569
+ self,
570
+ args: typing.Any,
571
+ *,
572
+ source_encoding: SourceEncoding,
573
+ ) -> T:
574
+ match source_encoding:
575
+ case SourceEncoding.API:
576
+ return self.parse_api(args)
577
+ case SourceEncoding.STORAGE:
578
+ return self.parse_storage(args)
579
+ case _:
580
+ typing.assert_never(source_encoding)
581
+
582
+ # IMPROVE: Args would be better typed as "object"
583
+ @abstractmethod
584
+ def parse_storage(self, args: typing.Any) -> T: ...
585
+
586
+ @abstractmethod
587
+ def parse_api(self, args: typing.Any) -> T: ...
588
+
589
+ def parse_yaml_file(self, path: str) -> T:
590
+ with open(path, encoding="utf-8") as data_in:
591
+ return self.parse_storage(msgspec.yaml.decode(data_in.read()))
592
+
593
+ def parse_yaml_resource(self, package: resources.Package, resource: str) -> T:
594
+ with resources.open_text(package, resource) as fp:
595
+ return self.parse_storage(msgspec.yaml.decode(fp.read()))
596
+
597
+
598
+ class CachedParser(ParserBase[T], typing.Generic[T]):
442
599
  def __init__(
443
600
  self,
444
601
  args: type[T],
445
602
  strict_property_parsing: bool = False,
446
603
  ):
447
604
  self.arguments = args
448
- self.parser_api: typing.Optional[ParserFunction[T]] = None
449
- self.parser_storage: typing.Optional[ParserFunction[T]] = None
605
+ self.parser_api: ParserFunction[T] | None = None
606
+ self.parser_storage: ParserFunction[T] | None = None
450
607
  self.strict_property_parsing = strict_property_parsing
451
608
 
452
609
  def parse_api(self, args: typing.Any) -> T:
@@ -460,8 +617,7 @@ class CachedParser(typing.Generic[T]):
460
617
  if self.parser_api is None:
461
618
  self.parser_api = build_parser(
462
619
  self.arguments,
463
- ParserOptions(
464
- convert_to_snake_case=True,
620
+ ParserOptions.Api(
465
621
  strict_property_parsing=self.strict_property_parsing,
466
622
  ),
467
623
  )
@@ -475,18 +631,9 @@ class CachedParser(typing.Generic[T]):
475
631
  if self.parser_storage is None:
476
632
  self.parser_storage = build_parser(
477
633
  self.arguments,
478
- ParserOptions(
479
- convert_to_snake_case=False,
634
+ ParserOptions.Storage(
480
635
  strict_property_parsing=self.strict_property_parsing,
481
636
  ),
482
637
  )
483
638
  assert self.parser_storage is not None
484
639
  return self.parser_storage(args)
485
-
486
- def parse_yaml_file(self, path: str) -> T:
487
- with open(path, encoding="utf-8") as data_in:
488
- return self.parse_storage(yaml.safe_load(data_in))
489
-
490
- def parse_yaml_resource(self, package: resources.Package, resource: str) -> T:
491
- with resources.open_text(package, resource) as fp:
492
- return self.parse_storage(yaml.safe_load(fp))
@@ -17,3 +17,4 @@ from .file_type_utils import FileSystemSFTPConfig as FileSystemSFTPConfig
17
17
  from .file_type_utils import FileTransfer as FileTransfer
18
18
  from .file_type_utils import IncompatibleFileReference as IncompatibleFileReference
19
19
  from .file_type_utils import RemoteObjectReference as RemoteObjectReference
20
+ from .filesystem_session import FileSystemSession as FileSystemSession
@@ -0,0 +1,144 @@
1
+ from io import BytesIO
2
+
3
+ from azure.core.credentials import AzureSasCredential
4
+ from azure.storage.blob import BlobServiceClient, ContainerClient
5
+
6
+ from pkgs.filesystem_utils.file_type_utils import (
7
+ FileObjectData,
8
+ FileSystemBlobConfig,
9
+ FileSystemFileReference,
10
+ FileSystemObject,
11
+ FileTransfer,
12
+ IncompatibleFileReference,
13
+ )
14
+
15
+ from .filesystem_session import FileSystemSession
16
+
17
+
18
+ def _add_slash(prefix: str) -> str:
19
+ if len(prefix) > 0 and prefix[-1] != "/":
20
+ prefix = prefix + "/"
21
+ return prefix
22
+
23
+
24
+ class BlobSession(FileSystemSession):
25
+ config: FileSystemBlobConfig
26
+
27
+ def __init__(self, blob_config: FileSystemBlobConfig) -> None:
28
+ super().__init__()
29
+ self.config = blob_config
30
+
31
+ def start(self) -> None:
32
+ self.service_client: BlobServiceClient | None = BlobServiceClient(
33
+ self.config.account_url, credential=self.config.credential
34
+ )
35
+ self.container_client: ContainerClient | None = (
36
+ self.service_client.get_container_client(self.config.container)
37
+ )
38
+
39
+ def __enter__(self) -> "BlobSession":
40
+ self.start()
41
+ return self
42
+
43
+ def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
44
+ self.service_client = None
45
+ self.container_client = None
46
+
47
+ def list_files(
48
+ self,
49
+ dir_path: FileSystemObject,
50
+ *,
51
+ recursive: bool = False,
52
+ valid_extensions: list[str] | None = None,
53
+ ) -> list[FileSystemObject]:
54
+ if not isinstance(dir_path, FileSystemFileReference):
55
+ raise IncompatibleFileReference()
56
+
57
+ assert self.service_client is not None and self.container_client is not None, (
58
+ "call to list_files on uninitialized blob session"
59
+ )
60
+
61
+ filesystem_file_references: list[FileSystemObject] = []
62
+ prefix = _add_slash(dir_path.filepath)
63
+ for blob in self.container_client.list_blobs(name_starts_with=prefix):
64
+ if not recursive and (
65
+ blob.name == prefix or "/" in blob.name[len(prefix) :]
66
+ ):
67
+ continue
68
+ if valid_extensions is None or any(
69
+ blob.name.endswith(valid_extension)
70
+ for valid_extension in valid_extensions
71
+ ):
72
+ filesystem_file_references.append(
73
+ FileSystemFileReference(
74
+ filepath=blob.name,
75
+ )
76
+ )
77
+
78
+ return filesystem_file_references
79
+
80
+ def download_files(
81
+ self,
82
+ filepaths: list[FileSystemObject],
83
+ ) -> list[FileObjectData]:
84
+ downloaded_files: list[FileObjectData] = []
85
+ assert self.service_client is not None and self.container_client is not None, (
86
+ "call to download_files on uninitialized blob session"
87
+ )
88
+
89
+ for file_object in filepaths:
90
+ if (
91
+ not isinstance(file_object, FileSystemFileReference)
92
+ or file_object.filename is None
93
+ ):
94
+ raise IncompatibleFileReference()
95
+
96
+ blob_client = self.container_client.get_blob_client(file_object.filepath)
97
+ download_stream = blob_client.download_blob()
98
+ file_data = download_stream.readall()
99
+ downloaded_files.append(
100
+ FileObjectData(
101
+ file_data=file_data,
102
+ file_IO=BytesIO(file_data),
103
+ filename=file_object.filename,
104
+ filepath=file_object.filepath,
105
+ )
106
+ )
107
+
108
+ return downloaded_files
109
+
110
+ def move_files(self, file_mappings: list[FileTransfer]) -> None:
111
+ assert self.service_client is not None and self.container_client is not None, (
112
+ "call to move_files on uninitialized blob session"
113
+ )
114
+
115
+ for src_file, dest_file in file_mappings:
116
+ if not isinstance(src_file, FileSystemFileReference) or not isinstance(
117
+ dest_file, FileSystemFileReference
118
+ ):
119
+ raise IncompatibleFileReference()
120
+
121
+ source_blob_client = self.container_client.get_blob_client(
122
+ src_file.filepath
123
+ )
124
+ dest_blob_client = self.container_client.get_blob_client(dest_file.filepath)
125
+
126
+ source_url = (
127
+ f"{source_blob_client.url}?{self.config.credential.signature}"
128
+ if isinstance(self.config.credential, AzureSasCredential)
129
+ else source_blob_client.url
130
+ )
131
+
132
+ dest_blob_client.start_copy_from_url(source_url)
133
+ source_blob_client.delete_blob()
134
+
135
+ def delete_files(self, filepaths: list[FileSystemObject]) -> None:
136
+ assert self.service_client is not None and self.container_client is not None, (
137
+ "call to delete_files on uninitialized blob session"
138
+ )
139
+ for file_object in filepaths:
140
+ if not isinstance(file_object, FileSystemFileReference):
141
+ raise IncompatibleFileReference()
142
+
143
+ blob_client = self.container_client.get_blob_client(file_object.filepath)
144
+ blob_client.delete_blob()