UncountablePythonSDK 0.0.115__py3-none-any.whl → 0.0.142.dev0__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 (119) hide show
  1. docs/conf.py +52 -5
  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 +1 -1
  7. docs/requirements.txt +3 -2
  8. examples/basic_auth.py +7 -0
  9. examples/integration-server/jobs/materials_auto/example_cron.py +3 -0
  10. examples/integration-server/jobs/materials_auto/example_http.py +19 -7
  11. examples/integration-server/jobs/materials_auto/example_instrument.py +100 -0
  12. examples/integration-server/jobs/materials_auto/example_parse.py +140 -0
  13. examples/integration-server/jobs/materials_auto/example_predictions.py +61 -0
  14. examples/integration-server/jobs/materials_auto/example_runsheet_wh.py +57 -16
  15. examples/integration-server/jobs/materials_auto/profile.yaml +27 -0
  16. examples/integration-server/pyproject.toml +4 -4
  17. examples/oauth.py +7 -0
  18. pkgs/argument_parser/__init__.py +1 -0
  19. pkgs/argument_parser/_is_namedtuple.py +3 -0
  20. pkgs/argument_parser/argument_parser.py +22 -3
  21. pkgs/serialization_util/serialization_helpers.py +3 -1
  22. pkgs/type_spec/builder.py +66 -19
  23. pkgs/type_spec/builder_types.py +9 -0
  24. pkgs/type_spec/config.py +26 -5
  25. pkgs/type_spec/cross_output_links.py +10 -16
  26. pkgs/type_spec/emit_open_api.py +72 -22
  27. pkgs/type_spec/emit_open_api_util.py +1 -0
  28. pkgs/type_spec/emit_python.py +76 -12
  29. pkgs/type_spec/emit_typescript.py +48 -32
  30. pkgs/type_spec/emit_typescript_util.py +44 -6
  31. pkgs/type_spec/load_types.py +2 -2
  32. pkgs/type_spec/open_api_util.py +16 -1
  33. pkgs/type_spec/parts/base.ts.prepart +4 -0
  34. pkgs/type_spec/type_info/emit_type_info.py +37 -4
  35. pkgs/type_spec/ui_entry_actions/generate_ui_entry_actions.py +1 -0
  36. pkgs/type_spec/value_spec/__main__.py +2 -2
  37. pkgs/type_spec/value_spec/emit_python.py +6 -1
  38. uncountable/core/client.py +10 -3
  39. uncountable/integration/cli.py +175 -23
  40. uncountable/integration/executors/executors.py +1 -2
  41. uncountable/integration/executors/generic_upload_executor.py +1 -1
  42. uncountable/integration/http_server/types.py +3 -1
  43. uncountable/integration/job.py +35 -3
  44. uncountable/integration/queue_runner/command_server/__init__.py +4 -0
  45. uncountable/integration/queue_runner/command_server/command_client.py +89 -0
  46. uncountable/integration/queue_runner/command_server/command_server.py +117 -5
  47. uncountable/integration/queue_runner/command_server/constants.py +4 -0
  48. uncountable/integration/queue_runner/command_server/protocol/command_server.proto +51 -0
  49. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +34 -11
  50. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +102 -1
  51. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +180 -0
  52. uncountable/integration/queue_runner/command_server/types.py +44 -1
  53. uncountable/integration/queue_runner/datastore/datastore_sqlite.py +189 -8
  54. uncountable/integration/queue_runner/datastore/interface.py +13 -0
  55. uncountable/integration/queue_runner/datastore/model.py +8 -1
  56. uncountable/integration/queue_runner/job_scheduler.py +85 -21
  57. uncountable/integration/queue_runner/queue_runner.py +10 -2
  58. uncountable/integration/queue_runner/types.py +2 -0
  59. uncountable/integration/queue_runner/worker.py +28 -29
  60. uncountable/integration/scheduler.py +121 -23
  61. uncountable/integration/server.py +36 -6
  62. uncountable/integration/telemetry.py +129 -8
  63. uncountable/integration/webhook_server/entrypoint.py +2 -0
  64. uncountable/types/__init__.py +38 -0
  65. uncountable/types/api/entity/create_or_update_entity.py +1 -0
  66. uncountable/types/api/entity/export_entities.py +13 -0
  67. uncountable/types/api/entity/list_aggregate.py +79 -0
  68. uncountable/types/api/entity/list_entities.py +25 -0
  69. uncountable/types/api/entity/set_barcode.py +43 -0
  70. uncountable/types/api/entity/transition_entity_phase.py +2 -1
  71. uncountable/types/api/files/download_file.py +15 -1
  72. uncountable/types/api/integrations/__init__.py +1 -0
  73. uncountable/types/api/integrations/publish_realtime_data.py +41 -0
  74. uncountable/types/api/integrations/push_notification.py +49 -0
  75. uncountable/types/api/integrations/register_sockets_token.py +41 -0
  76. uncountable/types/api/listing/__init__.py +1 -0
  77. uncountable/types/api/listing/fetch_listing.py +57 -0
  78. uncountable/types/api/notebooks/__init__.py +1 -0
  79. uncountable/types/api/notebooks/add_notebook_content.py +119 -0
  80. uncountable/types/api/outputs/get_output_organization.py +173 -0
  81. uncountable/types/api/recipes/edit_recipe_inputs.py +1 -1
  82. uncountable/types/api/recipes/get_recipe_output_metadata.py +2 -2
  83. uncountable/types/api/recipes/get_recipes_data.py +29 -0
  84. uncountable/types/api/recipes/lock_recipes.py +2 -1
  85. uncountable/types/api/recipes/set_recipe_total.py +59 -0
  86. uncountable/types/api/recipes/unlock_recipes.py +2 -1
  87. uncountable/types/api/runsheet/export_default_runsheet.py +44 -0
  88. uncountable/types/api/uploader/complete_async_parse.py +46 -0
  89. uncountable/types/api/user/__init__.py +1 -0
  90. uncountable/types/api/user/get_current_user_info.py +40 -0
  91. uncountable/types/async_batch_processor.py +266 -0
  92. uncountable/types/async_batch_t.py +5 -0
  93. uncountable/types/client_base.py +432 -2
  94. uncountable/types/client_config.py +1 -0
  95. uncountable/types/client_config_t.py +10 -0
  96. uncountable/types/entity_t.py +9 -1
  97. uncountable/types/exports_t.py +1 -0
  98. uncountable/types/integration_server_t.py +2 -0
  99. uncountable/types/integration_session.py +10 -0
  100. uncountable/types/integration_session_t.py +60 -0
  101. uncountable/types/integrations.py +10 -0
  102. uncountable/types/integrations_t.py +62 -0
  103. uncountable/types/listing.py +46 -0
  104. uncountable/types/listing_t.py +533 -0
  105. uncountable/types/notices.py +8 -0
  106. uncountable/types/notices_t.py +37 -0
  107. uncountable/types/notifications.py +11 -0
  108. uncountable/types/notifications_t.py +74 -0
  109. uncountable/types/queued_job.py +2 -0
  110. uncountable/types/queued_job_t.py +20 -2
  111. uncountable/types/sockets.py +20 -0
  112. uncountable/types/sockets_t.py +169 -0
  113. uncountable/types/uploader.py +24 -0
  114. uncountable/types/uploader_t.py +222 -0
  115. {uncountablepythonsdk-0.0.115.dist-info → uncountablepythonsdk-0.0.142.dev0.dist-info}/METADATA +5 -2
  116. {uncountablepythonsdk-0.0.115.dist-info → uncountablepythonsdk-0.0.142.dev0.dist-info}/RECORD +118 -79
  117. docs/quickstart.md +0 -19
  118. {uncountablepythonsdk-0.0.115.dist-info → uncountablepythonsdk-0.0.142.dev0.dist-info}/WHEEL +0 -0
  119. {uncountablepythonsdk-0.0.115.dist-info → uncountablepythonsdk-0.0.142.dev0.dist-info}/top_level.txt +0 -0
