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

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

Potentially problematic release.


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

Files changed (298) hide show
  1. docs/conf.py +54 -7
  2. docs/index.md +107 -4
  3. docs/integration_examples/create_ingredient.md +43 -0
  4. docs/integration_examples/create_output.md +56 -0
  5. docs/integration_examples/index.md +6 -0
  6. docs/justfile +2 -2
  7. docs/requirements.txt +6 -4
  8. examples/basic_auth.py +7 -0
  9. examples/create_ingredient_sdk.py +34 -0
  10. examples/download_files.py +26 -0
  11. examples/integration-server/jobs/materials_auto/concurrent_cron.py +11 -0
  12. examples/integration-server/jobs/materials_auto/example_cron.py +3 -0
  13. examples/integration-server/jobs/materials_auto/example_http.py +47 -0
  14. examples/integration-server/jobs/materials_auto/example_instrument.py +100 -0
  15. examples/integration-server/jobs/materials_auto/example_parse.py +140 -0
  16. examples/integration-server/jobs/materials_auto/example_predictions.py +61 -0
  17. examples/integration-server/jobs/materials_auto/example_runsheet_wh.py +39 -0
  18. examples/integration-server/jobs/materials_auto/example_wh.py +17 -9
  19. examples/integration-server/jobs/materials_auto/profile.yaml +61 -0
  20. examples/integration-server/pyproject.toml +10 -10
  21. examples/oauth.py +7 -0
  22. examples/set_recipe_metadata_file.py +1 -1
  23. examples/upload_files.py +1 -2
  24. pkgs/argument_parser/__init__.py +8 -0
  25. pkgs/argument_parser/_is_namedtuple.py +3 -0
  26. pkgs/argument_parser/argument_parser.py +196 -63
  27. pkgs/filesystem_utils/__init__.py +1 -0
  28. pkgs/filesystem_utils/_blob_session.py +144 -0
  29. pkgs/filesystem_utils/_gdrive_session.py +5 -5
  30. pkgs/filesystem_utils/_s3_session.py +2 -1
  31. pkgs/filesystem_utils/_sftp_session.py +6 -3
  32. pkgs/filesystem_utils/file_type_utils.py +30 -10
  33. pkgs/serialization/__init__.py +7 -2
  34. pkgs/serialization/annotation.py +64 -0
  35. pkgs/serialization/missing_sentry.py +1 -1
  36. pkgs/serialization/opaque_key.py +1 -1
  37. pkgs/serialization/serial_alias.py +47 -0
  38. pkgs/serialization/serial_class.py +40 -48
  39. pkgs/serialization/serial_generic.py +16 -0
  40. pkgs/serialization/serial_union.py +16 -16
  41. pkgs/serialization_util/__init__.py +6 -0
  42. pkgs/serialization_util/dataclasses.py +14 -0
  43. pkgs/serialization_util/serialization_helpers.py +15 -5
  44. pkgs/type_spec/actions_registry/__main__.py +0 -4
  45. pkgs/type_spec/actions_registry/emit_typescript.py +2 -4
  46. pkgs/type_spec/builder.py +248 -70
  47. pkgs/type_spec/builder_types.py +9 -0
  48. pkgs/type_spec/config.py +40 -7
  49. pkgs/type_spec/cross_output_links.py +99 -0
  50. pkgs/type_spec/emit_open_api.py +121 -34
  51. pkgs/type_spec/emit_open_api_util.py +5 -5
  52. pkgs/type_spec/emit_python.py +277 -86
  53. pkgs/type_spec/emit_typescript.py +102 -29
  54. pkgs/type_spec/emit_typescript_util.py +66 -10
  55. pkgs/type_spec/load_types.py +16 -3
  56. pkgs/type_spec/non_discriminated_union_exceptions.py +14 -0
  57. pkgs/type_spec/open_api_util.py +29 -4
  58. pkgs/type_spec/parts/base.py.prepart +11 -8
  59. pkgs/type_spec/parts/base.ts.prepart +4 -0
  60. pkgs/type_spec/type_info/__main__.py +3 -1
  61. pkgs/type_spec/type_info/emit_type_info.py +115 -22
  62. pkgs/type_spec/ui_entry_actions/__init__.py +4 -0
  63. pkgs/type_spec/ui_entry_actions/generate_ui_entry_actions.py +308 -0
  64. pkgs/type_spec/util.py +3 -3
  65. pkgs/type_spec/value_spec/__main__.py +26 -9
  66. pkgs/type_spec/value_spec/convert_type.py +18 -0
  67. pkgs/type_spec/value_spec/emit_python.py +13 -3
  68. pkgs/type_spec/value_spec/types.py +1 -1
  69. uncountable/core/async_batch.py +1 -1
  70. uncountable/core/client.py +133 -34
  71. uncountable/core/environment.py +3 -3
  72. uncountable/core/file_upload.py +39 -15
  73. uncountable/integration/cli.py +116 -23
  74. uncountable/integration/construct_client.py +3 -3
  75. uncountable/integration/executors/executors.py +12 -2
  76. uncountable/integration/executors/generic_upload_executor.py +66 -14
  77. uncountable/integration/http_server/__init__.py +5 -0
  78. uncountable/integration/http_server/types.py +69 -0
  79. uncountable/integration/job.py +192 -7
  80. uncountable/integration/queue_runner/command_server/__init__.py +4 -0
  81. uncountable/integration/queue_runner/command_server/command_client.py +65 -0
  82. uncountable/integration/queue_runner/command_server/command_server.py +83 -5
  83. uncountable/integration/queue_runner/command_server/constants.py +4 -0
  84. uncountable/integration/queue_runner/command_server/protocol/command_server.proto +36 -0
  85. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +28 -11
  86. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +77 -1
  87. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +135 -0
  88. uncountable/integration/queue_runner/command_server/types.py +25 -2
  89. uncountable/integration/queue_runner/datastore/datastore_sqlite.py +168 -11
  90. uncountable/integration/queue_runner/datastore/interface.py +10 -0
  91. uncountable/integration/queue_runner/datastore/model.py +8 -1
  92. uncountable/integration/queue_runner/job_scheduler.py +63 -23
  93. uncountable/integration/queue_runner/queue_runner.py +10 -2
  94. uncountable/integration/queue_runner/worker.py +22 -17
  95. uncountable/integration/scan_profiles.py +1 -1
  96. uncountable/integration/scheduler.py +74 -25
  97. uncountable/integration/secret_retrieval/retrieve_secret.py +1 -1
  98. uncountable/integration/server.py +42 -12
  99. uncountable/integration/telemetry.py +63 -10
  100. uncountable/integration/webhook_server/entrypoint.py +39 -112
  101. uncountable/types/__init__.py +58 -1
  102. uncountable/types/api/batch/execute_batch.py +5 -6
  103. uncountable/types/api/batch/execute_batch_load_async.py +2 -3
  104. uncountable/types/api/chemical/convert_chemical_formats.py +10 -5
  105. uncountable/types/api/condition_parameters/__init__.py +1 -0
  106. uncountable/types/api/condition_parameters/upsert_condition_match.py +72 -0
  107. uncountable/types/api/entity/create_entities.py +7 -7
  108. uncountable/types/api/entity/create_entity.py +8 -8
  109. uncountable/types/api/entity/create_or_update_entity.py +48 -0
  110. uncountable/types/api/entity/export_entities.py +59 -0
  111. uncountable/types/api/entity/get_entities_data.py +3 -4
  112. uncountable/types/api/entity/grant_entity_permissions.py +6 -6
  113. uncountable/types/api/entity/list_aggregate.py +79 -0
  114. uncountable/types/api/entity/list_entities.py +34 -10
  115. uncountable/types/api/entity/lock_entity.py +4 -4
  116. uncountable/types/api/entity/lookup_entity.py +116 -0
  117. uncountable/types/api/entity/resolve_entity_ids.py +5 -6
  118. uncountable/types/api/entity/set_entity_field_values.py +44 -0
  119. uncountable/types/api/entity/set_values.py +3 -3
  120. uncountable/types/api/entity/transition_entity_phase.py +14 -7
  121. uncountable/types/api/entity/unlock_entity.py +3 -3
  122. uncountable/types/api/equipment/associate_equipment_input.py +2 -3
  123. uncountable/types/api/field_options/upsert_field_options.py +7 -7
  124. uncountable/types/api/files/__init__.py +1 -0
  125. uncountable/types/api/files/download_file.py +77 -0
  126. uncountable/types/api/id_source/list_id_source.py +6 -7
  127. uncountable/types/api/id_source/match_id_source.py +4 -5
  128. uncountable/types/api/input_groups/get_input_group_names.py +3 -4
  129. uncountable/types/api/inputs/create_inputs.py +10 -9
  130. uncountable/types/api/inputs/get_input_data.py +11 -12
  131. uncountable/types/api/inputs/get_input_names.py +6 -7
  132. uncountable/types/api/inputs/get_inputs_data.py +6 -7
  133. uncountable/types/api/inputs/set_input_attribute_values.py +5 -6
  134. uncountable/types/api/inputs/set_input_category.py +5 -5
  135. uncountable/types/api/inputs/set_input_subcategories.py +3 -3
  136. uncountable/types/api/inputs/set_intermediate_type.py +4 -4
  137. uncountable/types/api/integrations/__init__.py +1 -0
  138. uncountable/types/api/integrations/publish_realtime_data.py +41 -0
  139. uncountable/types/api/integrations/push_notification.py +49 -0
  140. uncountable/types/api/integrations/register_sockets_token.py +41 -0
  141. uncountable/types/api/listing/__init__.py +1 -0
  142. uncountable/types/api/listing/fetch_listing.py +58 -0
  143. uncountable/types/api/material_families/update_entity_material_families.py +3 -4
  144. uncountable/types/api/notebooks/__init__.py +1 -0
  145. uncountable/types/api/notebooks/add_notebook_content.py +119 -0
  146. uncountable/types/api/outputs/get_output_data.py +12 -13
  147. uncountable/types/api/outputs/get_output_names.py +5 -6
  148. uncountable/types/api/outputs/get_output_organization.py +173 -0
  149. uncountable/types/api/outputs/resolve_output_conditions.py +7 -8
  150. uncountable/types/api/permissions/set_core_permissions.py +16 -10
  151. uncountable/types/api/project/get_projects.py +6 -7
  152. uncountable/types/api/project/get_projects_data.py +7 -8
  153. uncountable/types/api/recipe_links/create_recipe_link.py +5 -5
  154. uncountable/types/api/recipe_links/remove_recipe_link.py +4 -4
  155. uncountable/types/api/recipe_metadata/get_recipe_metadata_data.py +6 -7
  156. uncountable/types/api/recipes/add_recipe_to_project.py +3 -3
  157. uncountable/types/api/recipes/add_time_series_data.py +64 -0
  158. uncountable/types/api/recipes/archive_recipes.py +4 -4
  159. uncountable/types/api/recipes/associate_recipe_as_input.py +5 -5
  160. uncountable/types/api/recipes/associate_recipe_as_lot.py +3 -3
  161. uncountable/types/api/recipes/clear_recipe_outputs.py +3 -3
  162. uncountable/types/api/recipes/create_mix_order.py +44 -0
  163. uncountable/types/api/recipes/create_recipe.py +8 -9
  164. uncountable/types/api/recipes/create_recipes.py +8 -9
  165. uncountable/types/api/recipes/disassociate_recipe_as_input.py +3 -3
  166. uncountable/types/api/recipes/edit_recipe_inputs.py +101 -24
  167. uncountable/types/api/recipes/get_column_calculation_values.py +4 -5
  168. uncountable/types/api/recipes/get_curve.py +4 -5
  169. uncountable/types/api/recipes/get_recipe_calculations.py +6 -7
  170. uncountable/types/api/recipes/get_recipe_links.py +3 -4
  171. uncountable/types/api/recipes/get_recipe_names.py +3 -4
  172. uncountable/types/api/recipes/get_recipe_output_metadata.py +5 -6
  173. uncountable/types/api/recipes/get_recipes_data.py +62 -34
  174. uncountable/types/api/recipes/lock_recipes.py +9 -8
  175. uncountable/types/api/recipes/remove_recipe_from_project.py +3 -3
  176. uncountable/types/api/recipes/set_recipe_inputs.py +9 -10
  177. uncountable/types/api/recipes/set_recipe_metadata.py +3 -3
  178. uncountable/types/api/recipes/set_recipe_output_annotations.py +11 -12
  179. uncountable/types/api/recipes/set_recipe_output_file.py +5 -6
  180. uncountable/types/api/recipes/set_recipe_outputs.py +24 -13
  181. uncountable/types/api/recipes/set_recipe_tags.py +14 -9
  182. uncountable/types/api/recipes/set_recipe_total.py +59 -0
  183. uncountable/types/api/recipes/unarchive_recipes.py +3 -3
  184. uncountable/types/api/recipes/unlock_recipes.py +7 -6
  185. uncountable/types/api/runsheet/__init__.py +1 -0
  186. uncountable/types/api/runsheet/complete_async_upload.py +41 -0
  187. uncountable/types/api/triggers/run_trigger.py +4 -4
  188. uncountable/types/api/uploader/complete_async_parse.py +46 -0
  189. uncountable/types/api/uploader/invoke_uploader.py +4 -5
  190. uncountable/types/api/user/__init__.py +1 -0
  191. uncountable/types/api/user/get_current_user_info.py +40 -0
  192. uncountable/types/async_batch.py +1 -1
  193. uncountable/types/async_batch_processor.py +506 -23
  194. uncountable/types/async_batch_t.py +35 -8
  195. uncountable/types/async_jobs.py +0 -1
  196. uncountable/types/async_jobs_t.py +1 -2
  197. uncountable/types/auth_retrieval.py +0 -1
  198. uncountable/types/auth_retrieval_t.py +6 -6
  199. uncountable/types/base.py +0 -1
  200. uncountable/types/base_t.py +11 -9
  201. uncountable/types/calculations.py +0 -1
  202. uncountable/types/calculations_t.py +1 -2
  203. uncountable/types/chemical_structure.py +0 -1
  204. uncountable/types/chemical_structure_t.py +5 -5
  205. uncountable/types/client_base.py +614 -69
  206. uncountable/types/client_config.py +1 -1
  207. uncountable/types/client_config_t.py +13 -3
  208. uncountable/types/curves.py +0 -1
  209. uncountable/types/curves_t.py +6 -7
  210. uncountable/types/data.py +12 -0
  211. uncountable/types/data_t.py +103 -0
  212. uncountable/types/entity.py +1 -1
  213. uncountable/types/entity_t.py +90 -10
  214. uncountable/types/experiment_groups.py +0 -1
  215. uncountable/types/experiment_groups_t.py +1 -2
  216. uncountable/types/exports.py +8 -0
  217. uncountable/types/exports_t.py +34 -0
  218. uncountable/types/field_values.py +19 -1
  219. uncountable/types/field_values_t.py +242 -9
  220. uncountable/types/fields.py +0 -1
  221. uncountable/types/fields_t.py +1 -2
  222. uncountable/types/generic_upload.py +0 -1
  223. uncountable/types/generic_upload_t.py +14 -14
  224. uncountable/types/id_source.py +0 -1
  225. uncountable/types/id_source_t.py +13 -7
  226. uncountable/types/identifier.py +0 -1
  227. uncountable/types/identifier_t.py +10 -5
  228. uncountable/types/input_attributes.py +0 -1
  229. uncountable/types/input_attributes_t.py +3 -4
  230. uncountable/types/inputs.py +0 -1
  231. uncountable/types/inputs_t.py +3 -4
  232. uncountable/types/integration_server.py +0 -1
  233. uncountable/types/integration_server_t.py +13 -4
  234. uncountable/types/integration_session.py +10 -0
  235. uncountable/types/integration_session_t.py +60 -0
  236. uncountable/types/integrations.py +10 -0
  237. uncountable/types/integrations_t.py +62 -0
  238. uncountable/types/job_definition.py +2 -1
  239. uncountable/types/job_definition_t.py +57 -32
  240. uncountable/types/listing.py +9 -0
  241. uncountable/types/listing_t.py +51 -0
  242. uncountable/types/notices.py +8 -0
  243. uncountable/types/notices_t.py +37 -0
  244. uncountable/types/notifications.py +11 -0
  245. uncountable/types/notifications_t.py +74 -0
  246. uncountable/types/outputs.py +0 -1
  247. uncountable/types/outputs_t.py +2 -3
  248. uncountable/types/overrides.py +0 -1
  249. uncountable/types/overrides_t.py +10 -4
  250. uncountable/types/permissions.py +0 -1
  251. uncountable/types/permissions_t.py +1 -2
  252. uncountable/types/phases.py +0 -1
  253. uncountable/types/phases_t.py +1 -2
  254. uncountable/types/post_base.py +0 -1
  255. uncountable/types/post_base_t.py +1 -2
  256. uncountable/types/queued_job.py +2 -1
  257. uncountable/types/queued_job_t.py +29 -12
  258. uncountable/types/recipe_identifiers.py +0 -1
  259. uncountable/types/recipe_identifiers_t.py +18 -8
  260. uncountable/types/recipe_inputs.py +0 -1
  261. uncountable/types/recipe_inputs_t.py +1 -2
  262. uncountable/types/recipe_links.py +0 -1
  263. uncountable/types/recipe_links_t.py +3 -4
  264. uncountable/types/recipe_metadata.py +0 -1
  265. uncountable/types/recipe_metadata_t.py +9 -10
  266. uncountable/types/recipe_output_metadata.py +0 -1
  267. uncountable/types/recipe_output_metadata_t.py +1 -2
  268. uncountable/types/recipe_tags.py +0 -1
  269. uncountable/types/recipe_tags_t.py +1 -2
  270. uncountable/types/recipe_workflow_steps.py +0 -1
  271. uncountable/types/recipe_workflow_steps_t.py +7 -7
  272. uncountable/types/recipes.py +0 -1
  273. uncountable/types/recipes_t.py +2 -2
  274. uncountable/types/response.py +0 -1
  275. uncountable/types/response_t.py +2 -2
  276. uncountable/types/secret_retrieval.py +0 -1
  277. uncountable/types/secret_retrieval_t.py +7 -7
  278. uncountable/types/sockets.py +20 -0
  279. uncountable/types/sockets_t.py +169 -0
  280. uncountable/types/structured_filters.py +25 -0
  281. uncountable/types/structured_filters_t.py +248 -0
  282. uncountable/types/units.py +0 -1
  283. uncountable/types/units_t.py +1 -2
  284. uncountable/types/uploader.py +24 -0
  285. uncountable/types/uploader_t.py +222 -0
  286. uncountable/types/users.py +0 -1
  287. uncountable/types/users_t.py +1 -2
  288. uncountable/types/webhook_job.py +1 -1
  289. uncountable/types/webhook_job_t.py +14 -3
  290. uncountable/types/workflows.py +0 -1
  291. uncountable/types/workflows_t.py +3 -4
  292. uncountablepythonsdk-0.0.132.dist-info/METADATA +64 -0
  293. uncountablepythonsdk-0.0.132.dist-info/RECORD +363 -0
  294. {UncountablePythonSDK-0.0.82.dist-info → uncountablepythonsdk-0.0.132.dist-info}/WHEEL +1 -1
  295. UncountablePythonSDK-0.0.82.dist-info/METADATA +0 -60
  296. UncountablePythonSDK-0.0.82.dist-info/RECORD +0 -292
  297. docs/quickstart.md +0 -19
  298. {UncountablePythonSDK-0.0.82.dist-info → uncountablepythonsdk-0.0.132.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,8 @@
1
+ import datetime
1
2
  import uuid
2
- from datetime import datetime, timezone
3
+ from datetime import UTC
3
4
 
4
- from sqlalchemy import delete, insert, select, update
5
+ from sqlalchemy import delete, insert, or_, select, text, update
5
6
  from sqlalchemy.engine import Engine
6
7
 
7
8
  from pkgs.argument_parser import CachedParser
@@ -13,6 +14,8 @@ from uncountable.types import queued_job_t
13
14
 
14
15
  queued_job_payload_parser = CachedParser(queued_job_t.QueuedJobPayload)
15
16
 
17
+ MAX_QUEUE_WINDOW_DAYS = 30
18
+
16
19
 
17
20
  class DatastoreSqlite(Datastore):
18
21
  def __init__(self, session_maker: DBSessionMaker) -> None:
@@ -22,6 +25,17 @@ class DatastoreSqlite(Datastore):
22
25
  @classmethod
23
26
  def setup(cls, engine: Engine) -> None:
24
27
  Base.metadata.create_all(engine)
28
+ with engine.connect() as connection:
29
+ if not bool(
30
+ connection.execute(
31
+ text(
32
+ "select exists (select 1 from pragma_table_info('queued_jobs') where name='status');"
33
+ )
34
+ ).scalar()
35
+ ):
36
+ connection.execute(
37
+ text("alter table queued_jobs add column status VARCHAR")
38
+ )
25
39
 
26
40
  def add_job_to_queue(
27
41
  self, job_payload: queued_job_t.QueuedJobPayload, job_ref_name: str
@@ -30,11 +44,12 @@ class DatastoreSqlite(Datastore):
30
44
  serialized_payload = serialize_for_storage(job_payload)
31
45
  queued_job_uuid = str(uuid.uuid4())
32
46
  num_attempts = 0
33
- submitted_at = datetime.now(timezone.utc)
47
+ submitted_at = datetime.datetime.now(UTC)
34
48
  insert_stmt = insert(QueuedJob).values({
35
49
  QueuedJob.id.key: queued_job_uuid,
36
50
  QueuedJob.job_ref_name.key: job_ref_name,
37
51
  QueuedJob.payload.key: serialized_payload,
52
+ QueuedJob.status.key: queued_job_t.JobStatus.QUEUED,
38
53
  QueuedJob.num_attempts: num_attempts,
39
54
  QueuedJob.submitted_at: submitted_at,
40
55
  })
@@ -43,10 +58,48 @@ class DatastoreSqlite(Datastore):
43
58
  queued_job_uuid=queued_job_uuid,
44
59
  job_ref_name=job_ref_name,
45
60
  payload=job_payload,
61
+ status=queued_job_t.JobStatus.QUEUED,
46
62
  submitted_at=submitted_at,
47
63
  num_attempts=num_attempts,
48
64
  )
49
65
 
66
+ def retry_job(
67
+ self,
68
+ queued_job_uuid: str,
69
+ ) -> queued_job_t.QueuedJob | None:
70
+ with self.session_maker() as session:
71
+ select_stmt = select(
72
+ QueuedJob.id,
73
+ QueuedJob.payload,
74
+ QueuedJob.num_attempts,
75
+ QueuedJob.job_ref_name,
76
+ QueuedJob.status,
77
+ QueuedJob.submitted_at,
78
+ ).filter(QueuedJob.id == queued_job_uuid)
79
+ existing_job = session.execute(select_stmt).one_or_none()
80
+
81
+ if (
82
+ existing_job is None
83
+ or existing_job.status != queued_job_t.JobStatus.FAILED
84
+ ):
85
+ return None
86
+
87
+ update_stmt = (
88
+ update(QueuedJob)
89
+ .values({QueuedJob.status.key: queued_job_t.JobStatus.QUEUED})
90
+ .filter(QueuedJob.id == queued_job_uuid)
91
+ )
92
+ session.execute(update_stmt)
93
+
94
+ return queued_job_t.QueuedJob(
95
+ queued_job_uuid=existing_job.id,
96
+ job_ref_name=existing_job.job_ref_name,
97
+ num_attempts=existing_job.num_attempts,
98
+ status=queued_job_t.JobStatus.QUEUED,
99
+ submitted_at=existing_job.submitted_at,
100
+ payload=queued_job_payload_parser.parse_storage(existing_job.payload),
101
+ )
102
+
50
103
  def increment_num_attempts(self, queued_job_uuid: str) -> int:
51
104
  with self.session_maker() as session:
52
105
  update_stmt = (
@@ -56,7 +109,7 @@ class DatastoreSqlite(Datastore):
56
109
  )
57
110
  session.execute(update_stmt)
58
111
  session.flush()
59
- # IMPROVE: python3.12's sqlite does not support the RETURNING clause
112
+ # IMPROVE: python3's sqlite does not support the RETURNING clause
60
113
  select_stmt = select(QueuedJob.num_attempts).filter(
61
114
  QueuedJob.id == queued_job_uuid
62
115
  )
@@ -67,15 +120,103 @@ class DatastoreSqlite(Datastore):
67
120
  delete_stmt = delete(QueuedJob).filter(QueuedJob.id == queued_job_uuid)
68
121
  session.execute(delete_stmt)
69
122
 
123
+ def update_job_status(
124
+ self, queued_job_uuid: str, status: queued_job_t.JobStatus
125
+ ) -> None:
126
+ with self.session_maker() as session:
127
+ update_stmt = (
128
+ update(QueuedJob)
129
+ .values({QueuedJob.status.key: status})
130
+ .filter(QueuedJob.id == queued_job_uuid)
131
+ )
132
+ session.execute(update_stmt)
133
+
134
+ def list_queued_job_metadata(
135
+ self, offset: int = 0, limit: int | None = 100
136
+ ) -> list[queued_job_t.QueuedJobMetadata]:
137
+ with self.session_maker() as session:
138
+ select_statement = (
139
+ select(
140
+ QueuedJob.id,
141
+ QueuedJob.job_ref_name,
142
+ QueuedJob.num_attempts,
143
+ QueuedJob.status,
144
+ QueuedJob.submitted_at,
145
+ )
146
+ .order_by(QueuedJob.submitted_at)
147
+ .offset(offset)
148
+ .limit(limit)
149
+ )
150
+
151
+ queued_job_metadata: list[queued_job_t.QueuedJobMetadata] = [
152
+ queued_job_t.QueuedJobMetadata(
153
+ queued_job_uuid=row.id,
154
+ job_ref_name=row.job_ref_name,
155
+ num_attempts=row.num_attempts,
156
+ status=row.status or queued_job_t.JobStatus.QUEUED,
157
+ submitted_at=row.submitted_at,
158
+ )
159
+ for row in session.execute(select_statement)
160
+ ]
161
+
162
+ return queued_job_metadata
163
+
164
+ def get_next_queued_job_for_ref_name(
165
+ self, job_ref_name: str
166
+ ) -> queued_job_t.QueuedJob | None:
167
+ with self.session_maker() as session:
168
+ select_stmt = (
169
+ select(
170
+ QueuedJob.id,
171
+ QueuedJob.payload,
172
+ QueuedJob.num_attempts,
173
+ QueuedJob.job_ref_name,
174
+ QueuedJob.status,
175
+ QueuedJob.submitted_at,
176
+ )
177
+ .filter(QueuedJob.job_ref_name == job_ref_name)
178
+ .filter(
179
+ or_(
180
+ QueuedJob.status == queued_job_t.JobStatus.QUEUED,
181
+ QueuedJob.status.is_(None),
182
+ )
183
+ )
184
+ .limit(1)
185
+ .order_by(QueuedJob.submitted_at)
186
+ )
187
+
188
+ for row in session.execute(select_stmt):
189
+ parsed_payload = queued_job_payload_parser.parse_storage(row.payload)
190
+ return queued_job_t.QueuedJob(
191
+ queued_job_uuid=row.id,
192
+ job_ref_name=row.job_ref_name,
193
+ num_attempts=row.num_attempts,
194
+ status=row.status or queued_job_t.JobStatus.QUEUED,
195
+ submitted_at=row.submitted_at,
196
+ payload=parsed_payload,
197
+ )
198
+
199
+ return None
200
+
70
201
  def load_job_queue(self) -> list[queued_job_t.QueuedJob]:
71
202
  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)
