UncountablePythonSDK 0.0.7__py3-none-any.whl → 0.0.92__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (311) hide show
  1. UncountablePythonSDK-0.0.92.dist-info/METADATA +61 -0
  2. UncountablePythonSDK-0.0.92.dist-info/RECORD +301 -0
  3. {UncountablePythonSDK-0.0.7.dist-info → UncountablePythonSDK-0.0.92.dist-info}/WHEEL +1 -1
  4. {UncountablePythonSDK-0.0.7.dist-info → UncountablePythonSDK-0.0.92.dist-info}/top_level.txt +1 -1
  5. docs/.gitignore +1 -0
  6. docs/conf.py +57 -0
  7. docs/index.md +13 -0
  8. docs/justfile +12 -0
  9. docs/quickstart.md +19 -0
  10. docs/requirements.txt +7 -0
  11. docs/static/favicons/android-chrome-192x192.png +0 -0
  12. docs/static/favicons/android-chrome-512x512.png +0 -0
  13. docs/static/favicons/apple-touch-icon.png +0 -0
  14. docs/static/favicons/browserconfig.xml +9 -0
  15. docs/static/favicons/favicon-16x16.png +0 -0
  16. docs/static/favicons/favicon-32x32.png +0 -0
  17. docs/static/favicons/manifest.json +18 -0
  18. docs/static/favicons/mstile-150x150.png +0 -0
  19. docs/static/favicons/safari-pinned-tab.svg +32 -0
  20. docs/static/logo_blue.png +0 -0
  21. examples/async_batch.py +35 -0
  22. examples/create_entity.py +22 -17
  23. examples/download_files.py +26 -0
  24. examples/edit_recipe_inputs.py +50 -0
  25. examples/integration-server/jobs/materials_auto/example_cron.py +18 -0
  26. examples/integration-server/jobs/materials_auto/example_wh.py +15 -0
  27. examples/integration-server/jobs/materials_auto/profile.yaml +43 -0
  28. examples/integration-server/pyproject.toml +224 -0
  29. examples/invoke_uploader.py +26 -0
  30. examples/set_recipe_metadata_file.py +40 -0
  31. examples/set_recipe_output_file_sdk.py +26 -0
  32. examples/upload_files.py +18 -0
  33. pkgs/argument_parser/__init__.py +5 -0
  34. pkgs/argument_parser/_is_enum.py +1 -6
  35. pkgs/argument_parser/argument_parser.py +232 -76
  36. pkgs/argument_parser/case_convert.py +4 -3
  37. pkgs/filesystem_utils/__init__.py +20 -0
  38. pkgs/filesystem_utils/_blob_session.py +137 -0
  39. pkgs/filesystem_utils/_gdrive_session.py +309 -0
  40. pkgs/filesystem_utils/_local_session.py +69 -0
  41. pkgs/filesystem_utils/_s3_session.py +117 -0
  42. pkgs/filesystem_utils/_sftp_session.py +147 -0
  43. pkgs/filesystem_utils/file_type_utils.py +91 -0
  44. pkgs/filesystem_utils/filesystem_session.py +39 -0
  45. pkgs/py.typed +0 -0
  46. pkgs/serialization/__init__.py +8 -1
  47. pkgs/serialization/annotation.py +64 -0
  48. pkgs/serialization/opaque_key.py +1 -1
  49. pkgs/serialization/serial_alias.py +47 -0
  50. pkgs/serialization/serial_class.py +65 -50
  51. pkgs/serialization/serial_generic.py +16 -0
  52. pkgs/serialization/serial_union.py +84 -0
  53. pkgs/serialization/yaml.py +57 -0
  54. pkgs/serialization_util/__init__.py +7 -7
  55. pkgs/serialization_util/_get_type_for_serialization.py +1 -3
  56. pkgs/serialization_util/convert_to_snakecase.py +27 -0
  57. pkgs/serialization_util/dataclasses.py +14 -0
  58. pkgs/serialization_util/serialization_helpers.py +118 -73
  59. pkgs/strenum_compat/strenum_compat.py +1 -9
  60. pkgs/type_spec/actions_registry/__init__.py +0 -0
  61. pkgs/type_spec/actions_registry/__main__.py +126 -0
  62. pkgs/type_spec/actions_registry/emit_typescript.py +182 -0
  63. pkgs/type_spec/builder.py +475 -89
  64. pkgs/type_spec/config.py +24 -19
  65. pkgs/type_spec/emit_io_ts.py +5 -2
  66. pkgs/type_spec/emit_open_api.py +266 -32
  67. pkgs/type_spec/emit_open_api_util.py +32 -13
  68. pkgs/type_spec/emit_python.py +601 -150
  69. pkgs/type_spec/emit_typescript.py +74 -273
  70. pkgs/type_spec/emit_typescript_util.py +239 -5
  71. pkgs/type_spec/load_types.py +55 -10
  72. pkgs/type_spec/open_api_util.py +30 -41
  73. pkgs/type_spec/parts/base.py.prepart +4 -3
  74. pkgs/type_spec/type_info/emit_type_info.py +178 -16
  75. pkgs/type_spec/util.py +11 -11
  76. pkgs/type_spec/value_spec/__main__.py +3 -3
  77. pkgs/type_spec/value_spec/convert_type.py +8 -1
  78. pkgs/type_spec/value_spec/emit_python.py +13 -4
  79. uncountable/__init__.py +1 -2
  80. uncountable/core/__init__.py +12 -2
  81. uncountable/core/async_batch.py +37 -0
  82. uncountable/core/client.py +293 -43
  83. uncountable/core/environment.py +41 -0
  84. uncountable/core/file_upload.py +135 -0
  85. uncountable/core/types.py +17 -0
  86. uncountable/integration/__init__.py +0 -0
  87. uncountable/integration/cli.py +49 -0
  88. uncountable/integration/construct_client.py +51 -0
  89. uncountable/integration/cron.py +29 -0
  90. uncountable/integration/db/__init__.py +0 -0
  91. uncountable/integration/db/connect.py +18 -0
  92. uncountable/integration/db/session.py +25 -0
  93. uncountable/integration/entrypoint.py +13 -0
  94. uncountable/integration/executors/__init__.py +0 -0
  95. uncountable/integration/executors/executors.py +148 -0
  96. uncountable/integration/executors/generic_upload_executor.py +284 -0
  97. uncountable/integration/executors/script_executor.py +25 -0
  98. uncountable/integration/job.py +87 -0
  99. uncountable/integration/queue_runner/__init__.py +0 -0
  100. uncountable/integration/queue_runner/command_server/__init__.py +24 -0
  101. uncountable/integration/queue_runner/command_server/command_client.py +68 -0
  102. uncountable/integration/queue_runner/command_server/command_server.py +64 -0
  103. uncountable/integration/queue_runner/command_server/protocol/__init__.py +0 -0
  104. uncountable/integration/queue_runner/command_server/protocol/command_server.proto +22 -0
  105. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +40 -0
  106. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +38 -0
  107. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +129 -0
  108. uncountable/integration/queue_runner/command_server/types.py +52 -0
  109. uncountable/integration/queue_runner/datastore/__init__.py +3 -0
  110. uncountable/integration/queue_runner/datastore/datastore_sqlite.py +93 -0
  111. uncountable/integration/queue_runner/datastore/interface.py +19 -0
  112. uncountable/integration/queue_runner/datastore/model.py +17 -0
  113. uncountable/integration/queue_runner/job_scheduler.py +163 -0
  114. uncountable/integration/queue_runner/queue_runner.py +26 -0
  115. uncountable/integration/queue_runner/types.py +7 -0
  116. uncountable/integration/queue_runner/worker.py +119 -0
  117. uncountable/integration/scan_profiles.py +67 -0
  118. uncountable/integration/scheduler.py +150 -0
  119. uncountable/integration/secret_retrieval/__init__.py +3 -0
  120. uncountable/integration/secret_retrieval/retrieve_secret.py +93 -0
  121. uncountable/integration/server.py +117 -0
  122. uncountable/integration/telemetry.py +209 -0
  123. uncountable/integration/webhook_server/entrypoint.py +170 -0
  124. uncountable/types/__init__.py +151 -5
  125. uncountable/types/api/batch/execute_batch.py +15 -7
  126. uncountable/types/api/batch/execute_batch_load_async.py +42 -0
  127. uncountable/types/api/chemical/__init__.py +1 -0
  128. uncountable/types/api/chemical/convert_chemical_formats.py +63 -0
  129. uncountable/types/api/entity/create_entities.py +23 -10
  130. uncountable/types/api/entity/create_entity.py +21 -12
  131. uncountable/types/api/entity/get_entities_data.py +19 -29
  132. uncountable/types/api/entity/grant_entity_permissions.py +48 -0
  133. uncountable/types/api/entity/list_entities.py +28 -20
  134. uncountable/types/api/entity/lock_entity.py +45 -0
  135. uncountable/types/api/entity/resolve_entity_ids.py +19 -7
  136. uncountable/types/api/entity/set_entity_field_values.py +44 -0
  137. uncountable/types/api/entity/set_values.py +13 -28
  138. uncountable/types/api/entity/transition_entity_phase.py +80 -0
  139. uncountable/types/api/entity/unlock_entity.py +44 -0
  140. uncountable/types/api/equipment/__init__.py +1 -0
  141. uncountable/types/api/equipment/associate_equipment_input.py +44 -0
  142. uncountable/types/api/field_options/__init__.py +1 -0
  143. uncountable/types/api/field_options/upsert_field_options.py +55 -0
  144. uncountable/types/api/files/__init__.py +1 -0
  145. uncountable/types/api/files/download_file.py +77 -0
  146. uncountable/types/api/id_source/__init__.py +1 -0
  147. uncountable/types/api/id_source/list_id_source.py +56 -0
  148. uncountable/types/api/id_source/match_id_source.py +54 -0
  149. uncountable/types/api/input_groups/get_input_group_names.py +18 -7
  150. uncountable/types/api/inputs/create_inputs.py +25 -24
  151. uncountable/types/api/inputs/get_input_data.py +37 -31
  152. uncountable/types/api/inputs/get_input_names.py +20 -9
  153. uncountable/types/api/inputs/get_inputs_data.py +33 -27
  154. uncountable/types/api/inputs/set_input_attribute_values.py +18 -13
  155. uncountable/types/api/inputs/set_input_category.py +44 -0
  156. uncountable/types/api/inputs/set_input_subcategories.py +45 -0
  157. uncountable/types/api/inputs/set_intermediate_type.py +50 -0
  158. uncountable/types/api/material_families/__init__.py +1 -0
  159. uncountable/types/api/material_families/update_entity_material_families.py +48 -0
  160. uncountable/types/api/outputs/get_output_data.py +38 -29
  161. uncountable/types/api/outputs/get_output_names.py +20 -9
  162. uncountable/types/api/outputs/resolve_output_conditions.py +23 -10
  163. uncountable/types/api/permissions/__init__.py +1 -0
  164. uncountable/types/api/permissions/set_core_permissions.py +105 -0
  165. uncountable/types/api/project/get_projects.py +23 -19
  166. uncountable/types/api/project/get_projects_data.py +26 -43
  167. uncountable/types/api/recipe_links/__init__.py +1 -0
  168. uncountable/types/api/recipe_links/create_recipe_link.py +46 -0
  169. uncountable/types/api/recipe_links/remove_recipe_link.py +45 -0
  170. uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +21 -10
  171. uncountable/types/api/recipes/add_recipe_to_project.py +42 -0
  172. uncountable/types/api/recipes/archive_recipes.py +42 -0
  173. uncountable/types/api/recipes/associate_recipe_as_input.py +44 -0
  174. uncountable/types/api/recipes/associate_recipe_as_lot.py +43 -0
  175. uncountable/types/api/recipes/clear_recipe_outputs.py +42 -0
  176. uncountable/types/api/recipes/create_recipe.py +51 -0
  177. uncountable/types/api/recipes/create_recipes.py +25 -24
  178. uncountable/types/api/recipes/disassociate_recipe_as_input.py +42 -0
  179. uncountable/types/api/recipes/edit_recipe_inputs.py +283 -0
  180. uncountable/types/api/recipes/get_column_calculation_values.py +58 -0
  181. uncountable/types/api/recipes/get_curve.py +13 -27
  182. uncountable/types/api/recipes/get_recipe_calculations.py +21 -21
  183. uncountable/types/api/recipes/get_recipe_links.py +14 -6
  184. uncountable/types/api/recipes/get_recipe_names.py +18 -7
  185. uncountable/types/api/recipes/get_recipe_output_metadata.py +18 -19
  186. uncountable/types/api/recipes/get_recipes_data.py +83 -144
  187. uncountable/types/api/recipes/lock_recipes.py +63 -0
  188. uncountable/types/api/recipes/remove_recipe_from_project.py +42 -0
  189. uncountable/types/api/recipes/set_recipe_inputs.py +21 -11
  190. uncountable/types/api/recipes/set_recipe_metadata.py +43 -0
  191. uncountable/types/api/recipes/set_recipe_output_annotations.py +115 -0
  192. uncountable/types/api/recipes/set_recipe_output_file.py +56 -0
  193. uncountable/types/api/recipes/set_recipe_outputs.py +28 -15
  194. uncountable/types/api/recipes/set_recipe_tags.py +109 -0
  195. uncountable/types/api/recipes/unarchive_recipes.py +41 -0
  196. uncountable/types/api/recipes/unlock_recipes.py +50 -0
  197. uncountable/types/api/triggers/__init__.py +1 -0
  198. uncountable/types/api/triggers/run_trigger.py +43 -0
  199. uncountable/types/api/uploader/__init__.py +1 -0
  200. uncountable/types/api/uploader/invoke_uploader.py +47 -0
  201. uncountable/types/async_batch.py +13 -0
  202. uncountable/types/async_batch_processor.py +384 -0
  203. uncountable/types/async_batch_t.py +97 -0
  204. uncountable/types/async_jobs.py +9 -0
  205. uncountable/types/async_jobs_t.py +53 -0
  206. uncountable/types/auth_retrieval.py +12 -0
  207. uncountable/types/auth_retrieval_t.py +75 -0
  208. uncountable/types/base.py +5 -78
  209. uncountable/types/base_t.py +85 -0
  210. uncountable/types/calculations.py +8 -0
  211. uncountable/types/calculations_t.py +27 -0
  212. uncountable/types/chemical_structure.py +8 -0
  213. uncountable/types/chemical_structure_t.py +28 -0
  214. uncountable/types/client_base.py +1115 -76
  215. uncountable/types/client_config.py +8 -0
  216. uncountable/types/client_config_t.py +26 -0
  217. uncountable/types/curves.py +10 -0
  218. uncountable/types/curves_t.py +51 -0
  219. uncountable/types/entity.py +8 -266
  220. uncountable/types/entity_t.py +393 -0
  221. uncountable/types/experiment_groups.py +8 -0
  222. uncountable/types/experiment_groups_t.py +27 -0
  223. uncountable/types/field_values.py +17 -23
  224. uncountable/types/field_values_t.py +204 -0
  225. uncountable/types/fields.py +8 -0
  226. uncountable/types/fields_t.py +28 -0
  227. uncountable/types/generic_upload.py +15 -0
  228. uncountable/types/generic_upload_t.py +119 -0
  229. uncountable/types/id_source.py +12 -0
  230. uncountable/types/id_source_t.py +68 -0
  231. uncountable/types/identifier.py +11 -0
  232. uncountable/types/identifier_t.py +63 -0
  233. uncountable/types/input_attributes.py +8 -0
  234. uncountable/types/input_attributes_t.py +30 -0
  235. uncountable/types/inputs.py +11 -0
  236. uncountable/types/inputs_t.py +83 -0
  237. uncountable/types/integration_server.py +9 -0
  238. uncountable/types/integration_server_t.py +42 -0
  239. uncountable/types/job_definition.py +27 -0
  240. uncountable/types/job_definition_t.py +260 -0
  241. uncountable/types/outputs.py +8 -0
  242. uncountable/types/outputs_t.py +30 -0
  243. uncountable/types/overrides.py +10 -0
  244. uncountable/types/overrides_t.py +49 -0
  245. uncountable/types/permissions.py +8 -0
  246. uncountable/types/permissions_t.py +46 -0
  247. uncountable/types/phases.py +8 -0
  248. uncountable/types/phases_t.py +27 -0
  249. uncountable/types/post_base.py +8 -0
  250. uncountable/types/post_base_t.py +30 -0
  251. uncountable/types/queued_job.py +16 -0
  252. uncountable/types/queued_job_t.py +123 -0
  253. uncountable/types/recipe_identifiers.py +12 -0
  254. uncountable/types/recipe_identifiers_t.py +76 -0
  255. uncountable/types/recipe_inputs.py +9 -0
  256. uncountable/types/recipe_inputs_t.py +30 -0
  257. uncountable/types/recipe_links.py +4 -44
  258. uncountable/types/recipe_links_t.py +54 -0
  259. uncountable/types/recipe_metadata.py +10 -0
  260. uncountable/types/recipe_metadata_t.py +58 -0
  261. uncountable/types/recipe_output_metadata.py +8 -0
  262. uncountable/types/recipe_output_metadata_t.py +28 -0
  263. uncountable/types/recipe_tags.py +8 -0
  264. uncountable/types/recipe_tags_t.py +27 -0
  265. uncountable/types/recipe_workflow_steps.py +14 -0
  266. uncountable/types/recipe_workflow_steps_t.py +95 -0
  267. uncountable/types/recipes.py +8 -0
  268. uncountable/types/recipes_t.py +25 -0
  269. uncountable/types/response.py +8 -0
  270. uncountable/types/response_t.py +26 -0
  271. uncountable/types/secret_retrieval.py +12 -0
  272. uncountable/types/secret_retrieval_t.py +75 -0
  273. uncountable/types/units.py +8 -0
  274. uncountable/types/units_t.py +27 -0
  275. uncountable/types/users.py +8 -0
  276. uncountable/types/users_t.py +28 -0
  277. uncountable/types/webhook_job.py +9 -0
  278. uncountable/types/webhook_job_t.py +37 -0
  279. uncountable/types/workflows.py +9 -0
  280. uncountable/types/workflows_t.py +39 -0
  281. UncountablePythonSDK-0.0.7.dist-info/METADATA +0 -27
  282. UncountablePythonSDK-0.0.7.dist-info/RECORD +0 -119
  283. examples/recipe-import/importer.py +0 -39
  284. type_spec/external/api/batch/execute_batch.yaml +0 -56
  285. type_spec/external/api/entity/create_entities.yaml +0 -33
  286. type_spec/external/api/entity/create_entity.yaml +0 -39
  287. type_spec/external/api/entity/get_entities_data.yaml +0 -55
  288. type_spec/external/api/entity/list_entities.yaml +0 -62
  289. type_spec/external/api/entity/resolve_entity_ids.yaml +0 -29
  290. type_spec/external/api/entity/set_values.yaml +0 -45
  291. type_spec/external/api/input_groups/get_input_group_names.yaml +0 -29
  292. type_spec/external/api/inputs/create_inputs.yaml +0 -61
  293. type_spec/external/api/inputs/get_input_data.yaml +0 -108
  294. type_spec/external/api/inputs/get_input_names.yaml +0 -38
  295. type_spec/external/api/inputs/get_inputs_data.yaml +0 -95
  296. type_spec/external/api/inputs/set_input_attribute_values.yaml +0 -37
  297. type_spec/external/api/outputs/get_output_data.yaml +0 -103
  298. type_spec/external/api/outputs/get_output_names.yaml +0 -35
  299. type_spec/external/api/outputs/resolve_output_conditions.yaml +0 -50
  300. type_spec/external/api/project/get_projects.yaml +0 -52
  301. type_spec/external/api/project/get_projects_data.yaml +0 -86
  302. type_spec/external/api/recipe_metadata/get_recipe_metadata_data.yaml +0 -41
  303. type_spec/external/api/recipes/create_recipes.yaml +0 -60
  304. type_spec/external/api/recipes/get_curve.yaml +0 -50
  305. type_spec/external/api/recipes/get_recipe_calculations.yaml +0 -49
  306. type_spec/external/api/recipes/get_recipe_links.yaml +0 -26
  307. type_spec/external/api/recipes/get_recipe_names.yaml +0 -29
  308. type_spec/external/api/recipes/get_recipe_output_metadata.yaml +0 -49
  309. type_spec/external/api/recipes/get_recipes_data.yaml +0 -372
  310. type_spec/external/api/recipes/set_recipe_inputs.yaml +0 -36
  311. type_spec/external/api/recipes/set_recipe_outputs.yaml +0 -56