@@ -24,11 +24,31 @@ class CommandServerStub(object):
24
24
  request_serializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.EnqueueJobRequest.SerializeToString,
25
25
  response_deserializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.EnqueueJobResult.FromString,
26
26
  )
27
+ self.RetryJob = channel.unary_unary(
28
+ "/CommandServer/RetryJob",
29
+ request_serializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.RetryJobRequest.SerializeToString,
30
+ response_deserializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.RetryJobResult.FromString,
31
+ )
27
32
  self.CheckHealth = channel.unary_unary(
28
33
  "/CommandServer/CheckHealth",
29
34
  request_serializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.CheckHealthRequest.SerializeToString,
30
35
  response_deserializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.CheckHealthResult.FromString,
31
36
  )
37
+ self.ListQueuedJobs = channel.unary_unary(
38
+ "/CommandServer/ListQueuedJobs",
39
+ request_serializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.ListQueuedJobsRequest.SerializeToString,
40
+ response_deserializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.ListQueuedJobsResult.FromString,
41
+ )
42
+ self.VaccuumQueuedJobs = channel.unary_unary(
43
+ "/CommandServer/VaccuumQueuedJobs",
44
+ request_serializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.VaccuumQueuedJobsRequest.SerializeToString,
45
+ response_deserializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.VaccuumQueuedJobsResult.FromString,
46
+ )
47
+ self.CancelJob = channel.unary_unary(
48
+ "/CommandServer/CancelJob",
49
+ request_serializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.CancelJobRequest.SerializeToString,
50
+ response_deserializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.CancelJobResult.FromString,
51
+ )
32
52
 