203
+ select_stmt = (
204
+ select(
205
+ QueuedJob.id,
206
+ QueuedJob.payload,
207
+ QueuedJob.num_attempts,
208
+ QueuedJob.job_ref_name,
209
+ QueuedJob.status,
210
+ QueuedJob.submitted_at,
211
+ )
212
+ .filter(
213
+ or_(
214
+ QueuedJob.status == queued_job_t.JobStatus.QUEUED,
215
+ QueuedJob.status.is_(None),
216
+ )
217
+ )
218
+ .order_by(QueuedJob.submitted_at)
219
+ )
79
220
 
80
221
  queued_jobs: list[queued_job_t.QueuedJob] = []
81
222
  for row in session.execute(select_stmt):
@@ -85,9 +226,25 @@ class DatastoreSqlite(Datastore):
85
226
  queued_job_uuid=row.id,
86
227
  job_ref_name=row.job_ref_name,
87
228
  num_attempts=row.num_attempts,
229
+ status=row.status or queued_job_t.JobStatus.QUEUED,
88
230
  submitted_at=row.submitted_at,
89
231
  payload=parsed_payload,
90
232
  )
91
233
  )
92
234
 
93
235
  return queued_jobs
236
+
237
+ def vaccuum_queued_jobs(self) -> None:
238
+ with self.session_maker() as session:
239
+ delete_stmt = (
240
+ delete(QueuedJob)
241
+ .filter(QueuedJob.status == queued_job_t.JobStatus.QUEUED)
242
+ .filter(
243
+ QueuedJob.submitted_at
244
+ <= (
245
+ datetime.datetime.now(UTC)
246
+ - datetime.timedelta(days=MAX_QUEUE_WINDOW_DAYS)
247
+ )
248
+ )
249
+ )
250
+ session.execute(delete_stmt)
@@ -17,3 +17,13 @@ class Datastore(ABC):
17
17
 