@@ -0,0 +1,24 @@
1
+ from .command_client import check_health, send_job_queue_message
2
+ from .command_server import serve
3
+ from .types import (
4
+ CommandEnqueueJob,
5
+ CommandEnqueueJobResponse,
6
+ CommandQueue,
7
+ CommandServerBadResponse,
8
+ CommandServerException,
9
+ CommandServerTimeout,
10
+ CommandTask,
11
+ )
12
+
13
+ __all__: list[str] = [
14
+ "serve",
15
+ "check_health",
16
+ "send_job_queue_message",
17
+ "CommandEnqueueJob",
18
+ "CommandEnqueueJobResponse",
19
+ "CommandTask",
20
+ "CommandQueue",
21
+ "CommandServerTimeout",
22
+ "CommandServerException",
23
+ "CommandServerBadResponse",
24
+ ]
@@ -0,0 +1,68 @@
1
+ from contextlib import contextmanager
2
+ from typing import Generator
3
+
4
+ import grpc
5
+ import simplejson as json
6
+
7
+ from pkgs.serialization_util import serialize_for_api
8
+ from uncountable.integration.queue_runner.command_server.protocol.command_server_pb2 import (
9
+ CheckHealthRequest,
10
+ CheckHealthResult,
11
+ EnqueueJobRequest,
12
+ EnqueueJobResult,
13
+ )
14
+ from uncountable.integration.queue_runner.command_server.types import (
15
+ CommandServerBadResponse,
16
+ CommandServerTimeout,
17
+ )
18
+ from uncountable.types import queued_job_t
19
+
20
+ from .protocol.command_server_pb2_grpc import CommandServerStub
21
+
22
+ _LOCAL_RPC_HOST = "localhost"
23
+ _DEFAULT_MESSAGE_TIMEOUT_SECS = 2
24
+
25
+
26
+ @contextmanager
27
+ def command_server_connection(
28
+ host: str, port: int
29
+ ) -> Generator[CommandServerStub, None, None]:
30
+ try:
31
+ with grpc.insecure_channel(f"{host}:{port}") as channel:
32
+ stub = CommandServerStub(channel)
33
+ yield stub
34
+ except grpc._channel._InactiveRpcError as e:
35
+ raise CommandServerTimeout() from e
36
+
37
+
38
+ def send_job_queue_message(
39
+ *,
40
+ job_ref_name: str,
41
+ payload: queued_job_t.QueuedJobPayload,
42
+ host: str = "localhost",
43
+ port: int,
44
+ ) -> str:
45
+ with command_server_connection(host=host, port=port) as stub:
46
+ request = EnqueueJobRequest(
47
+ job_ref_name=job_ref_name,
48
+ serialized_payload=json.dumps(serialize_for_api(payload)),
49
+ )
50
+
51
+ response = stub.EnqueueJob(request, timeout=_DEFAULT_MESSAGE_TIMEOUT_SECS)
52
+
53
+ assert isinstance(response, EnqueueJobResult)
54
+ if not response.successfully_queued:
55
+ raise CommandServerBadResponse("queue operation was not successful")
56
+
57
+ return response.queued_job_uuid
58
+
59
+
60
+ def check_health(*, host: str = _LOCAL_RPC_HOST, port: int) -> bool:
61
+ with command_server_connection(host=host, port=port) as stub:
62
+ request = CheckHealthRequest()
63
+
64
+ response = stub.CheckHealth(request, timeout=_DEFAULT_MESSAGE_TIMEOUT_SECS)
65
+
66
+ assert isinstance(response, CheckHealthResult)
67
+
68
+ return response.success
@@ -0,0 +1,64 @@
1
+ import asyncio
2
+
3
+ import simplejson as json
4
+ from grpc import aio
5
+
6
+ from pkgs.argument_parser import CachedParser
7
+ from uncountable.core.environment import get_local_admin_server_port
8
+ from uncountable.integration.queue_runner.command_server.protocol.command_server_pb2 import (
9
+ CheckHealthRequest,
10
+ CheckHealthResult,
11
+ EnqueueJobRequest,
12
+ EnqueueJobResult,
13
+ )
14
+ from uncountable.integration.queue_runner.command_server.types import (
15
+ CommandEnqueueJob,
16
+ CommandEnqueueJobResponse,
17
+ CommandQueue,
18
+ )
19
+ from uncountable.types import queued_job_t
20
+
21
+ from .protocol.command_server_pb2_grpc import (
22
+ CommandServerServicer,
23
+ add_CommandServerServicer_to_server,
24
+ )
25
+
26
+ queued_job_payload_parser = CachedParser(queued_job_t.QueuedJobPayload)
27
+
28
+
29
+ async def serve(command_queue: CommandQueue) -> None:
30
+ server = aio.server()
31
+
32
+ class CommandServerHandler(CommandServerServicer):
33
+ async def EnqueueJob(
34
+ self, request: EnqueueJobRequest, context: aio.ServicerContext
35
+ ) -> EnqueueJobResult:
36
+ payload_json = json.loads(request.serialized_payload)
37
+ payload = queued_job_payload_parser.parse_api(payload_json)
38
+ response_queue: asyncio.Queue[CommandEnqueueJobResponse] = asyncio.Queue()
39
+ await command_queue.put(
40
+ CommandEnqueueJob(
41
+ job_ref_name=request.job_ref_name,
42
+ payload=payload,
43
+ response_queue=response_queue,
44
+ )
45
+ )
46
+ response = await response_queue.get()
47
+ result = EnqueueJobResult(
48
+ successfully_queued=True, queued_job_uuid=response.queued_job_uuid
49
+ )
50
+ return result
51
+
52
+ async def CheckHealth(
53
+ self, request: CheckHealthRequest, context: aio.ServicerContext
54
+ ) -> CheckHealthResult:
55
+ return CheckHealthResult(success=True)
56
+
57
+ add_CommandServerServicer_to_server(CommandServerHandler(), server)
58
+
59
+ listen_addr = f"[::]:{get_local_admin_server_port()}"
60
+
61
+ server.add_insecure_port(listen_addr)
62
+
63
+ await server.start()
64
+ await server.wait_for_termination()
@@ -0,0 +1,22 @@
1
+ syntax = "proto3";
2
+
3
+ service CommandServer {
4
+ rpc EnqueueJob(EnqueueJobRequest) returns (EnqueueJobResult) {}
5
+ rpc CheckHealth(CheckHealthRequest) returns (CheckHealthResult) {}
6
+ }
7
+
8
+ message EnqueueJobRequest {
9
+ string job_ref_name = 1;
10
+ string serialized_payload = 2;
11
+ }
12
+
13
+ message EnqueueJobResult {
14
+ bool successfully_queued = 1;
15
+ string queued_job_uuid = 2;
16
+ }
17
+
18
+ message CheckHealthRequest {}
19
+
20
+ message CheckHealthResult {
21
+ bool success = 1;
22
+ }
@@ -0,0 +1,40 @@
1
+ # ruff: noqa
2
+ # -*- coding: utf-8 -*-
3
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
4
+ # source: uncountable/integration/queue_runner/command_server/protocol/command_server.proto
5
+ # Protobuf Python Version: 4.25.1
6
+ """Generated protocol buffer code."""
7
+
8
+ from google.protobuf import descriptor as _descriptor
9
+ from google.protobuf import descriptor_pool as _descriptor_pool
10
+ from google.protobuf import symbol_database as _symbol_database
11
+ from google.protobuf.internal import builder as _builder
12
+ # @@protoc_insertion_point(imports)
13
+
14
+ _sym_db = _symbol_database.Default()
15
+
16
+
17
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(
18
+ b'\nQuncountable/integration/queue_runner/command_server/protocol/command_server.proto"E\n\x11\x45nqueueJobRequest\x12\x14\n\x0cjob_ref_name\x18\x01 \x01(\t\x12\x1a\n\x12serialized_payload\x18\x02 \x01(\t"H\n\x10\x45nqueueJobResult\x12\x1b\n\x13successfully_queued\x18\x01 \x01(\x08\x12\x17\n\x0fqueued_job_uuid\x18\x02 \x01(\t"\x14\n\x12\x43heckHealthRequest"$\n\x11\x43heckHealthResult\x12\x0f\n\x07success\x18\x01 \x01(\x08\x32\x80\x01\n\rCommandServer\x12\x35\n\nEnqueueJob\x12\x12.EnqueueJobRequest\x1a\x11.EnqueueJobResult"\x00\x12\x38\n\x0b\x43heckHealth\x12\x13.CheckHealthRequest\x1a\x12.CheckHealthResult"\x00\x62\x06proto3'
19
+ )
20
+
21
+ _globals = globals()
22
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
23
+ _builder.BuildTopDescriptorsAndMessages(
24
+ DESCRIPTOR,
25
+ "uncountable.integration.queue_runner.command_server.protocol.command_server_pb2",
26
+ _globals,
27
+ )
28
+ if _descriptor._USE_C_DESCRIPTORS == False:
29
+ DESCRIPTOR._options = None
30
+ _globals["_ENQUEUEJOBREQUEST"]._serialized_start = 85
31
+ _globals["_ENQUEUEJOBREQUEST"]._serialized_end = 154
32
+ _globals["_ENQUEUEJOBRESULT"]._serialized_start = 156
33
+ _globals["_ENQUEUEJOBRESULT"]._serialized_end = 228
34
+ _globals["_CHECKHEALTHREQUEST"]._serialized_start = 230
35
+ _globals["_CHECKHEALTHREQUEST"]._serialized_end = 250
36
+ _globals["_CHECKHEALTHRESULT"]._serialized_start = 252
37
+ _globals["_CHECKHEALTHRESULT"]._serialized_end = 288
38
+ _globals["_COMMANDSERVER"]._serialized_start = 291
39
+ _globals["_COMMANDSERVER"]._serialized_end = 419
40
+ # @@protoc_insertion_point(module_scope)
@@ -0,0 +1,38 @@
1
+ # ruff: noqa
2
+ from google.protobuf import descriptor as _descriptor
3
+ from google.protobuf import message as _message
4
+ from typing import ClassVar as _ClassVar, Optional as _Optional
5
+
6
+ DESCRIPTOR: _descriptor.FileDescriptor
7
+
8
+ class EnqueueJobRequest(_message.Message):
9
+ __slots__ = ("job_ref_name", "serialized_payload")
10
+ JOB_REF_NAME_FIELD_NUMBER: _ClassVar[int]
11
+ SERIALIZED_PAYLOAD_FIELD_NUMBER: _ClassVar[int]
12
+ job_ref_name: str
13
+ serialized_payload: str
14
+ def __init__(
15
+ self,
16
+ job_ref_name: _Optional[str] = ...,
17
+ serialized_payload: _Optional[str] = ...,
18
+ ) -> None: ...
19
+
20
+ class EnqueueJobResult(_message.Message):
21
+ __slots__ = ("successfully_queued", "queued_job_uuid")
22
+ SUCCESSFULLY_QUEUED_FIELD_NUMBER: _ClassVar[int]
23
+ QUEUED_JOB_UUID_FIELD_NUMBER: _ClassVar[int]
24
+ successfully_queued: bool
25
+ queued_job_uuid: str
26
+ def __init__(
27
+ self, successfully_queued: bool = ..., queued_job_uuid: _Optional[str] = ...
28
+ ) -> None: ...
29
+
30
+ class CheckHealthRequest(_message.Message):
31
+ __slots__ = ()
32
+ def __init__(self) -> None: ...
33
+
34
+ class CheckHealthResult(_message.Message):
35
+ __slots__ = ("success",)
36
+ SUCCESS_FIELD_NUMBER: _ClassVar[int]
37
+ success: bool
38
+ def __init__(self, success: bool = ...) -> None: ...
@@ -0,0 +1,129 @@
1
+ # mypy: disable-error-code="no-untyped-def"
2
+ # ruff: noqa
3
+ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
4
+ """Client and server classes corresponding to protobuf-defined services."""
5
+
6
+ import grpc
7
+
8
+ from uncountable.integration.queue_runner.command_server.protocol import (
9
+ command_server_pb2 as uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2,
10
+ )
11
+
12
+
13
+ class CommandServerStub(object):
14
+ """Missing associated documentation comment in .proto file."""
15
+
16
+ def __init__(self, channel):
17
+ """Constructor.
18
+
19
+ Args:
20
+ channel: A grpc.Channel.
21
+ """
22
+ self.EnqueueJob = channel.unary_unary(
23
+ "/CommandServer/EnqueueJob",
24
+ request_serializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.EnqueueJobRequest.SerializeToString,
25
+ response_deserializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.EnqueueJobResult.FromString,
26
+ )
27
+ self.CheckHealth = channel.unary_unary(
28
+ "/CommandServer/CheckHealth",
29
+ request_serializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.CheckHealthRequest.SerializeToString,
30
+ response_deserializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.CheckHealthResult.FromString,
31
+ )
32
+
33
+
34
+ class CommandServerServicer(object):
35
+ """Missing associated documentation comment in .proto file."""
36
+
37
+ def EnqueueJob(self, request, context):
38
+ """Missing associated documentation comment in .proto file."""
39
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
40
+ context.set_details("Method not implemented!")
41
+ raise NotImplementedError("Method not implemented!")
42
+
43
+ def CheckHealth(self, request, context):
44
+ """Missing associated documentation comment in .proto file."""
45
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
46
+ context.set_details("Method not implemented!")
47
+ raise NotImplementedError("Method not implemented!")
48
+
49
+
50
+ def add_CommandServerServicer_to_server(servicer, server):
51
+ rpc_method_handlers = {
52
+ "EnqueueJob": grpc.unary_unary_rpc_method_handler(
53
+ servicer.EnqueueJob,
54
+ request_deserializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.EnqueueJobRequest.FromString,
55
+ response_serializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.EnqueueJobResult.SerializeToString,
56
+ ),
57
+ "CheckHealth": grpc.unary_unary_rpc_method_handler(
58
+ servicer.CheckHealth,
59
+ request_deserializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.CheckHealthRequest.FromString,
60
+ response_serializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.CheckHealthResult.SerializeToString,
61
+ ),
62
+ }
63
+ generic_handler = grpc.method_handlers_generic_handler(
64
+ "CommandServer", rpc_method_handlers
65
+ )
66
+ server.add_generic_rpc_handlers((generic_handler,))
67
+
68
+
69
+ # This class is part of an EXPERIMENTAL API.
70
+ class CommandServer(object):
71
+ """Missing associated documentation comment in .proto file."""
72
+
73
+ @staticmethod
74
+ def EnqueueJob(
75
+ request,
76
+ target,
77
+ options=(),
78
+ channel_credentials=None,
79
+ call_credentials=None,
80
+ insecure=False,
81
+ compression=None,
82
+ wait_for_ready=None,
83
+ timeout=None,
84
+ metadata=None,
85
+ ):
86
+ return grpc.experimental.unary_unary(
87
+ request,
88
+ target,
89
+ "/CommandServer/EnqueueJob",
90
+ uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.EnqueueJobRequest.SerializeToString,
91
+ uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.EnqueueJobResult.FromString,
92
+ options,
93
+ channel_credentials,
94
+ insecure,
95
+ call_credentials,
96
+ compression,
97
+ wait_for_ready,
98
+ timeout,
99
+ metadata,
100
+ )
101
+
102
+ @staticmethod
103
+ def CheckHealth(
104
+ request,
105
+ target,
106
+ options=(),
107
+ channel_credentials=None,
108
+ call_credentials=None,
109
+ insecure=False,
110
+ compression=None,
111
+ wait_for_ready=None,
112
+ timeout=None,
113
+ metadata=None,
114
+ ):
115
+ return grpc.experimental.unary_unary(
116
+ request,
117
+ target,
118
+ "/CommandServer/CheckHealth",
119
+ uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.CheckHealthRequest.SerializeToString,
120
+ uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.CheckHealthResult.FromString,
121
+ options,
122
+ channel_credentials,
123
+ insecure,
124
+ call_credentials,
125
+ compression,
126
+ wait_for_ready,
127
+ timeout,
128
+ metadata,
129
+ )
@@ -0,0 +1,52 @@
1
+ import asyncio
2
+ import typing
3
+ from dataclasses import dataclass
4
+ from enum import StrEnum
5
+
6
+ from uncountable.types import queued_job_t
7
+
8
+
9
+ class CommandType(StrEnum):
10
+ ENQUEUE_JOB = "enqueue_job"
11
+
12
+
13
+ RT = typing.TypeVar("RT")
14
+
15
+
16
+ @dataclass(kw_only=True)
17
+ class CommandBase(typing.Generic[RT]):
18
+ type: CommandType
19
+ response_queue: asyncio.Queue[RT]
20
+
21
+
22
+ @dataclass(kw_only=True)
23
+ class CommandEnqueueJobResponse:
24
+ queued_job_uuid: str
25
+
26
+
27
+ @dataclass(kw_only=True)
28
+ class CommandEnqueueJob(CommandBase[CommandEnqueueJobResponse]):
29
+ type: CommandType = CommandType.ENQUEUE_JOB
30
+ job_ref_name: str
31
+ payload: queued_job_t.QueuedJobPayload
32
+ response_queue: asyncio.Queue[CommandEnqueueJobResponse]
33
+
34
+
35
+ _Command = CommandEnqueueJob
36
+
37
+
38
+ CommandQueue = asyncio.Queue[_Command]
39
+
40
+ CommandTask = asyncio.Task[_Command]
41
+
42
+
43
+ class CommandServerException(Exception):
44
+ pass
45
+
46
+
47
+ class CommandServerTimeout(CommandServerException):
48
+ pass
49
+
50
+
51
+ class CommandServerBadResponse(CommandServerException):
52
+ pass
@@ -0,0 +1,3 @@
1
+ from .datastore_sqlite import DatastoreSqlite
2
+
3
+ __all__: list[str] = ["DatastoreSqlite"]
@@ -0,0 +1,93 @@
1
+ import uuid
2
+ from datetime import datetime, timezone
3
+
4
+ from sqlalchemy import delete, insert, select, update
5
+ from sqlalchemy.engine import Engine
6
+
7
+ from pkgs.argument_parser import CachedParser
8
+ from pkgs.serialization_util import serialize_for_storage
9
+ from uncountable.integration.db.session import DBSessionMaker
10
+ from uncountable.integration.queue_runner.datastore.interface import Datastore
11
+ from uncountable.integration.queue_runner.datastore.model import Base, QueuedJob
12
+ from uncountable.types import queued_job_t
13
+
14
+ queued_job_payload_parser = CachedParser(queued_job_t.QueuedJobPayload)
15
+
16
+
17
+ class DatastoreSqlite(Datastore):
18
+ def __init__(self, session_maker: DBSessionMaker) -> None:
19
+ self.session_maker = session_maker
20
+ super().__init__()
21
+
22
+ @classmethod
23
+ def setup(cls, engine: Engine) -> None:
24
+ Base.metadata.create_all(engine)
25
+
26
+ def add_job_to_queue(
27
+ self, job_payload: queued_job_t.QueuedJobPayload, job_ref_name: str
28
+ ) -> queued_job_t.QueuedJob:
29
+ with self.session_maker() as session:
30
+ serialized_payload = serialize_for_storage(job_payload)
31
+ queued_job_uuid = str(uuid.uuid4())
32
+ num_attempts = 0
33
+ submitted_at = datetime.now(timezone.utc)
34
+ insert_stmt = insert(QueuedJob).values({
35
+ QueuedJob.id.key: queued_job_uuid,
36
+ QueuedJob.job_ref_name.key: job_ref_name,
37
+ QueuedJob.payload.key: serialized_payload,
38
+ QueuedJob.num_attempts: num_attempts,
39
+ QueuedJob.submitted_at: submitted_at,
40
+ })
41
+ session.execute(insert_stmt)
42
+ return queued_job_t.QueuedJob(
43
+ queued_job_uuid=queued_job_uuid,
44
+ job_ref_name=job_ref_name,
45
+ payload=job_payload,
46
+ submitted_at=submitted_at,
47
+ num_attempts=num_attempts,
48
+ )
49
+
50
+ def increment_num_attempts(self, queued_job_uuid: str) -> int:
51
+ with self.session_maker() as session:
52
+ update_stmt = (
53
+ update(QueuedJob)
54
+ .values({QueuedJob.num_attempts.key: QueuedJob.num_attempts + 1})
55
+ .filter(QueuedJob.id == queued_job_uuid)
56
+ )
57
+ session.execute(update_stmt)
58
+ session.flush()
59
+ # IMPROVE: python3.12's sqlite does not support the RETURNING clause
60
+ select_stmt = select(QueuedJob.num_attempts).filter(
61
+ QueuedJob.id == queued_job_uuid
62
+ )
63
+ return int(session.execute(select_stmt).one().num_attempts)
64
+
65
+ def remove_job_from_queue(self, queued_job_uuid: str) -> None:
66
+ with self.session_maker() as session:
67
+ delete_stmt = delete(QueuedJob).filter(QueuedJob.id == queued_job_uuid)
68
+ session.execute(delete_stmt)
69
+
70
+ def load_job_queue(self) -> list[queued_job_t.QueuedJob]:
71
+ with self.session_maker() as session:
72
+ select_stmt = select(
73
+ QueuedJob.id,
74
+ QueuedJob.payload,
75
+ QueuedJob.num_attempts,
76
+ QueuedJob.job_ref_name,
77
+ QueuedJob.submitted_at,
78
+ ).order_by(QueuedJob.submitted_at)
79
+
80
+ queued_jobs: list[queued_job_t.QueuedJob] = []
81
+ for row in session.execute(select_stmt):
82
+ parsed_payload = queued_job_payload_parser.parse_storage(row.payload)
83
+ queued_jobs.append(
84
+ queued_job_t.QueuedJob(
85
+ queued_job_uuid=row.id,
86
+ job_ref_name=row.job_ref_name,
87
+ num_attempts=row.num_attempts,
88
+ submitted_at=row.submitted_at,
89
+ payload=parsed_payload,
90
+ )
91
+ )
92
+
93
+ return queued_jobs
@@ -0,0 +1,19 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from uncountable.types import queued_job_t
4
+
5
+
6
+ class Datastore(ABC):
7
+ @abstractmethod
8
+ def add_job_to_queue(
9
+ self, job_payload: queued_job_t.QueuedJobPayload, job_ref_name: str
10
+ ) -> queued_job_t.QueuedJob: ...
11
+
12
+ @abstractmethod
13
+ def remove_job_from_queue(self, queued_job_uuid: str) -> None: ...
14
+
15
+ @abstractmethod
16
+ def increment_num_attempts(self, queued_job_uuid: str) -> int: ...
17
+
18
+ @abstractmethod
19
+ def load_job_queue(self) -> list[queued_job_t.QueuedJob]: ...
@@ -0,0 +1,17 @@
1
+ from sqlalchemy import JSON, BigInteger, Column, DateTime, Text
2
+ from sqlalchemy.orm import declarative_base
3
+ from sqlalchemy.sql import func
4
+
5
+ Base = declarative_base()
6
+
7
+
8
+ class QueuedJob(Base):
9
+ __tablename__ = "queued_jobs"
10
+
11
+ id = Column(Text, primary_key=True)
12
+ job_ref_name = Column(Text, nullable=False, index=True)
13
+ submitted_at = Column(
14
+ DateTime(timezone=True), server_default=func.current_timestamp(), nullable=False
15
+ )
16
+ payload = Column(JSON, nullable=False)
17
+ num_attempts = Column(BigInteger, nullable=False, default=0, server_default="0")