33
53
 
34
54
  class CommandServerServicer(object):
@@ -40,12 +60,36 @@ class CommandServerServicer(object):
40
60
  context.set_details("Method not implemented!")
41
61
  raise NotImplementedError("Method not implemented!")
42
62
 
63
+ def RetryJob(self, request, context):
64
+ """Missing associated documentation comment in .proto file."""
65
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
66
+ context.set_details("Method not implemented!")
67
+ raise NotImplementedError("Method not implemented!")
68
+
43
69
  def CheckHealth(self, request, context):
44
70
  """Missing associated documentation comment in .proto file."""
45
71
  context.set_code(grpc.StatusCode.UNIMPLEMENTED)
46
72
  context.set_details("Method not implemented!")
47
73
  raise NotImplementedError("Method not implemented!")
48
74
 
75
+ def ListQueuedJobs(self, request, context):
76
+ """Missing associated documentation comment in .proto file."""
77
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
78
+ context.set_details("Method not implemented!")
79
+ raise NotImplementedError("Method not implemented!")
80
+
81
+ def VaccuumQueuedJobs(self, request, context):
82
+ """Missing associated documentation comment in .proto file."""
83
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
84
+ context.set_details("Method not implemented!")
85
+ raise NotImplementedError("Method not implemented!")
86
+
87
+ def CancelJob(self, request, context):
88
+ """Missing associated documentation comment in .proto file."""
89
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
90
+ context.set_details("Method not implemented!")
91
+ raise NotImplementedError("Method not implemented!")
92
+
49
93
 
50
94
  def add_CommandServerServicer_to_server(servicer, server):
51
95
  rpc_method_handlers = {
@@ -54,11 +98,31 @@ def add_CommandServerServicer_to_server(servicer, server):
54
98
  request_deserializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.EnqueueJobRequest.FromString,
55
99
  response_serializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.EnqueueJobResult.SerializeToString,
56
100
  ),
101
+ "RetryJob": grpc.unary_unary_rpc_method_handler(
102
+ servicer.RetryJob,
103
+ request_deserializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.RetryJobRequest.FromString,
104
+ response_serializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.RetryJobResult.SerializeToString,
105
+ ),
57
106
  "CheckHealth": grpc.unary_unary_rpc_method_handler(
58
107
  servicer.CheckHealth,
59
108
  request_deserializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.CheckHealthRequest.FromString,
60
109
  response_serializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.CheckHealthResult.SerializeToString,
61
110
  ),
111
+ "ListQueuedJobs": grpc.unary_unary_rpc_method_handler(
112
+ servicer.ListQueuedJobs,
113
+ request_deserializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.ListQueuedJobsRequest.FromString,
114
+ response_serializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.ListQueuedJobsResult.SerializeToString,
115
+ ),
116
+ "VaccuumQueuedJobs": grpc.unary_unary_rpc_method_handler(
117
+ servicer.VaccuumQueuedJobs,
118
+ request_deserializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.VaccuumQueuedJobsRequest.FromString,
119
+ response_serializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.VaccuumQueuedJobsResult.SerializeToString,
120
+ ),
121
+ "CancelJob": grpc.unary_unary_rpc_method_handler(
122
+ servicer.CancelJob,
123
+ request_deserializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.CancelJobRequest.FromString,
124
+ response_serializer=uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.CancelJobResult.SerializeToString,
125
+ ),
62
126
  }