18
18
  @abstractmethod
19
19
  def load_job_queue(self) -> list[queued_job_t.QueuedJob]: ...
20
+
21
+ @abstractmethod
22
+ def get_next_queued_job_for_ref_name(
23
+ self, job_ref_name: str
24
+ ) -> queued_job_t.QueuedJob | None: ...
25
+
26
+ @abstractmethod
27
+ def list_queued_job_metadata(
28
+ self, offset: int, limit: int | None
29
+ ) -> list[queued_job_t.QueuedJobMetadata]: ...
@@ -1,7 +1,9 @@
1
- from sqlalchemy import JSON, BigInteger, Column, DateTime, Text
1
+ from sqlalchemy import JSON, BigInteger, Column, DateTime, Enum, Text
2
2
  from sqlalchemy.orm import declarative_base
3
3
  from sqlalchemy.sql import func
4
4
 
5
+ from uncountable.types import queued_job_t
6
+
5
7
  Base = declarative_base()
6
8
 
7
9
 
@@ -15,3 +17,8 @@ class QueuedJob(Base):
15
17
  )
16
18
  payload = Column(JSON, nullable=False)
17
19
  num_attempts = Column(BigInteger, nullable=False, default=0, server_default="0")
20
+ status = Column(
21
+ Enum(queued_job_t.JobStatus, length=None),
22
+ default=queued_job_t.JobStatus.QUEUED,
23
+ nullable=True,
24
+ )
@@ -1,18 +1,22 @@
1
1
  import asyncio
