UncountablePythonSDK 0.0.189__py3-none-any.whl → 0.0.191__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.
- pkgs/type_spec/AGENTS.md +13 -0
- pkgs/type_spec/CLAUDE.md +5 -0
- pkgs/type_spec/emit_typescript_util.py +25 -4
- pkgs/type_spec/parts/base.ts.prepart +6 -1
- uncountable/core/__init__.py +2 -1
- uncountable/core/client.py +8 -1
- uncountable/core/types.py +6 -1
- uncountable/integration/cli.py +2 -0
- uncountable/integration/job.py +47 -30
- uncountable/integration/job_queue.py +14 -0
- uncountable/integration/ownership_lock.py +77 -0
- uncountable/integration/queue_runner/command_server/command_client.py +2 -0
- uncountable/integration/queue_runner/command_server/command_server.py +2 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server.proto +1 -0
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +4 -4
- uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +2 -0
- uncountable/integration/queue_runner/command_server/types.py +1 -0
- uncountable/integration/queue_runner/datastore/datastore_sql_base.py +14 -7
- uncountable/integration/queue_runner/datastore/interface.py +6 -1
- uncountable/integration/queue_runner/job_scheduler.py +66 -23
- uncountable/integration/queue_runner/worker.py +8 -0
- uncountable/integration/scheduler.py +102 -60
- uncountable/integration/webhook_server/entrypoint.py +17 -5
- uncountable/integration/webhook_server/gunicorn_config.py +9 -0
- uncountable/types/entity_t.py +102 -100
- {uncountablepythonsdk-0.0.189.dist-info → uncountablepythonsdk-0.0.191.dist-info}/METADATA +2 -1
- {uncountablepythonsdk-0.0.189.dist-info → uncountablepythonsdk-0.0.191.dist-info}/RECORD +29 -24
- {uncountablepythonsdk-0.0.189.dist-info → uncountablepythonsdk-0.0.191.dist-info}/WHEEL +0 -0
- {uncountablepythonsdk-0.0.189.dist-info → uncountablepythonsdk-0.0.191.dist-info}/top_level.txt +0 -0
pkgs/type_spec/AGENTS.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!-- DO NOT EDIT: this file is generated by `docs/tools/compile.py`.
|
|
2
|
+
Edit fragments under `docs/fragments/` or manifests under `docs/manifests/`,
|
|
3
|
+
then run `just docs compile` to regenerate. -->
|
|
4
|
+
|
|
5
|
+
## Keep feature-specific mechanisms out of the type_spec type system unless universally applicable
|
|
6
|
+
|
|
7
|
+
Do not extend the type_spec type system with a mechanism scoped to one feature's view of how types
|
|
8
|
+
serialize or behave — for example, custom key conversions added in preparation for LLM/JSON-schema
|
|
9
|
+
tool responses — unless it has been proven universally applicable across type_spec's consumers.
|
|
10
|
+
type_spec is a generic type-definition system shared by the backend, frontend, and SDK, so tying it
|
|
11
|
+
to one view of a feature (such as LLM features) couples the generic types too closely to that single
|
|
12
|
+
consumer. Keep a capability that only one feature needs at that feature's own layer, and promote it
|
|
13
|
+
into the shared type_spec builder/emitter only once multiple consumers demonstrably need it.
|
pkgs/type_spec/CLAUDE.md
ADDED
|
@@ -175,6 +175,27 @@ def emit_constant_ts(ctx: EmitTypescriptContext, sconst: builder.SpecConstant) -
|
|
|
175
175
|
ctx.out.write(f"export const {const_name} = {value}\n")
|
|
176
176
|
|
|
177
177
|
|
|
178
|
+
def _collect_object_properties(
|
|
179
|
+
stype: builder.SpecTypeDefnObject,
|
|
180
|
+
) -> dict[str, builder.SpecProperty]:
|
|
181
|
+
base_properties = (
|
|
182
|
+
_collect_object_properties(stype.base) if stype.base is not None else {}
|
|
183
|
+
)
|
|
184
|
+
return {**base_properties, **(stype.properties or {})}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _type_info_ref_type_ts(*, stype: builder.SpecTypeDefnObject, base_ref: str) -> str:
|
|
188
|
+
defaulted_names = sorted(
|
|
189
|
+
ts_property_name(prop)
|
|
190
|
+
for prop in _collect_object_properties(stype).values()
|
|
191
|
+
if prop.has_default and not prop.parse_require and prop.default is not None
|
|
192
|
+
)
|
|
193
|
+
if len(defaulted_names) == 0:
|
|
194
|
+
return f"{base_ref}.TypeInfoRef<{stype.name}>"
|
|
195
|
+
defaulted_keys = " | ".join(f'"{name}"' for name in defaulted_names)
|
|
196
|
+
return f"{base_ref}.TypeInfoRef<{stype.name}, {base_ref}.WithDefaultsApplied<{stype.name}, {defaulted_keys}>>"
|
|
197
|
+
|
|
198
|
+
|
|
178
199
|
def emit_type_info_ref_ts(
|
|
179
200
|
ctx: EmitTypescriptContext,
|
|
180
201
|
stype: builder.SpecTypeDefn,
|
|
@@ -184,9 +205,9 @@ def emit_type_info_ref_ts(
|
|
|
184
205
|
assert ctx.namespace.emit_type_info, (
|
|
185
206
|
f"{ctx.namespace.name}: $emit_type_info_refs requires $emit_type_info"
|
|
186
207
|
)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
), (
|
|
208
|
+
if not isinstance(stype, builder.SpecTypeDefnObject):
|
|
209
|
+
return
|
|
210
|
+
assert len(stype.parameters) == 0, (
|
|
190
211
|
f"{ctx.namespace.name}.{stype.name}: type info refs support non-generic objects only"
|
|
191
212
|
)
|
|
192
213
|
|
|
@@ -195,7 +216,7 @@ def emit_type_info_ref_ts(
|
|
|
195
216
|
ctx.out.write("\n")
|
|
196
217
|
ctx.out.write(MODIFY_NOTICE)
|
|
197
218
|
ctx.out.write(
|
|
198
|
-
f"export const {stype.name}TypeInfoRef: {base_ref}
|
|
219
|
+
f"export const {stype.name}TypeInfoRef: {_type_info_ref_type_ts(stype=stype, base_ref=base_ref)} = {{\n"
|
|
199
220
|
)
|
|
200
221
|
ctx.out.write(f'{INDENT}typeName: "{ctx.namespace.name}.{stype.name}",\n')
|
|
201
222
|
ctx.out.write("}\n")
|
|
@@ -40,11 +40,16 @@ declare const brand: unique symbol
|
|
|
40
40
|
export type Branded<TBase, TBrand extends string> = TBase & { readonly [brand]: TBrand }
|
|
41
41
|
|
|
42
42
|
declare const typeInfoValue: unique symbol
|
|
43
|
-
|
|
43
|
+
declare const typeInfoParsedValue: unique symbol
|
|
44
|
+
export interface TypeInfoRef<T, TParsed extends T = T> {
|
|
44
45
|
readonly typeName: string
|
|
45
46
|
readonly [typeInfoValue]?: T
|
|
47
|
+
readonly [typeInfoParsedValue]?: TParsed
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
export type WithDefaultsApplied<T, TDefaultedKey extends keyof T> = Omit<T, TDefaultedKey> &
|
|
51
|
+
Required<Pick<T, TDefaultedKey>>
|
|
52
|
+
|
|
48
53
|
// Ids matching a strict integer number are converted to integers
|
|
49
54
|
export const ID_REGEX = /^-?[1-9][0-9]{0,20}$/
|
|
50
55
|
// Matches ISO 8601 dates. Stricter than `Date.parse`.
|
uncountable/core/__init__.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from .async_batch import AsyncBatchProcessor
|
|
2
2
|
from .client import Client
|
|
3
3
|
from .file_upload import MediaFileUpload, UploadedFile
|
|
4
|
-
from .types import AuthDetailsApiKey, AuthDetailsOAuth
|
|
4
|
+
from .types import AuthDetailsApiKey, AuthDetailsBearerToken, AuthDetailsOAuth
|
|
5
5
|
|
|
6
6
|
__all__: list[str] = [
|
|
7
7
|
"AuthDetailsApiKey",
|
|
8
|
+
"AuthDetailsBearerToken",
|
|
8
9
|
"AuthDetailsOAuth",
|
|
9
10
|
"AsyncBatchProcessor",
|
|
10
11
|
"Client",
|
uncountable/core/client.py
CHANGED
|
@@ -31,7 +31,12 @@ from uncountable.types.request_headers_t import (
|
|
|
31
31
|
)
|
|
32
32
|
|
|
33
33
|
from .file_upload import FileUpload, FileUploader, UploadedFile
|
|
34
|
-
from .types import
|
|
34
|
+
from .types import (
|
|
35
|
+
AuthDetailsAll,
|
|
36
|
+
AuthDetailsApiKey,
|
|
37
|
+
AuthDetailsBearerToken,
|
|
38
|
+
AuthDetailsOAuth,
|
|
39
|
+
)
|
|
35
40
|
|
|
36
41
|
DT = typing.TypeVar("DT")
|
|
37
42
|
|
|
@@ -351,6 +356,8 @@ class Client(ClientMethods):
|
|
|
351
356
|
case AuthDetailsOAuth():
|
|
352
357
|
token = self._get_oauth_bearer_token(oauth_details=self._auth_details)
|
|
353
358
|
return {"Authorization": f"Bearer {token}"}
|
|
359
|
+
case AuthDetailsBearerToken():
|
|
360
|
+
return {"Authorization": f"Bearer {self._auth_details.token}"}
|
|
354
361
|
typing.assert_never(self._auth_details)
|
|
355
362
|
|
|
356
363
|
def _build_http_request(
|
uncountable/core/types.py
CHANGED
|
@@ -13,5 +13,10 @@ class AuthDetailsOAuth:
|
|
|
13
13
|
scope: str = "unc.rnd"
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
@dataclass(kw_only=True)
|
|
17
|
+
class AuthDetailsBearerToken:
|
|
18
|
+
token: str
|
|
19
|
+
|
|
20
|
+
|
|
16
21
|
AuthDetails = AuthDetailsApiKey # Legacy Mapping
|
|
17
|
-
AuthDetailsAll = AuthDetailsApiKey | AuthDetailsOAuth
|
|
22
|
+
AuthDetailsAll = AuthDetailsApiKey | AuthDetailsOAuth | AuthDetailsBearerToken
|
uncountable/integration/cli.py
CHANGED
|
@@ -89,6 +89,8 @@ def register_cancel_queued_job_parser(
|
|
|
89
89
|
print(
|
|
90
90
|
"Job successfully cancelled. The integration server will restart."
|
|
91
91
|
)
|
|
92
|
+
case CommandCancelJobStatus.CANCELLED_QUEUED:
|
|
93
|
+
print("Queued job successfully cancelled.")
|
|
92
94
|
case CommandCancelJobStatus.NO_JOB_FOUND:
|
|
93
95
|
print("Job not found.")
|
|
94
96
|
case CommandCancelJobStatus.JOB_ALREADY_COMPLETED:
|
uncountable/integration/job.py
CHANGED
|
@@ -10,7 +10,6 @@ from pkgs.argument_parser import CachedParser
|
|
|
10
10
|
from pkgs.serialization_util import serialize_for_api
|
|
11
11
|
from uncountable.core.async_batch import AsyncBatchProcessor
|
|
12
12
|
from uncountable.core.client import Client
|
|
13
|
-
from uncountable.core.environment import get_local_admin_server_port
|
|
14
13
|
from uncountable.core.file_upload import FileUpload
|
|
15
14
|
from uncountable.core.types import AuthDetailsOAuth
|
|
16
15
|
from uncountable.integration.http_server import (
|
|
@@ -18,12 +17,7 @@ from uncountable.integration.http_server import (
|
|
|
18
17
|
GenericHttpResponse,
|
|
19
18
|
HttpException,
|
|
20
19
|
)
|
|
21
|
-
from uncountable.integration.
|
|
22
|
-
send_job_queue_message,
|
|
23
|
-
)
|
|
24
|
-
from uncountable.integration.queue_runner.command_server.types import (
|
|
25
|
-
CommandServerException,
|
|
26
|
-
)
|
|
20
|
+
from uncountable.integration.job_queue import JobQueue
|
|
27
21
|
from uncountable.integration.request_context import extract_request_context
|
|
28
22
|
from uncountable.integration.secret_retrieval import retrieve_secret
|
|
29
23
|
from uncountable.integration.telemetry import JobLogger
|
|
@@ -111,11 +105,7 @@ class _RequestValidatorClient(Client):
|
|
|
111
105
|
return self._oauth_bearer_token
|
|
112
106
|
|
|
113
107
|
|
|
114
|
-
class
|
|
115
|
-
@property
|
|
116
|
-
def payload_type(self) -> type[GenericHttpRequest]:
|
|
117
|
-
return GenericHttpRequest
|
|
118
|
-
|
|
108
|
+
class HttpJob(ABC):
|
|
119
109
|
@staticmethod
|
|
120
110
|
@abstractmethod
|
|
121
111
|
def validate_request(
|
|
@@ -130,6 +120,22 @@ class CustomHttpJob(Job[GenericHttpRequest]):
|
|
|
130
120
|
"""
|
|
131
121
|
...
|
|
132
122
|
|
|
123
|
+
@abstractmethod
|
|
124
|
+
def respond_to_request(
|
|
125
|
+
self,
|
|
126
|
+
*,
|
|
127
|
+
request: GenericHttpRequest,
|
|
128
|
+
job_definition: HttpJobDefinitionBase,
|
|
129
|
+
profile_meta: ProfileMetadata,
|
|
130
|
+
job_queue: JobQueue,
|
|
131
|
+
) -> GenericHttpResponse: ...
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class CustomHttpJob(Job[GenericHttpRequest], HttpJob):
|
|
135
|
+
@property
|
|
136
|
+
def payload_type(self) -> type[GenericHttpRequest]:
|
|
137
|
+
return GenericHttpRequest
|
|
138
|
+
|
|
133
139
|
@staticmethod
|
|
134
140
|
def get_validated_oauth_request_user_id(
|
|
135
141
|
*, profile_metadata: ProfileMetadata, request: GenericHttpRequest
|
|
@@ -162,6 +168,19 @@ class CustomHttpJob(Job[GenericHttpRequest]):
|
|
|
162
168
|
"""
|
|
163
169
|
...
|
|
164
170
|
|
|
171
|
+
@typing.override
|
|
172
|
+
def respond_to_request(
|
|
173
|
+
self,
|
|
174
|
+
*,
|
|
175
|
+
request: GenericHttpRequest,
|
|
176
|
+
job_definition: HttpJobDefinitionBase,
|
|
177
|
+
profile_meta: ProfileMetadata,
|
|
178
|
+
job_queue: JobQueue,
|
|
179
|
+
) -> GenericHttpResponse:
|
|
180
|
+
return self.handle_request(
|
|
181
|
+
request=request, job_definition=job_definition, profile_meta=profile_meta
|
|
182
|
+
)
|
|
183
|
+
|
|
165
184
|
def run_outer(self, args: JobArguments) -> JobResult:
|
|
166
185
|
args.logger.log_warning(
|
|
167
186
|
message=f"Unexpected call to run_outer for CustomHttpJob: {args.job_definition.id}"
|
|
@@ -189,7 +208,7 @@ def _resolve_webhook_signature_key(
|
|
|
189
208
|
)
|
|
190
209
|
|
|
191
210
|
|
|
192
|
-
class WebhookJob[WPT](Job[webhook_job_t.WebhookEventPayload]):
|
|
211
|
+
class WebhookJob[WPT](Job[webhook_job_t.WebhookEventPayload], HttpJob):
|
|
193
212
|
@property
|
|
194
213
|
def payload_type(self) -> type[webhook_job_t.WebhookEventPayload]:
|
|
195
214
|
return webhook_job_t.WebhookEventPayload
|
|
@@ -219,12 +238,14 @@ class WebhookJob[WPT](Job[webhook_job_t.WebhookEventPayload]):
|
|
|
219
238
|
if not hmac.compare_digest(request_body_signature, passed_signature):
|
|
220
239
|
raise HttpException.payload_failed_signature()
|
|
221
240
|
|
|
222
|
-
@
|
|
223
|
-
def
|
|
241
|
+
@typing.override
|
|
242
|
+
def respond_to_request(
|
|
243
|
+
self,
|
|
224
244
|
*,
|
|
225
245
|
request: GenericHttpRequest,
|
|
226
|
-
job_definition:
|
|
227
|
-
profile_meta: ProfileMetadata,
|
|
246
|
+
job_definition: HttpJobDefinitionBase,
|
|
247
|
+
profile_meta: ProfileMetadata,
|
|
248
|
+
job_queue: JobQueue,
|
|
228
249
|
) -> GenericHttpResponse:
|
|
229
250
|
try:
|
|
230
251
|
request_body = simplejson.loads(request.body_text)
|
|
@@ -234,19 +255,15 @@ class WebhookJob[WPT](Job[webhook_job_t.WebhookEventPayload]):
|
|
|
234
255
|
|
|
235
256
|
request_context = extract_request_context(headers=request.headers)
|
|
236
257
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
port=get_local_admin_server_port(),
|
|
247
|
-
)
|
|
248
|
-
except CommandServerException as e:
|
|
249
|
-
raise HttpException.unknown_error() from e
|
|
258
|
+
job_queue.enqueue(
|
|
259
|
+
job_ref_name=job_definition.id,
|
|
260
|
+
payload=queued_job_t.QueuedJobPayload(
|
|
261
|
+
invocation_context=queued_job_t.InvocationContextWebhook(
|
|
262
|
+
webhook_payload=webhook_payload,
|
|
263
|
+
request_context=request_context,
|
|
264
|
+
)
|
|
265
|
+
),
|
|
266
|
+
)
|
|
250
267
|
|
|
251
268
|
return GenericHttpResponse(
|
|
252
269
|
response=simplejson.dumps(serialize_for_api(WebhookResponse())),
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from uncountable.integration.queue_runner.datastore import Datastore
|
|
2
|
+
from uncountable.types import queued_job_t
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class JobQueue:
|
|
6
|
+
def __init__(self, datastore: Datastore) -> None:
|
|
7
|
+
self._datastore = datastore
|
|
8
|
+
|
|
9
|
+
def enqueue(
|
|
10
|
+
self, *, job_ref_name: str, payload: queued_job_t.QueuedJobPayload
|
|
11
|
+
) -> str:
|
|
12
|
+
return self._datastore.add_job_to_queue(
|
|
13
|
+
job_payload=payload, job_ref_name=job_ref_name
|
|
14
|
+
).queued_job_uuid
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from collections.abc import Callable, Generator
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
|
|
6
|
+
from opentelemetry.trace import get_current_span
|
|
7
|
+
from sqlalchemy import Connection, text
|
|
8
|
+
from sqlalchemy.engine import Engine
|
|
9
|
+
|
|
10
|
+
from uncountable.integration.telemetry import Logger
|
|
11
|
+
|
|
12
|
+
OWNERSHIP_ADVISORY_LOCK_KEY = 0x756E635F696E7467
|
|
13
|
+
OWNERSHIP_LOCK_POLL_INTERVAL_SECS = 1
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OwnershipOutcome(StrEnum):
|
|
17
|
+
ACQUIRED = "acquired"
|
|
18
|
+
SHUTDOWN_REQUESTED = "shutdown_requested"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _try_acquire_advisory_lock(connection: Connection) -> bool:
|
|
22
|
+
return bool(
|
|
23
|
+
connection.execute(
|
|
24
|
+
text("select pg_try_advisory_lock(:key)"),
|
|
25
|
+
{"key": OWNERSHIP_ADVISORY_LOCK_KEY},
|
|
26
|
+
).scalar_one()
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _release_advisory_lock(connection: Connection) -> None:
|
|
31
|
+
connection.execute(
|
|
32
|
+
text("select pg_advisory_unlock(:key)"),
|
|
33
|
+
{"key": OWNERSHIP_ADVISORY_LOCK_KEY},
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _wait_for_advisory_lock(
|
|
38
|
+
*, connection: Connection, is_shutdown_requested: Callable[[], bool]
|
|
39
|
+
) -> OwnershipOutcome:
|
|
40
|
+
logger = Logger(get_current_span())
|
|
41
|
+
if is_shutdown_requested():
|
|
42
|
+
return OwnershipOutcome.SHUTDOWN_REQUESTED
|
|
43
|
+
if _try_acquire_advisory_lock(connection):
|
|
44
|
+
return OwnershipOutcome.ACQUIRED
|
|
45
|
+
logger.log_info("waiting for the integration ownership lock held by another pod")
|
|
46
|
+
while not is_shutdown_requested():
|
|
47
|
+
time.sleep(OWNERSHIP_LOCK_POLL_INTERVAL_SECS)
|
|
48
|
+
if is_shutdown_requested():
|
|
49
|
+
return OwnershipOutcome.SHUTDOWN_REQUESTED
|
|
50
|
+
if _try_acquire_advisory_lock(connection):
|
|
51
|
+
logger.log_info("acquired the integration ownership lock")
|
|
52
|
+
return OwnershipOutcome.ACQUIRED
|
|
53
|
+
return OwnershipOutcome.SHUTDOWN_REQUESTED
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@contextmanager
|
|
57
|
+
def hold_integration_ownership(
|
|
58
|
+
*, engine: Engine, is_shutdown_requested: Callable[[], bool]
|
|
59
|
+
) -> Generator[OwnershipOutcome]:
|
|
60
|
+
dialect_name = engine.dialect.name
|
|
61
|
+
match dialect_name:
|
|
62
|
+
case "sqlite":
|
|
63
|
+
yield OwnershipOutcome.ACQUIRED
|
|
64
|
+
case "postgresql":
|
|
65
|
+
with engine.connect().execution_options(
|
|
66
|
+
isolation_level="AUTOCOMMIT"
|
|
67
|
+
) as connection:
|
|
68
|
+
outcome = _wait_for_advisory_lock(
|
|
69
|
+
connection=connection, is_shutdown_requested=is_shutdown_requested
|
|
70
|
+
)
|
|
71
|
+
try:
|
|
72
|
+
yield outcome
|
|
73
|
+
finally:
|
|
74
|
+
if outcome == OwnershipOutcome.ACQUIRED:
|
|
75
|
+
_release_advisory_lock(connection)
|
|
76
|
+
case _:
|
|
77
|
+
raise ValueError(f"unsupported database dialect: {dialect_name!r}")
|
|
@@ -85,6 +85,8 @@ def send_job_cancellation_message(
|
|
|
85
85
|
return CommandCancelJobStatus.NO_JOB_FOUND
|
|
86
86
|
case CancelJobStatus.CANCELLED_WITH_RESTART:
|
|
87
87
|
return CommandCancelJobStatus.CANCELLED_WITH_RESTART
|
|
88
|
+
case CancelJobStatus.CANCELLED_QUEUED:
|
|
89
|
+
return CommandCancelJobStatus.CANCELLED_QUEUED
|
|
88
90
|
case CancelJobStatus.JOB_ALREADY_COMPLETED:
|
|
89
91
|
return CommandCancelJobStatus.JOB_ALREADY_COMPLETED
|
|
90
92
|
case _:
|
|
@@ -94,6 +94,8 @@ async def serve(command_queue: CommandQueue, datastore: Datastore) -> None:
|
|
|
94
94
|
proto_status = CancelJobStatus.NO_JOB_FOUND
|
|
95
95
|
case CommandCancelJobStatus.CANCELLED_WITH_RESTART:
|
|
96
96
|
proto_status = CancelJobStatus.CANCELLED_WITH_RESTART
|
|
97
|
+
case CommandCancelJobStatus.CANCELLED_QUEUED:
|
|
98
|
+
proto_status = CancelJobStatus.CANCELLED_QUEUED
|
|
97
99
|
case CommandCancelJobStatus.JOB_ALREADY_COMPLETED:
|
|
98
100
|
proto_status = CancelJobStatus.JOB_ALREADY_COMPLETED
|
|
99
101
|
case _:
|
|
@@ -29,7 +29,7 @@ from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(
|
|
32
|
-
b'\nQuncountable/integration/queue_runner/command_server/protocol/command_server.proto\x1a\x1fgoogle/protobuf/timestamp.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"\x1f\n\x0fRetryJobRequest\x12\x0c\n\x04uuid\x18\x01 \x01(\t"F\n\x0eRetryJobResult\x12\x1b\n\x13successfully_queued\x18\x01 \x01(\x08\x12\x17\n\x0fqueued_job_uuid\x18\x02 \x01(\t"\x1a\n\x18VaccuumQueuedJobsRequest"\x19\n\x17VaccuumQueuedJobsResult"\x14\n\x12\x43heckHealthRequest"$\n\x11\x43heckHealthResult\x12\x0f\n\x07success\x18\x01 \x01(\x08"\xc2\x01\n\x15ListQueuedJobsRequest\x12\x0e\n\x06offset\x18\x01 \x01(\r\x12\r\n\x05limit\x18\x02 \x01(\r\x12\x0e\n\x06status\x18\x03 \x01(\t\x12\x14\n\x0cjob_ref_name\x18\x04 \x01(\t\x12\x32\n\x0esubmitted_from\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0csubmitted_to\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xf4\x01\n\x14ListQueuedJobsResult\x12\x43\n\x0bqueued_jobs\x18\x01 \x03(\x0b\x32..ListQueuedJobsResult.ListQueuedJobsResultItem\x1a\x96\x01\n\x18ListQueuedJobsResultItem\x12\x0c\n\x04uuid\x18\x01 \x01(\t\x12\x14\n\x0cjob_ref_name\x18\x02 \x01(\t\x12\x14\n\x0cnum_attempts\x18\x03 \x01(\x03\x12\x30\n\x0csubmitted_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06status\x18\x05 \x01(\t"$\n\x10\x43\x61ncelJobRequest\x12\x10\n\x08job_uuid\x18\x01 \x01(\t"3\n\x0f\x43\x61ncelJobResult\x12 \n\x06status\x18\x01 \x01(\x0e\x32\x10.CancelJobStatus")\n\x17ProvisionWebhookRequest\x12\x0e\n\x06job_id\x18\x01 \x01(\t".\n\x16ProvisionWebhookResult\x12\x14\n\x0cprofile_name\x18\x01 \x01(\t*
|
|
32
|
+
b'\nQuncountable/integration/queue_runner/command_server/protocol/command_server.proto\x1a\x1fgoogle/protobuf/timestamp.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"\x1f\n\x0fRetryJobRequest\x12\x0c\n\x04uuid\x18\x01 \x01(\t"F\n\x0eRetryJobResult\x12\x1b\n\x13successfully_queued\x18\x01 \x01(\x08\x12\x17\n\x0fqueued_job_uuid\x18\x02 \x01(\t"\x1a\n\x18VaccuumQueuedJobsRequest"\x19\n\x17VaccuumQueuedJobsResult"\x14\n\x12\x43heckHealthRequest"$\n\x11\x43heckHealthResult\x12\x0f\n\x07success\x18\x01 \x01(\x08"\xc2\x01\n\x15ListQueuedJobsRequest\x12\x0e\n\x06offset\x18\x01 \x01(\r\x12\r\n\x05limit\x18\x02 \x01(\r\x12\x0e\n\x06status\x18\x03 \x01(\t\x12\x14\n\x0cjob_ref_name\x18\x04 \x01(\t\x12\x32\n\x0esubmitted_from\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x0csubmitted_to\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp"\xf4\x01\n\x14ListQueuedJobsResult\x12\x43\n\x0bqueued_jobs\x18\x01 \x03(\x0b\x32..ListQueuedJobsResult.ListQueuedJobsResultItem\x1a\x96\x01\n\x18ListQueuedJobsResultItem\x12\x0c\n\x04uuid\x18\x01 \x01(\t\x12\x14\n\x0cjob_ref_name\x18\x02 \x01(\t\x12\x14\n\x0cnum_attempts\x18\x03 \x01(\x03\x12\x30\n\x0csubmitted_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0e\n\x06status\x18\x05 \x01(\t"$\n\x10\x43\x61ncelJobRequest\x12\x10\n\x08job_uuid\x18\x01 \x01(\t"3\n\x0f\x43\x61ncelJobResult\x12 \n\x06status\x18\x01 \x01(\x0e\x32\x10.CancelJobStatus")\n\x17ProvisionWebhookRequest\x12\x0e\n\x06job_id\x18\x01 \x01(\t".\n\x16ProvisionWebhookResult\x12\x14\n\x0cprofile_name\x18\x01 \x01(\t*p\n\x0f\x43\x61ncelJobStatus\x12\x1a\n\x16\x43\x41NCELLED_WITH_RESTART\x10\x00\x12\x10\n\x0cNO_JOB_FOUND\x10\x01\x12\x19\n\x15JOB_ALREADY_COMPLETED\x10\x02\x12\x14\n\x10\x43\x41NCELLED_QUEUED\x10\x03\x32\xbd\x03\n\rCommandServer\x12\x35\n\nEnqueueJob\x12\x12.EnqueueJobRequest\x1a\x11.EnqueueJobResult"\x00\x12/\n\x08RetryJob\x12\x10.RetryJobRequest\x1a\x0f.RetryJobResult"\x00\x12\x38\n\x0b\x43heckHealth\x12\x13.CheckHealthRequest\x1a\x12.CheckHealthResult"\x00\x12\x41\n\x0eListQueuedJobs\x12\x16.ListQueuedJobsRequest\x1a\x15.ListQueuedJobsResult"\x00\x12J\n\x11VaccuumQueuedJobs\x12\x19.VaccuumQueuedJobsRequest\x1a\x18.VaccuumQueuedJobsResult"\x00\x12\x32\n\tCancelJob\x12\x11.CancelJobRequest\x1a\x10.CancelJobResult"\x00\x12G\n\x10ProvisionWebhook\x12\x18.ProvisionWebhookRequest\x1a\x17.ProvisionWebhookResult"\x00\x62\x06proto3'
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
_globals = globals()
|
|
@@ -42,7 +42,7 @@ _builder.BuildTopDescriptorsAndMessages(
|
|
|
42
42
|
if not _descriptor._USE_C_DESCRIPTORS:
|
|
43
43
|
DESCRIPTOR._loaded_options = None
|
|
44
44
|
_globals["_CANCELJOBSTATUS"]._serialized_start = 1109
|
|
45
|
-
_globals["_CANCELJOBSTATUS"]._serialized_end =
|
|
45
|
+
_globals["_CANCELJOBSTATUS"]._serialized_end = 1221
|
|
46
46
|
_globals["_ENQUEUEJOBREQUEST"]._serialized_start = 118
|
|
47
47
|
_globals["_ENQUEUEJOBREQUEST"]._serialized_end = 187
|
|
48
48
|
_globals["_ENQUEUEJOBRESULT"]._serialized_start = 189
|
|
@@ -73,6 +73,6 @@ if not _descriptor._USE_C_DESCRIPTORS:
|
|
|
73
73
|
_globals["_PROVISIONWEBHOOKREQUEST"]._serialized_end = 1059
|
|
74
74
|
_globals["_PROVISIONWEBHOOKRESULT"]._serialized_start = 1061
|
|
75
75
|
_globals["_PROVISIONWEBHOOKRESULT"]._serialized_end = 1107
|
|
76
|
-
_globals["_COMMANDSERVER"]._serialized_start =
|
|
77
|
-
_globals["_COMMANDSERVER"]._serialized_end =
|
|
76
|
+
_globals["_COMMANDSERVER"]._serialized_start = 1224
|
|
77
|
+
_globals["_COMMANDSERVER"]._serialized_end = 1669
|
|
78
78
|
# @@protoc_insertion_point(module_scope)
|
|
@@ -16,10 +16,12 @@ class CancelJobStatus(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
|
|
16
16
|
CANCELLED_WITH_RESTART: _ClassVar[CancelJobStatus]
|
|
17
17
|
NO_JOB_FOUND: _ClassVar[CancelJobStatus]
|
|
18
18
|
JOB_ALREADY_COMPLETED: _ClassVar[CancelJobStatus]
|
|
19
|
+
CANCELLED_QUEUED: _ClassVar[CancelJobStatus]
|
|
19
20
|
|
|
20
21
|
CANCELLED_WITH_RESTART: CancelJobStatus
|
|
21
22
|
NO_JOB_FOUND: CancelJobStatus
|
|
22
23
|
JOB_ALREADY_COMPLETED: CancelJobStatus
|
|
24
|
+
CANCELLED_QUEUED: CancelJobStatus
|
|
23
25
|
|
|
24
26
|
class EnqueueJobRequest(_message.Message):
|
|
25
27
|
__slots__ = ("job_ref_name", "serialized_payload")
|
|
@@ -8,6 +8,7 @@ from uncountable.types import queued_job_t
|
|
|
8
8
|
|
|
9
9
|
class CommandCancelJobStatus(StrEnum):
|
|
10
10
|
CANCELLED_WITH_RESTART = "cancelled_with_restart"
|
|
11
|
+
CANCELLED_QUEUED = "cancelled_queued"
|
|
11
12
|
NO_JOB_FOUND = "no_job_found"
|
|
12
13
|
JOB_ALREADY_COMPLETED = "job_already_completed"
|
|
13
14
|
|
|
@@ -204,7 +204,19 @@ class DatastoreSqlBase(Datastore):
|
|
|
204
204
|
|
|
205
205
|
return None
|
|
206
206
|
|
|
207
|
-
def
|
|
207
|
+
def list_queued_job_uuids(self) -> list[str]:
|
|
208
|
+
with self.session_maker() as session:
|
|
209
|
+
select_stmt = select(QueuedJob.id).filter(
|
|
210
|
+
or_(
|
|
211
|
+
QueuedJob.status == queued_job_t.JobStatus.QUEUED,
|
|
212
|
+
QueuedJob.status.is_(None),
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
return [row.id for row in session.execute(select_stmt)]
|
|
216
|
+
|
|
217
|
+
def load_queued_jobs(
|
|
218
|
+
self, *, queued_job_uuids: list[str]
|
|
219
|
+
) -> list[queued_job_t.QueuedJob]:
|
|
208
220
|
with self.session_maker() as session:
|
|
209
221
|
select_stmt = (
|
|
210
222
|
select(
|
|
@@ -215,12 +227,7 @@ class DatastoreSqlBase(Datastore):
|
|
|
215
227
|
QueuedJob.status,
|
|
216
228
|
QueuedJob.submitted_at,
|
|
217
229
|
)
|
|
218
|
-
.filter(
|
|
219
|
-
or_(
|
|
220
|
-
QueuedJob.status == queued_job_t.JobStatus.QUEUED,
|
|
221
|
-
QueuedJob.status.is_(None),
|
|
222
|
-
)
|
|
223
|
-
)
|
|
230
|
+
.filter(QueuedJob.id.in_(queued_job_uuids))
|
|
224
231
|
.order_by(QueuedJob.submitted_at)
|
|
225
232
|
)
|
|
226
233
|
|
|
@@ -31,7 +31,12 @@ class Datastore(ABC):
|
|
|
31
31
|
def increment_num_attempts(self, queued_job_uuid: str) -> int: ...
|
|
32
32
|
|
|
33
33
|
@abstractmethod
|
|
34
|
-
def
|
|
34
|
+
def list_queued_job_uuids(self) -> list[str]: ...
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def load_queued_jobs(
|
|
38
|
+
self, *, queued_job_uuids: list[str]
|
|
39
|
+
) -> list[queued_job_t.QueuedJob]: ...
|
|
35
40
|
|
|
36
41
|
@abstractmethod
|
|
37
42
|
def get_next_queued_job_for_ref_name(
|