63
127
  generic_handler = grpc.method_handlers_generic_handler(
64
128
  "CommandServer", rpc_method_handlers
@@ -99,6 +163,35 @@ class CommandServer(object):
99
163
  metadata,
100
164
  )
101
165
 
166
+ @staticmethod
167
+ def RetryJob(
168
+ request,
169
+ target,
170
+ options=(),
171
+ channel_credentials=None,
172
+ call_credentials=None,
173
+ insecure=False,
174
+ compression=None,
175
+ wait_for_ready=None,
176
+ timeout=None,
177
+ metadata=None,
178
+ ):
179
+ return grpc.experimental.unary_unary(
180
+ request,
181
+ target,
182
+ "/CommandServer/RetryJob",
183
+ uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.RetryJobRequest.SerializeToString,
184
+ uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.RetryJobResult.FromString,
185
+ options,
186
+ channel_credentials,
187
+ insecure,
188
+ call_credentials,
189
+ compression,
190
+ wait_for_ready,
191
+ timeout,
192
+ metadata,
193
+ )
194
+
102
195
  @staticmethod
103
196
  def CheckHealth(
104
197
  request,
@@ -127,3 +220,90 @@ class CommandServer(object):
127
220
  timeout,
128
221
  metadata,
129
222
  )
223
+
224
+ @staticmethod
225
+ def ListQueuedJobs(
226
+ request,
227
+ target,
228
+ options=(),
229
+ channel_credentials=None,
230
+ call_credentials=None,
231
+ insecure=False,
232
+ compression=None,
233
+ wait_for_ready=None,
234
+ timeout=None,
235
+ metadata=None,
236
+ ):
237
+ return grpc.experimental.unary_unary(
238
+ request,
239
+ target,
240
+ "/CommandServer/ListQueuedJobs",
241
+ uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.ListQueuedJobsRequest.SerializeToString,
242
+ uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.ListQueuedJobsResult.FromString,
243
+ options,
244
+ channel_credentials,
245
+ insecure,
246
+ call_credentials,
247
+ compression,
248
+ wait_for_ready,
249
+ timeout,
250
+ metadata,
251
+ )
252
+
253
+ @staticmethod
254
+ def VaccuumQueuedJobs(
255
+ request,
256
+ target,
257
+ options=(),
258
+ channel_credentials=None,
259
+ call_credentials=None,
260
+ insecure=False,
261
+ compression=None,
262
+ wait_for_ready=None,
263
+ timeout=None,
264
+ metadata=None,
265
+ ):
266
+ return grpc.experimental.unary_unary(
267
+ request,
268
+ target,
269
+ "/CommandServer/VaccuumQueuedJobs",
270
+ uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.VaccuumQueuedJobsRequest.SerializeToString,
271
+ uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.VaccuumQueuedJobsResult.FromString,
272
+ options,
273
+ channel_credentials,
274
+ insecure,
275
+ call_credentials,
276
+ compression,
277
+ wait_for_ready,
278
+ timeout,
279
+ metadata,
280
+ )
281
+
282
+ @staticmethod
283
+ def CancelJob(
284
+ request,
285
+ target,
286
+ options=(),
287
+ channel_credentials=None,
288
+ call_credentials=None,
289
+ insecure=False,
290
+ compression=None,
291
+ wait_for_ready=None,
292
+ timeout=None,
293
+ metadata=None,
294
+ ):
295
+ return grpc.experimental.unary_unary(
296
+ request,
297
+ target,
298
+ "/CommandServer/CancelJob",
299
+ uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.CancelJobRequest.SerializeToString,
300
+ uncountable_dot_integration_dot_queue__runner_dot_command__server_dot_protocol_dot_command__server__pb2.CancelJobResult.FromString,
301
+ options,
302
+ channel_credentials,
303
+ insecure,
304
+ call_credentials,
305
+ compression,
306
+ wait_for_ready,
307
+ timeout,
308
+ metadata,
309
+ )
@@ -6,8 +6,17 @@ from enum import StrEnum
6
6
  from uncountable.types import queued_job_t