2
+ import sys
2
3
  import typing
3
4
  from concurrent.futures import ProcessPoolExecutor
4
5
  from dataclasses import dataclass
5
6
 
6
7
  from opentelemetry.trace import get_current_span
7
8
 
8
- from uncountable.integration.db.connect import IntegrationDBService, create_db_engine
9
- from uncountable.integration.db.session import get_session_maker
10
9
  from uncountable.integration.queue_runner.command_server import (
11
10
  CommandEnqueueJob,
12
11
  CommandEnqueueJobResponse,
13
12
  CommandQueue,
13
+ CommandRetryJob,
14
+ CommandRetryJobResponse,
14
15
  CommandTask,
15
16
  )
17
+ from uncountable.integration.queue_runner.command_server.types import (
18
+ CommandVaccuumQueuedJobs,
19
+ )
16
20
  from uncountable.integration.queue_runner.datastore import DatastoreSqlite
17
21
  from uncountable.integration.queue_runner.datastore.interface import Datastore
18
22
  from uncountable.integration.queue_runner.worker import Worker
@@ -34,6 +38,10 @@ class JobListenerKey:
34
38
  def _get_job_worker_key(
35
39
  job_definition: job_definition_t.JobDefinition, profile_name: str
36
40
  ) -> JobListenerKey:
41
+ if job_definition.subqueue_name is not None:
42
+ return JobListenerKey(
43
+ profile_name=profile_name, subqueue_name=job_definition.subqueue_name
44
+ )
37
45
  return JobListenerKey(profile_name=profile_name)
38
46
 
39
47
 
@@ -41,9 +49,12 @@ def on_worker_crash(
41
49
  worker_key: JobListenerKey,
42
50
  ) -> typing.Callable[[asyncio.Task], None]:
43
51
  def hook(task: asyncio.Task) -> None:
44
- raise Exception(
45
- f"worker {worker_key.profile_name}_{worker_key.subqueue_name} crashed unexpectedly"
52
+ Logger(get_current_span()).log_exception(
53
+ Exception(
54
+ f"worker {worker_key.profile_name}_{worker_key.subqueue_name} crashed unexpectedly"
55
+ )
46
56
  )
57
+ sys.exit(1)
47
58
 
48
59
  return hook
49
60
 
@@ -75,14 +86,11 @@ def _start_workers(
75
86
  return job_worker_lookup
76
87
 
77
88
 
78
- async def start_scheduler(command_queue: CommandQueue) -> None:
89
+ async def start_scheduler(
90
+ command_queue: CommandQueue, datastore: DatastoreSqlite
91
+ ) -> None:
79
92
  logger = Logger(get_current_span())
80
93
  result_queue: ResultQueue = asyncio.Queue()
81
- engine = create_db_engine(IntegrationDBService.RUNNER)
82
- session_maker = get_session_maker(engine)
83
-
84
- datastore = DatastoreSqlite(session_maker)
85
- datastore.setup(engine)
86
94
 
87
95
  with ProcessPoolExecutor(max_workers=_MAX_JOB_WORKERS) as process_pool:
88
96
  job_worker_lookup = _start_workers(
@@ -96,7 +104,9 @@ async def start_scheduler(command_queue: CommandQueue) -> None:
96
104
  worker = job_worker_lookup[queued_job.job_ref_name]
97
105
  except KeyError as e:
98
106
  logger.log_exception(e)
99
- datastore.remove_job_from_queue(queued_job.queued_job_uuid)
107
+ datastore.update_job_status(
108
+ queued_job.queued_job_uuid, queued_job_t.JobStatus.FAILED
109
+ )
100
110
  return
101
111
  await worker.listen_queue.put(queued_job)
102
112
 
@@ -106,19 +116,16 @@ async def start_scheduler(command_queue: CommandQueue) -> None:
106
116
  ) -> str:
107
117
  if isinstance(
108
118
  payload.invocation_context,
109
- queued_job_t.InvocationContextCron,
119
+ (
120
+ queued_job_t.InvocationContextCron,
121
+ queued_job_t.InvocationContextManual,
122
+ ),
110
123
  ):
111
- existing_queued_jobs = datastore.load_job_queue()
112
- duplicate_job = next(
113
- (
114
- job
115
- for job in existing_queued_jobs
116
- if job.job_ref_name == job_ref_name
117
- ),
118
- None,
124
+ existing_queued_job = datastore.get_next_queued_job_for_ref_name(
125
+ job_ref_name=job_ref_name
119
126
  )
120
- if duplicate_job is not None:
121
- return duplicate_job.queued_job_uuid
127
+ if existing_queued_job is not None:
128
+ return existing_queued_job.queued_job_uuid
122
129
  queued_job = datastore.add_job_to_queue(
123
130
  job_payload=payload,
124
131
  job_ref_name=job_ref_name,
@@ -135,6 +142,25 @@ async def start_scheduler(command_queue: CommandQueue) -> None:
135
142
  CommandEnqueueJobResponse(queued_job_uuid=queued_job_uuid)
136
143
  )
137
144
 
145
+ async def _handle_retry_job_command(command: CommandRetryJob) -> None:
146
+ queued_job = datastore.retry_job(command.queued_job_uuid)
147
+ if queued_job is None:
148
+ await command.response_queue.put(
149
+ CommandRetryJobResponse(queued_job_uuid=None)
150
+ )
151
+ return
152
+
153
+ await enqueue_queued_job(queued_job)
154
+ await command.response_queue.put(
155
+ CommandRetryJobResponse(queued_job_uuid=queued_job.queued_job_uuid)
156
+ )
157
+
158
+ def _handle_vaccuum_queued_jobs_command(
159
+ command: CommandVaccuumQueuedJobs,
160
+ ) -> None:
161
+ logger.log_info("Vaccuuming queued jobs...")
162
+ datastore.vaccuum_queued_jobs()
163
+
138
164
  for queued_job in queued_jobs:
139
165
  await enqueue_queued_job(queued_job)
140
166
 
@@ -151,10 +177,24 @@ async def start_scheduler(command_queue: CommandQueue) -> None:
151
177
  match command:
152
178
  case CommandEnqueueJob():
153
179
  await _handle_enqueue_job_command(command=command)
180
+ case CommandRetryJob():
181
+ await _handle_retry_job_command(command=command)
182
+ case CommandVaccuumQueuedJobs():
183
+ _handle_vaccuum_queued_jobs_command(command=command)
154
184
  case _:
155
185
  typing.assert_never(command)
156
186
  command_task = asyncio.create_task(command_queue.get())
157
187
  elif task == result_task:
158
188
  queued_job_result = result_task.result()
159
- datastore.remove_job_from_queue(queued_job_result.queued_job_uuid)
189
+ match queued_job_result.job_result.success:
190
+ case True:
191
+ datastore.update_job_status(
192
+ queued_job_result.queued_job_uuid,
193
+ queued_job_t.JobStatus.SUCCESS,
194
+ )
195
+ case False:
196
+ datastore.update_job_status(
197
+ queued_job_result.queued_job_uuid,
198
+ queued_job_t.JobStatus.FAILED,
199
+ )
160
200
  result_task = asyncio.create_task(result_queue.get())
@@ -1,16 +1,24 @@
1
1
  import asyncio
2
2
 
3
+ from uncountable.integration.db.connect import IntegrationDBService, create_db_engine
4
+ from uncountable.integration.db.session import get_session_maker
3
5
  from uncountable.integration.queue_runner.command_server import serve
4
6
  from uncountable.integration.queue_runner.command_server.types import CommandQueue
7
+ from uncountable.integration.queue_runner.datastore import DatastoreSqlite
5
8
  from uncountable.integration.queue_runner.job_scheduler import start_scheduler
6
9
 
7
10
 
8
11
  async def queue_runner_loop() -> None:
9
12
  command_queue: CommandQueue = asyncio.Queue()
13
+ engine = create_db_engine(IntegrationDBService.RUNNER)
14
+ session_maker = get_session_maker(engine)
10
15
 
11
- command_server = asyncio.create_task(serve(command_queue))
16
+ datastore = DatastoreSqlite(session_maker)
17
+ datastore.setup(engine)
12
18
 
13
- scheduler = asyncio.create_task(start_scheduler(command_queue))
19
+ command_server = asyncio.create_task(serve(command_queue, datastore))
20
+
21
+ scheduler = asyncio.create_task(start_scheduler(command_queue, datastore))
14
22
 
15
23
  await scheduler
16
24
  await command_server
@@ -2,6 +2,8 @@ import asyncio
2
2
  from concurrent.futures import ProcessPoolExecutor
3
3
  from dataclasses import dataclass
4
4
 
5
+ from opentelemetry.trace import get_current_span
6
+
5
7
  from uncountable.core.async_batch import AsyncBatchProcessor
6
8
  from uncountable.integration.construct_client import construct_uncountable_client
7
9
  from uncountable.integration.executors.executors import execute_job
@@ -9,7 +11,7 @@ from uncountable.integration.job import JobArguments
9
11
  from uncountable.integration.queue_runner.datastore.interface import Datastore
10
12
  from uncountable.integration.queue_runner.types import ListenQueue, ResultQueue
11
13
  from uncountable.integration.scan_profiles import load_profiles
12
- from uncountable.integration.telemetry import JobLogger, get_otel_tracer
14
+ from uncountable.integration.telemetry import JobLogger, Logger, get_otel_tracer
13
15
  from uncountable.types import base_t, job_definition_t, queued_job_t
14
16
 
15
17
 
@@ -28,19 +30,24 @@ class Worker:
28
30
  self.datastore = datastore
29
31
 
30
32
  async def run_worker_loop(self) -> None:
33
+ logger = Logger(get_current_span())
31
34
  while True:
32
- queued_job = await self.listen_queue.get()
33
- self.datastore.increment_num_attempts(queued_job.queued_job_uuid)
34
- loop = asyncio.get_event_loop()
35
- result = await loop.run_in_executor(
36
- self.process_pool, run_queued_job, queued_job
37
- )
38
- assert isinstance(result, job_definition_t.JobResult)
39
- await self.result_queue.put(
40
- queued_job_t.QueuedJobResult(
41
- job_result=result, queued_job_uuid=queued_job.queued_job_uuid
35
+ try:
36
+ queued_job = await self.listen_queue.get()
37
+ self.datastore.increment_num_attempts(queued_job.queued_job_uuid)
38
+ loop = asyncio.get_event_loop()
39
+ result = await loop.run_in_executor(
40
+ self.process_pool, run_queued_job, queued_job
42
41
  )
43
- )
42
+ assert isinstance(result, job_definition_t.JobResult)
43
+ await self.result_queue.put(
44
+ queued_job_t.QueuedJobResult(
45
+ job_result=result, queued_job_uuid=queued_job.queued_job_uuid
46
+ )
47
+ )
48
+ except BaseException as e:
49
+ logger.log_exception(e)
50
+ raise e
44
51
 
45
52
 
46
53
  @dataclass(kw_only=True)
@@ -83,7 +90,7 @@ def run_queued_job(
83
90
  )
84
91
  try:
85
92
  client = construct_uncountable_client(
86
- profile_meta=job_details.profile_metadata, job_logger=job_logger
93
+ profile_meta=job_details.profile_metadata, logger=job_logger
87
94
  )
88
95
  batch_processor = AsyncBatchProcessor(client=client)
89
96
 
@@ -96,6 +103,7 @@ def run_queued_job(
96
103
  profile_metadata=job_details.profile_metadata,
97
104
  logger=job_logger,
98
105
  payload=payload,
106
+ job_uuid=queued_job.queued_job_uuid,
99
107
  )
100
108
 
101
109
  return execute_job(
@@ -103,9 +111,6 @@ def run_queued_job(
103
111
  profile_metadata=job_details.profile_metadata,
104
112
  job_definition=job_details.job_definition,
105
113
  )
106
- except Exception as e:
107
- job_logger.log_exception(e)
108
- return job_definition_t.JobResult(success=False)
109
114
  except BaseException as e:
110
115
  job_logger.log_exception(e)
111
- raise e
116
+ return job_definition_t.JobResult(success=False)
@@ -26,7 +26,7 @@ def load_profiles() -> list[job_definition_t.ProfileMetadata]:
26
26
  profile_name = profile_file.name
27
27
  try:
28
28
  definition = profile_parser.parse_yaml_resource(
29
- package=".".join([profiles_module, profile_name]),
29
+ package=f"{profiles_module}.{profile_name}",
30
30
  resource="profile.yaml",
31
31
  )
32
32
  for job in definition.jobs: