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
@@ -13,11 +13,14 @@ One of the following can be specified on the name of a argument:
13
13
  After that you can also specify a `!` indicating the argument may not be null.
14
14
  If this is not specified, then a null input on this argument should produce a null output.
15
15
  We prefer not to use `!` as we want to encourage null pass-through where possible.
16
- If null is allowed as a legitimate value, such as in conditionals like `if`, then `!` must be specified.
16
+
17
+ If null is allowed as a legitimate value, such as in conditionals like `is_null`,
18
+ then `!usenull` must be specified, this distinguishes it from the pass-through case.
19
+ The accepted argument type must accept "None", it is not implied.
17
20
  """
18
21
 
19
22
  import sys
20
- from typing import TypeVar, cast
23
+ from typing import Match, Pattern, TypeVar, cast
21
24
 
22
25
  import regex as re
23
26
 
@@ -53,7 +56,7 @@ class Source:
53
56
  def has_more(self) -> bool:
54
57
  return self._at < len(self._text)
55
58
 
56
- def match(self, expression: re.Pattern) -> re.Match | None:
59
+ def match(self, expression: Pattern[str]) -> Match[str] | None:
57
60
  self.skip_space()
58
61
  m = expression.match(self._text, self._at)
59
62
  if m is not None:
@@ -84,7 +87,7 @@ class Source:
84
87
  return self._text[start : self._at]
85
88
 
86
89
 
87
- _re_argument_name = re.compile(r"([a-z_]+)(\?|\+)?(!)?:")
90
+ _re_argument_name = re.compile(r"([a-z_]+)(\?|\+)?(!|!usenull)?:")
88
91
 
89
92
 
90
93
  def parse_function_signature(text: str) -> ParsedFunctionSignature:
@@ -101,11 +104,18 @@ def parse_function_signature(text: str) -> ParsedFunctionSignature:
101
104
 
102
105
  type_str = source.extract_type()
103
106
  ref_name = arg_group.group(1)
104
- is_missing = arg_group.group(2) == "?"
105
- is_repeating = arg_group.group(2) == "+"
106
- pass_null = arg_group.group(3) is None
107
+ # is_missing = arg_group.group(2) == "?"
108
+ # is_repeating = arg_group.group(2) == "+"
107
109
  type_path = parse_type_str(type_str)
108
110
 
111
+ match arg_group.group(3):
112
+ case "!":
113
+ on_null = value_spec_t.OnNull.DISALLOW
114
+ case "!usenull":
115
+ on_null = value_spec_t.OnNull.USE
116
+ case _:
117
+ on_null = value_spec_t.OnNull.PASS
118
+
109
119
  extant = value_spec_t.ArgumentExtant.REQUIRED
110
120
  extant_marker = arg_group.group(2)
111
121
  if extant_marker == "?":
@@ -116,7 +126,7 @@ def parse_function_signature(text: str) -> ParsedFunctionSignature:
116
126
  arguments.append(
117
127
  ParsedFunctionArgument(
118
128
  ref_name=ref_name,
119
- pass_null=pass_null,
129
+ on_null=on_null,
120
130
  extant=extant,
121
131
  type_path=type_path,
122
132
  )
@@ -145,6 +155,7 @@ key_return = "return"
145
155
  key_description = "description"
146
156
  key_brief = "brief"
147
157
  key_name = "name"
158
+ key_draft = "draft"
148
159
 
149
160
 
150
161
  TypeT = TypeVar("TypeT")
@@ -208,7 +219,7 @@ def main() -> None:
208
219
  name=arg_name,
209
220
  description=arg_description,
210
221
  type=convert_to_value_spec_type(in_argument.type_path),
211
- pass_null=in_argument.pass_null,
222
+ on_null=in_argument.on_null,
212
223
  extant=in_argument.extant,
213
224
  )
214
225
  )
@@ -219,6 +230,11 @@ def main() -> None:
219
230
 
220
231
  brief = get_as(spec, key_brief, str)
221
232
  description = get_as(spec, key_description, str)
233
+ draft = (
234
+ get_as(spec, key_draft, bool)
235
+ if spec.get(key_draft) is not None
236
+ else None
237
+ )
222
238
 
223
239
  return_value = get(spec, key_return)
224
240
  where.append("return")
@@ -235,6 +251,7 @@ def main() -> None:
235
251
  type=convert_to_value_spec_type(parsed.return_type_path),
236
252
  description=return_description,
237
253
  ),
254
+ draft=draft,
238
255
  )
239
256
  )
240
257
  where.pop()
@@ -25,10 +25,17 @@ TYPE_MAP = {
25
25
  "List": MappedType(base_type=value_spec_t.BaseType.LIST, param_count=1),
26
26
  "Optional": MappedType(base_type=value_spec_t.BaseType.OPTIONAL, param_count=1),
27
27
  "String": MappedType(base_type=value_spec_t.BaseType.STRING),
28
- "Union": MappedType(base_type=value_spec_t.BaseType.UNION, variable_param_count=True),
28
+ "Union": MappedType(
29
+ base_type=value_spec_t.BaseType.UNION, variable_param_count=True
30
+ ),
29
31
  # not part of type_spec's types now
30
32
  "Symbol": MappedType(base_type=value_spec_t.BaseType.SYMBOL),
31
33
  "Any": MappedType(base_type=value_spec_t.BaseType.ANY),
34
+ "None": MappedType(base_type=value_spec_t.BaseType.NONE),
35
+ "Tuple": MappedType(
36
+ base_type=value_spec_t.BaseType.TUPLE, variable_param_count=True
37
+ ),
38
+ "Never": MappedType(base_type=value_spec_t.BaseType.NEVER),
32
39
  }
33
40
 
34
41
 
@@ -54,3 +61,16 @@ def convert_to_value_spec_type(parsed: ParsedTypePath) -> value_spec_t.ValueType
54
61
  return value_spec_t.ValueType(base_type=mapped.base_type, parameters=parameters)
55
62
 
56
63
  # Our formatter was duplicating the previous line for an unknown reason, this comment blocks that
64
+
65
+
66
+ def convert_from_value_spec_type(
67
+ base_type: value_spec_t.BaseType,
68
+ ) -> str:
69
+ for type_spec_type, mapped_type in TYPE_MAP.items():
70
+ if (
71
+ mapped_type.base_type == base_type
72
+ and mapped_type.param_count == 0
73
+ and mapped_type.variable_param_count is False
74
+ ):
75
+ return type_spec_type
76
+ raise ValueError(f"invalid value spec type {base_type}")
@@ -70,14 +70,19 @@ def _emit_function_wrapper(function: value_spec_t.Function) -> str:
70
70
  else:
71
71
  python_type = _emit_python_type(argument.type)
72
72
  if (
73
- argument.pass_null
73
+ argument.on_null == value_spec_t.OnNull.PASS
74
74
  or argument.extant == value_spec_t.ArgumentExtant.MISSING
75
75
  ):
76
76
  python_type += " | None"
77
77
  any_pass_null = True
78
+
79
+ if python_type.startswith("base_t.ExtJsonValue"):
80
+ return_statement = f"self._extract({index})"
81
+ else:
82
+ return_statement = f"cast({python_type}, self._extract({index}))"
78
83
  out.write(
79
84
  f"""{INDENT}def get_{argument.ref_name}(self) -> {python_type}:
80
- {INDENT}{INDENT}return cast({python_type}, self._extract({index}))
85
+ {INDENT}{INDENT}return {return_statement}
81
86
  """
82
87
  )
83
88
  out.write("\n")
@@ -160,8 +165,12 @@ def _emit_function(function: value_spec_t.Function, indent: str) -> str:
160
165
  sub_indent = indent + INDENT
161
166
  out.write(f"{_function_symbol_name(function)} = value_spec_t.Function(\n")
162
167
  out.write(f"{sub_indent}name={encode_common_string(function.name)},\n")
163
- out.write(f"{sub_indent}description={encode_common_string(function.description)},\n")
168
+ out.write(
169
+ f"{sub_indent}description={encode_common_string(function.description)},\n"
170
+ )
164
171
  out.write(f"{sub_indent}brief={encode_common_string(function.brief)},\n")
172
+ if function.draft:
173
+ out.write(f"{sub_indent}draft={function.draft},\n")
165
174
  out.write(
166
175
  f"{sub_indent}return_value={_emit_function_return(function.return_value, sub_indent)},\n"
167
176
  )
@@ -184,16 +193,25 @@ def _emit_argument(argument: value_spec_t.FunctionArgument, indent: str) -> str:
184
193
  out.write("value_spec_t.FunctionArgument(\n")
185
194
  out.write(f"{sub_indent}ref_name={encode_common_string(argument.ref_name)},\n")
186
195
  out.write(f"{sub_indent}name={encode_common_string(argument.name)},\n")
187
- out.write(f"{sub_indent}description={encode_common_string(argument.description)},\n")
188
- out.write(f"{sub_indent}pass_null={str(argument.pass_null)},\n")
189
- out.write(f"{sub_indent}extant=value_spec_t.ArgumentExtant.{argument.extant.name},\n")
196
+ out.write(
197
+ f"{sub_indent}description={encode_common_string(argument.description)},\n"
198
+ )
199
+ # Quick enum emit since we have only one such type here
200
+ out.write(
201
+ f"{sub_indent}on_null=value_spec_t.OnNull.{str(argument.on_null).upper()},\n"
202
+ )
203
+ out.write(
204
+ f"{sub_indent}extant=value_spec_t.ArgumentExtant.{argument.extant.name},\n"
205
+ )
190
206
  out.write(f"{sub_indent}type={_emit_type(argument.type, sub_indent)},\n")
191
207
  out.write(f"{indent})")
192
208
 
193
209
  return out.getvalue()
194
210
 
195
211
 
196
- def _emit_function_return(return_value: value_spec_t.FunctionReturn, indent: str) -> str:
212
+ def _emit_function_return(
213
+ return_value: value_spec_t.FunctionReturn, indent: str
214
+ ) -> str:
197
215
  out = io.StringIO()
198
216
 
199
217
  sub_indent = indent + INDENT
@@ -8,7 +8,7 @@ from ..util import ParsedTypePath
8
8
  @dataclass(kw_only=True, frozen=True)
9
9
  class ParsedFunctionArgument:
10
10
  ref_name: str
11
- pass_null: bool
11
+ on_null: value_spec_t.OnNull
12
12
  extant: value_spec_t.ArgumentExtant
13
13
  type_path: ParsedTypePath
14
14
 
@@ -4,7 +4,7 @@ from uncountable.types.async_batch import AsyncBatchRequest
4
4
  from uncountable.types.async_batch_processor import AsyncBatchProcessorBase
5
5
 
6
6
 
7
- class AsyncBatchSubmissionError(BaseException):
7
+ class AsyncBatchSubmissionError(Exception):
8
8
  pass
9
9
 
10
10
 
@@ -1,21 +1,24 @@
1
1
  import base64
2
- import json
2
+ import datetime
3
+ import re
3
4
  import typing
4
5
  from dataclasses import dataclass
5
- from datetime import datetime, timedelta
6
+ from datetime import UTC, timedelta
6
7
  from enum import StrEnum
7
- from urllib.parse import urljoin
8
+ from io import BytesIO
9
+ from urllib.parse import unquote, urljoin
8
10
  from uuid import uuid4
9
11
 
10
12
  import requests
13
+ import simplejson as json
11
14
  from opentelemetry.sdk.resources import Attributes
12
15
  from requests.exceptions import JSONDecodeError
13
16
 
14
17
  from pkgs.argument_parser import CachedParser
15
- from pkgs.serialization_util import serialize_for_api
16
- from pkgs.serialization_util.serialization_helpers import JsonValue
17
- from uncountable.core.version import get_version
18
- from uncountable.integration.telemetry import JobLogger
18
+ from pkgs.serialization_util import JsonValue, serialize_for_api
19
+ from uncountable.core.environment import get_version
20
+ from uncountable.integration.telemetry import Logger, push_scope_optional
21
+ from uncountable.types import download_file_t
19
22
  from uncountable.types.client_base import APIRequest, ClientMethods
20
23
  from uncountable.types.client_config import ClientConfigOptions
21
24
 
@@ -41,14 +44,14 @@ class HTTPRequestBase:
41
44
 
42
45
  @dataclass(kw_only=True)
43
46
  class HTTPGetRequest(HTTPRequestBase):
44
- method = EndpointMethod.GET
47
+ method: EndpointMethod = EndpointMethod.GET
45
48
  query_params: dict[str, str]
46
49
 
47
50
 
48
51
  @dataclass(kw_only=True)
49
52
  class HTTPPostRequest(HTTPRequestBase):
50
- method = EndpointMethod.POST
51
- body: typing.Union[str, dict[str, str]]
53
+ method: EndpointMethod = EndpointMethod.POST
54
+ body: str | dict[str, str]
52
55
 
53
56
 
54
57
  HTTPRequest = HTTPPostRequest | HTTPGetRequest
@@ -56,29 +59,39 @@ HTTPRequest = HTTPPostRequest | HTTPGetRequest
56
59
 
57
60
  @dataclass(kw_only=True)
58
61
  class ClientConfig(ClientConfigOptions):
59
- transform_request: typing.Callable[[requests.Request], requests.Request] | None = None
60
- job_logger: typing.Optional[JobLogger] = None
62
+ transform_request: typing.Callable[[requests.Request], requests.Request] | None = (
63
+ None
64
+ )
65
+ logger: Logger | None = None
61
66
 
62
67
 
63
68
  OAUTH_REFRESH_WINDOW_SECONDS = 60 * 5
64
69
 
65
70
 
66
- class APIResponseError(BaseException):
71
+ class APIResponseError(Exception):
67
72
  status_code: int
68
73
  message: str
69
74
  extra_details: dict[str, JsonValue] | None
70
75
 
71
76
  def __init__(
72
- self, status_code: int, message: str, extra_details: dict[str, JsonValue] | None
77
+ self,
78
+ status_code: int,
79
+ message: str,
80
+ extra_details: dict[str, JsonValue] | None,
81
+ request_id: str,
73
82
  ) -> None:
74
83
  super().__init__(status_code, message, extra_details)
75
84
  self.status_code = status_code
76
85
  self.message = message
77
86
  self.extra_details = extra_details
87
+ self.request_id = request_id
78
88
 
79
89
  @classmethod
80
90
  def construct_error(
81
- cls, status_code: int, extra_details: dict[str, JsonValue] | None
91
+ cls,
92
+ status_code: int,
93
+ extra_details: dict[str, JsonValue] | None,
94
+ request_id: str,
82
95
  ) -> "APIResponseError":
83
96
  message: str
84
97
  match status_code:
@@ -101,11 +114,23 @@ class APIResponseError(BaseException):
101
114
  case _:
102
115
  message = "unknown error"
103
116
  return APIResponseError(
104
- status_code=status_code, message=message, extra_details=extra_details
117
+ status_code=status_code,
118
+ message=message,
119
+ extra_details=extra_details,
120
+ request_id=request_id,
105
121
  )
106
122
 
123
+ def __str__(self) -> str:
124
+ details_obj = {
125
+ "request_id": self.request_id,
126
+ "status_code": self.status_code,
127
+ "extra_details": self.extra_details,
128
+ }
129
+ details = json.dumps(details_obj)
130
+ return f"API response error ({self.status_code}): '{self.message}'. Details: {details}"
107
131
 
108
- class SDKError(BaseException):
132
+
133
+ class SDKError(Exception):
109
134
  message: str
110
135
  request_id: str
111
136
 
@@ -121,7 +146,7 @@ class SDKError(BaseException):
121
146
  @dataclass(kw_only=True)
122
147
  class OAuthBearerTokenCache:
123
148
  token: str
124
- expires_at: datetime
149
+ expires_at: datetime.datetime
125
150
 
126
151
 
127
152
  @dataclass(kw_only=True)
@@ -135,6 +160,16 @@ class GetOauthBearerTokenData:
135
160
  oauth_bearer_token_data_parser = CachedParser(GetOauthBearerTokenData)
136
161
 
137
162
 
163
+ @dataclass
164
+ class DownloadedFile:
165
+ name: str
166
+ size: int
167
+ data: BytesIO
168
+
169
+
170
+ DownloadedFiles = list[DownloadedFile]
171
+
172
+
138
173
  class Client(ClientMethods):
139
174
  _parser_map: dict[type, CachedParser] = {}
140
175
  _auth_details: AuthDetailsAll
@@ -153,37 +188,53 @@ class Client(ClientMethods):
153
188
  ):
154
189
  self._auth_details = auth_details
155
190
  self._base_url = base_url
156
- self._file_uploader = FileUploader(self._base_url, self._auth_details)
157
191
  self._cfg = config or ClientConfig()
158
192
  self._session = requests.Session()
159
193
  self._session.verify = not self._cfg.allow_insecure_tls
194
+ self._file_uploader = FileUploader(
195
+ self._base_url,
196
+ self._auth_details,
197
+ self._cfg.allow_insecure_tls,
198
+ logger=self._cfg.logger,
199
+ )
160
200
 
161
- def _get_response_json(
162
- self, response: requests.Response, request_id: str
163
- ) -> dict[str, JsonValue]:
201
+ @classmethod
202
+ def _validate_response_status(
203
+ cls, response: requests.Response, request_id: str
204
+ ) -> None:
164
205
  if response.status_code < 200 or response.status_code > 299:
165
206
  extra_details: dict[str, JsonValue] | None = None
166
207
  try:
167
208
  data = response.json()
168
- if "error" in data:
169
- extra_details = data
209
+ extra_details = data
170
210
  except JSONDecodeError:
171
- pass
211
+ extra_details = {
212
+ "body": response.text,
213
+ }
172
214
  raise APIResponseError.construct_error(
173
- status_code=response.status_code, extra_details=extra_details
215
+ status_code=response.status_code,
216
+ extra_details=extra_details,
217
+ request_id=request_id,
174
218
  )
219
+
220
+ def _get_response_json(
221
+ self, response: requests.Response, request_id: str
222
+ ) -> dict[str, JsonValue]:
223
+ self._validate_response_status(response, request_id)
175
224
  try:
176
225
  return typing.cast(dict[str, JsonValue], response.json())
177
226
  except JSONDecodeError as e:
178
227
  raise SDKError("unable to process response", request_id=request_id) from e
179
228
 
180
- def _send_request(self, request: requests.Request) -> requests.Response:
229
+ def _send_request(
230
+ self, request: requests.Request, *, timeout: float | None = None
231
+ ) -> requests.Response:
181
232
  if self._cfg.extra_headers is not None:
182
233
  request.headers = {**request.headers, **self._cfg.extra_headers}
183
234
  if self._cfg.transform_request is not None:
184
235
  request = self._cfg.transform_request(request)
185
236
  prepared_request = request.prepare()
186
- response = self._session.send(prepared_request)
237
+ response = self._session.send(prepared_request, timeout=timeout)
187
238
  return response
188
239
 
189
240
  def do_request(self, *, api_request: APIRequest, return_type: type[DT]) -> DT:
@@ -201,15 +252,19 @@ class Client(ClientMethods):
201
252
  case _:
202
253
  typing.assert_never(http_request)
203
254
  request.headers = http_request.headers
204
- if self._cfg.job_logger is not None:
205
- attributes: Attributes = {
206
- "method": http_request.method,
207
- "endpoint": api_request.endpoint,
208
- }
209
- with self._cfg.job_logger.push_scope("api_call", attributes=attributes):
210
- response = self._send_request(request)
211
- else:
212
- response = self._send_request(request)
255
+ attributes: Attributes = {
256
+ "method": http_request.method,
257
+ "endpoint": api_request.endpoint,
258
+ }
259
+ with push_scope_optional(self._cfg.logger, "api_call", attributes=attributes):
260
+ if self._cfg.logger is not None:
261
+ self._cfg.logger.log_info(api_request.endpoint, attributes=attributes)
262
+ timeout = (
263
+ api_request.request_options.timeout_secs
264
+ if api_request.request_options is not None
265
+ else None
266
+ )
267
+ response = self._send_request(request, timeout=timeout)
213
268
  response_data = self._get_response_json(response, request_id=request_id)
214
269
  cached_parser = self._get_cached_parser(return_type)
215
270
  try:
@@ -227,7 +282,8 @@ class Client(ClientMethods):
227
282
  if (
228
283
  self._oauth_bearer_token_cache is None
229
284
  or (
230
- self._oauth_bearer_token_cache.expires_at - datetime.now()
285
+ self._oauth_bearer_token_cache.expires_at
286
+ - datetime.datetime.now(tz=UTC)
231
287
  ).total_seconds()
232
288
  < OAUTH_REFRESH_WINDOW_SECONDS
233
289
  ):
@@ -243,7 +299,8 @@ class Client(ClientMethods):
243
299
  token_data = oauth_bearer_token_data_parser.parse_storage(data)
244
300
  self._oauth_bearer_token_cache = OAuthBearerTokenCache(
245
301
  token=token_data.access_token,
246
- expires_at=datetime.now() + timedelta(seconds=token_data.expires_in),
302
+ expires_at=datetime.datetime.now(tz=UTC)
303
+ + timedelta(seconds=token_data.expires_in),
247
304
  )
248
305
 
249
306
  return self._oauth_bearer_token_cache.token
@@ -286,6 +343,52 @@ class Client(ClientMethods):
286
343
  case _:
287
344
  raise ValueError(f"unsupported request method: {method}")
288
345
 
346
+ def _get_downloaded_filename(self, *, cd: str | None) -> str:
347
+ if not cd:
348
+ return "Unknown"
349
+
350
+ fname = re.findall(r"filename\*=UTF-8''(.+)", cd)
351
+ if fname:
352
+ return unquote(fname[0])
353
+
354
+ fname = re.findall(r'filename="?(.+)"?', cd)
355
+ if fname:
356
+ return str(fname[0].strip('"'))
357
+
358
+ return "Unknown"
359
+
360
+ def download_files(
361
+ self, *, file_query: download_file_t.FileDownloadQuery
362
+ ) -> DownloadedFiles:
363
+ """Download a file from uncountable."""
364
+ request_id = str(uuid4())
365
+ api_request = APIRequest(
366
+ method=download_file_t.ENDPOINT_METHOD,
367
+ endpoint=download_file_t.ENDPOINT_PATH,
368
+ args=download_file_t.Arguments(
369
+ file_query=file_query,
370
+ ),
371
+ )
372
+ http_request = self._build_http_request(
373
+ api_request=api_request, request_id=request_id
374
+ )
375
+ request = requests.Request(http_request.method.value, http_request.url)
376
+ request.headers = http_request.headers
377
+ assert isinstance(http_request, HTTPGetRequest)
378
+ request.params = http_request.query_params
379
+ response = self._send_request(request)
380
+ self._validate_response_status(response, request_id)
381
+
382
+ content = response.content
383
+ content_disposition = response.headers.get("Content-Disposition", None)
384
+ return [
385
+ DownloadedFile(
386
+ name=self._get_downloaded_filename(cd=content_disposition),
387
+ size=len(content),
388
+ data=BytesIO(content),
389
+ )
390
+ ]
391
+
289
392
  def upload_files(
290
393
  self: typing.Self, *, file_uploads: list[FileUpload]
291
394
  ) -> list[UploadedFile]:
@@ -0,0 +1,41 @@
1
+ import functools
2
+ import os
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ from uncountable.types import integration_server_t
6
+
7
+
8
+ @functools.cache
9
+ def get_version() -> str:
10
+ try:
11
+ version_str = version("UncountablePythonSDK")
12
+ except PackageNotFoundError:
13
+ version_str = "unknown"
14
+ return version_str
15
+
16
+
17
+ def get_server_env() -> str | None:
18
+ return os.environ.get("UNC_SERVER_ENV")
19
+
20
+
21
+ def get_http_server_port() -> int:
22
+ return int(os.environ.get("UNC_WEBHOOK_SERVER_PORT", "5001"))
23
+
24
+
25
+ def get_local_admin_server_port() -> int:
26
+ return int(os.environ.get("UNC_ADMIN_SERVER_PORT", "50051"))
27
+
28
+
29
+ def get_otel_enabled() -> bool:
30
+ return os.environ.get("UNC_OTEL_ENABLED") == "true"
31
+
32
+
33
+ def get_profiles_module() -> str:
34
+ return os.environ["UNC_PROFILES_MODULE"]
35
+
36
+
37
+ def get_integration_envs() -> list[integration_server_t.IntegrationEnvironment]:
38
+ return [
39
+ integration_server_t.IntegrationEnvironment(env)
40
+ for env in os.environ.get("UNC_INTEGRATION_ENVS", "prod").split(",")
41
+ ]
@@ -4,11 +4,13 @@ from dataclasses import dataclass
4
4
  from enum import StrEnum
5
5
  from io import BytesIO
6
6
  from pathlib import Path
7
- from typing import Generator, Literal, Self
7
+ from typing import Generator, Literal, Self, assert_never
8
8
 
9
9
  import aiohttp
10
10
  import aiotus
11
11
 
12
+ from uncountable.integration.telemetry import Logger, push_scope_optional
13
+
12
14
  from .types import AuthDetailsAll, AuthDetailsApiKey
13
15
 
14
16
  _CHUNK_SIZE = 5 * 1024 * 1024 # s3 requires 5MiB minimum
@@ -68,10 +70,19 @@ class UploadFailed(Exception):
68
70
  class FileUploader:
69
71
  _auth_details: AuthDetailsAll
70
72
  _base_url: str
71
-
72
- def __init__(self: Self, base_url: str, auth_details: AuthDetailsAll) -> None:
73
+ _allow_insecure_tls: bool
74
+
75
+ def __init__(
76
+ self: Self,
77
+ base_url: str,
78
+ auth_details: AuthDetailsAll,
79
+ allow_insecure_tls: bool = False,
80
+ logger: Logger | None = None,
81
+ ) -> None:
73
82
  self._base_url = base_url
74
83
  self._auth_details = auth_details
84
+ self._allow_insecure_tls = allow_insecure_tls
85
+ self._logger = logger
75
86
 
76
87
  async def _upload_file(self: Self, file_upload: FileUpload) -> UploadedFile:
77
88
  creation_url = f"{self._base_url}/api/external/file_upload/files"
@@ -86,21 +97,44 @@ class FileUploader:
86
97
  auth=auth, headers={"Origin": self._base_url}
87
98
  ) as session,
88
99
  ):
89
- with file_upload_data(file_upload) as file_bytes:
90
- location = await aiotus.upload(
91
- creation_url,
92
- file_bytes.bytes_data,
93
- {"filename": file_bytes.name.encode()},
94
- client_session=session,
95
- chunksize=_CHUNK_SIZE,
96
- )
97
- if location is None:
98
- raise UploadFailed(f"Failed to upload: {file_bytes.name}")
99
- return UploadedFile(
100
- name=file_bytes.name, file_id=int(location.path.split("/")[-1])
101
- )
102
-
103
- def upload_files(self: Self, *, file_uploads: list[FileUpload]) -> list[UploadedFile]:
100
+ attributes = {}
101
+ match file_upload:
102
+ case MediaFileUpload():
103
+ attributes["file_path"] = file_upload.path
104
+ case DataFileUpload():
105
+ attributes["file_name"] = file_upload.name
106
+ case _:
107
+ assert_never(file_upload)
108
+ with push_scope_optional(
109
+ self._logger, "upload_file", attributes=attributes
110
+ ):
111
+ if self._logger is not None:
112
+ self._logger.log_info("Uploading file", attributes=attributes)
113
+ with file_upload_data(file_upload) as file_bytes:
114
+ if file_bytes.bytes_data.read(1) == b"":
115
+ raise UploadFailed(
116
+ f"Failed to upload empty file: {file_bytes.name}"
117
+ )
118
+ file_bytes.bytes_data.seek(0)
119
+ location = await aiotus.upload(
120
+ creation_url,
121
+ file_bytes.bytes_data,
122
+ {"filename": file_bytes.name.encode()},
123
+ client_session=session,
124
+ config=aiotus.RetryConfiguration(
125
+ ssl=not self._allow_insecure_tls
126
+ ),
127
+ chunksize=_CHUNK_SIZE,
128
+ )
129
+ if location is None:
130
+ raise UploadFailed(f"Failed to upload: {file_bytes.name}")
131
+ return UploadedFile(
132
+ name=file_bytes.name, file_id=int(location.path.split("/")[-1])
133
+ )
134
+
135
+ def upload_files(
136
+ self: Self, *, file_uploads: list[FileUpload]
137
+ ) -> list[UploadedFile]:
104
138
  return [
105
139
  asyncio.run(self._upload_file(file_upload)) for file_upload in file_uploads
106
140
  ]