7
7
 
8
8
 
9
+ class CommandCancelJobStatus(StrEnum):
10
+ CANCELLED_WITH_RESTART = "cancelled_with_restart"
11
+ NO_JOB_FOUND = "no_job_found"
12
+ JOB_ALREADY_COMPLETED = "job_already_completed"
13
+
14
+
9
15
  class CommandType(StrEnum):
10
16
  ENQUEUE_JOB = "enqueue_job"
17
+ RETRY_JOB = "retry_job"
18
+ VACCUUM_QUEUED_JOBS = "vaccuum_queued_jobs"
19
+ CANCEL_JOB = "cancel_job"
11
20
 
12
21
 
13
22
  RT = typing.TypeVar("RT")
@@ -24,6 +33,16 @@ class CommandEnqueueJobResponse:
24
33
  queued_job_uuid: str
25
34
 
26
35
 
36
+ @dataclass(kw_only=True)
37
+ class CommandRetryJobResponse:
38
+ queued_job_uuid: str | None
39
+
40
+
41
+ @dataclass(kw_only=True)
42
+ class CommandVaccuumQueuedJobsResponse:
43
+ pass
44
+
45
+
27
46
  @dataclass(kw_only=True)
28
47
  class CommandEnqueueJob(CommandBase[CommandEnqueueJobResponse]):
29
48
  type: CommandType = CommandType.ENQUEUE_JOB
@@ -32,7 +51,31 @@ class CommandEnqueueJob(CommandBase[CommandEnqueueJobResponse]):
32
51
  response_queue: asyncio.Queue[CommandEnqueueJobResponse]
33
52
 
34
53
 
35
- _Command = CommandEnqueueJob
54
+ @dataclass(kw_only=True)
55
+ class CommandRetryJob(CommandBase[CommandRetryJobResponse]):
56
+ type: CommandType = CommandType.RETRY_JOB
57
+ queued_job_uuid: str
58
+
59
+
60
+ @dataclass(kw_only=True)
61
+ class CommandVaccuumQueuedJobs(CommandBase[CommandVaccuumQueuedJobsResponse]):
62
+ type: CommandType = CommandType.VACCUUM_QUEUED_JOBS
63
+
64
+
65
+ @dataclass(kw_only=True)
66
+ class CommandCancelJobResponse:
67
+ status: CommandCancelJobStatus
68
+
69
+
70
+ @dataclass(kw_only=True)
71
+ class CommandCancelJob(CommandBase[CommandCancelJobResponse]):
72
+ type: CommandType = CommandType.CANCEL_JOB
73
+ queued_job_uuid: str
74
+
75
+
76
+ _Command = (
77
+ CommandEnqueueJob | CommandRetryJob | CommandVaccuumQueuedJobs | CommandCancelJob
78
+ )
36
79
 
37
80
 
38
81
  CommandQueue = asyncio.Queue[_Command]
@@ -2,7 +2,7 @@ import datetime
2
2
  import uuid
3
3
  from datetime import UTC
4
4
 
5
- from sqlalchemy import delete, insert, select, update
5
+ from sqlalchemy import delete, insert, or_, select, text, update
6
6
  from sqlalchemy.engine import Engine
7
7
 
8
8
  from pkgs.argument_parser import CachedParser
@@ -14,6 +14,8 @@ from uncountable.types import queued_job_t
14
14
 
15
15
  queued_job_payload_parser = CachedParser(queued_job_t.QueuedJobPayload)
16
16
 
17
+ MAX_QUEUE_WINDOW_DAYS = 30
18
+
17
19
 
18
20
  class DatastoreSqlite(Datastore):
19
21
  def __init__(self, session_maker: DBSessionMaker) -> None:
@@ -23,6 +25,17 @@ class DatastoreSqlite(Datastore):
23
25
  @classmethod
24
26
  def setup(cls, engine: Engine) -> None:
25
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
+ )
26
39
 
27
40
  def add_job_to_queue(
28
41
  self, job_payload: queued_job_t.QueuedJobPayload, job_ref_name: str
@@ -36,6 +49,7 @@ class DatastoreSqlite(Datastore):
36
49
  QueuedJob.id.key: queued_job_uuid,
37
50
  QueuedJob.job_ref_name.key: job_ref_name,
38
51
  QueuedJob.payload.key: serialized_payload,
52
+ QueuedJob.status.key: queued_job_t.JobStatus.QUEUED,
39
53
  QueuedJob.num_attempts: num_attempts,
40
54
  QueuedJob.submitted_at: submitted_at,
41
55
  })
@@ -44,10 +58,48 @@ class DatastoreSqlite(Datastore):
44
58
  queued_job_uuid=queued_job_uuid,
45
59
  job_ref_name=job_ref_name,
46
60
  payload=job_payload,
61
+ status=queued_job_t.JobStatus.QUEUED,
47
62
  submitted_at=submitted_at,
48
63
  num_attempts=num_attempts,
49
64
  )
50
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
+
51
103
  def increment_num_attempts(self, queued_job_uuid: str) -> int:
52
104
  with self.session_maker() as session:
53
105
  update_stmt = (
@@ -68,15 +120,103 @@ class DatastoreSqlite(Datastore):
68
120
  delete_stmt = delete(QueuedJob).filter(QueuedJob.id == queued_job_uuid)
69
121
  session.execute(delete_stmt)
70
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
+
71
201
  def load_job_queue(self) -> list[queued_job_t.QueuedJob]:
72
202
  with self.session_maker() as session:
73
- select_stmt = select(
74
- QueuedJob.id,
75
- QueuedJob.payload,
76
- QueuedJob.num_attempts,
77
- QueuedJob.job_ref_name,
78
- QueuedJob.submitted_at,
79
- ).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
+ )
80
220
 
81
221
  queued_jobs: list[queued_job_t.QueuedJob] = []
82
222
  for row in session.execute(select_stmt):
@@ -86,9 +226,50 @@ class DatastoreSqlite(Datastore):
86
226
  queued_job_uuid=row.id,
87
227
  job_ref_name=row.job_ref_name,
88
228
  num_attempts=row.num_attempts,
229
+ status=row.status or queued_job_t.JobStatus.QUEUED,
89
230
  submitted_at=row.submitted_at,
90
231
  payload=parsed_payload,
91
232
  )
92
233
  )
93
234
 
94
235
  return queued_jobs
236
+
237
+ def get_queued_job(self, *, uuid: str) -> queued_job_t.QueuedJob | None:
238
+ with self.session_maker() as session:
239
+ select_stmt = select(
240
+ QueuedJob.id,
241
+ QueuedJob.payload,
242
+ QueuedJob.num_attempts,
243
+ QueuedJob.job_ref_name,
244
+ QueuedJob.status,
245
+ QueuedJob.submitted_at,
246
+ ).filter(QueuedJob.id == uuid)
247
+
248
+ row = session.execute(select_stmt).one_or_none()
249
+ return (
250
+ queued_job_t.QueuedJob(
251
+ queued_job_uuid=row.id,
252
+ job_ref_name=row.job_ref_name,
253
+ num_attempts=row.num_attempts,
254
+ status=row.status or queued_job_t.JobStatus.QUEUED,
255
+ submitted_at=row.submitted_at,
256
+ payload=queued_job_payload_parser.parse_storage(row.payload),
257
+ )
258
+ if row is not None
259
+ else None
260
+ )
261
+
262
+ def vaccuum_queued_jobs(self) -> None:
263
+ with self.session_maker() as session:
264
+ delete_stmt = (
265
+ delete(QueuedJob)
266
+ .filter(QueuedJob.status == queued_job_t.JobStatus.QUEUED)
267
+ .filter(
268
+ QueuedJob.submitted_at
269
+ <= (
270
+ datetime.datetime.now(UTC)
271
+ - datetime.timedelta(days=MAX_QUEUE_WINDOW_DAYS)
272
+ )
273
+ )
274
+ )
275
+ session.execute(delete_stmt)
@@ -17,3 +17,16 @@ 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]: ...
30
+
31
+ @abstractmethod
32
+ def get_queued_job(self, *, uuid: str) -> queued_job_t.QueuedJob | None: ...
@@ -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
+ )