UncountablePythonSDK 0.0.190__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/emit_typescript_util.py +25 -4
- pkgs/type_spec/parts/base.ts.prepart +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 +86 -57
- uncountable/integration/webhook_server/entrypoint.py +17 -5
- uncountable/types/entity_t.py +2 -0
- {uncountablepythonsdk-0.0.190.dist-info → uncountablepythonsdk-0.0.191.dist-info}/METADATA +1 -1
- {uncountablepythonsdk-0.0.190.dist-info → uncountablepythonsdk-0.0.191.dist-info}/RECORD +23 -21
- {uncountablepythonsdk-0.0.190.dist-info → uncountablepythonsdk-0.0.191.dist-info}/WHEEL +0 -0
- {uncountablepythonsdk-0.0.190.dist-info → uncountablepythonsdk-0.0.191.dist-info}/top_level.txt +0 -0
|
@@ -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/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(
|
|
@@ -31,6 +31,9 @@ from uncountable.types import job_definition_t, queued_job_t
|
|
|
31
31
|
|
|
32
32
|
from .types import RESTART_EXIT_CODE, ResultQueue, ResultTask
|
|
33
33
|
|
|
34
|
+
QUEUED_JOB_POLL_INTERVAL_SECS = 1
|
|
35
|
+
RESTART_DELAY_SECS = 5
|
|
36
|
+
|
|
34
37
|
|
|
35
38
|
@dataclass(kw_only=True, frozen=True)
|
|
36
39
|
class JobListenerKey:
|
|
@@ -65,10 +68,12 @@ def on_worker_crash(
|
|
|
65
68
|
|
|
66
69
|
|
|
67
70
|
def _start_workers(
|
|
71
|
+
*,
|
|
68
72
|
process_pool: ProcessPoolExecutor,
|
|
69
73
|
result_queue: ResultQueue,
|
|
70
74
|
datastore: Datastore,
|
|
71
75
|
worker_tasks: list[asyncio.Task],
|
|
76
|
+
running_job_uuids: set[str],
|
|
72
77
|
) -> dict[str, Worker]:
|
|
73
78
|
profiles = load_profiles()
|
|
74
79
|
job_queue_worker_lookup: dict[JobListenerKey, Worker] = {}
|
|
@@ -84,6 +89,7 @@ def _start_workers(
|
|
|
84
89
|
listen_queue=asyncio.Queue(),
|
|
85
90
|
result_queue=result_queue,
|
|
86
91
|
datastore=datastore,
|
|
92
|
+
running_job_uuids=running_job_uuids,
|
|
87
93
|
)
|
|
88
94
|
task = asyncio.create_task(worker.run_worker_loop())
|
|
89
95
|
task.add_done_callback(on_worker_crash(job_worker_key))
|
|
@@ -108,11 +114,16 @@ async def start_scheduler(
|
|
|
108
114
|
with ProcessPoolExecutor(
|
|
109
115
|
max_workers=get_max_job_workers(), max_tasks_per_child=1
|
|
110
116
|
) as process_pool:
|
|
117
|
+
running_job_uuids: set[str] = set()
|
|
111
118
|
job_worker_lookup = _start_workers(
|
|
112
|
-
process_pool
|
|
119
|
+
process_pool=process_pool,
|
|
120
|
+
result_queue=result_queue,
|
|
121
|
+
datastore=datastore,
|
|
122
|
+
worker_tasks=worker_tasks,
|
|
123
|
+
running_job_uuids=running_job_uuids,
|
|
113
124
|
)
|
|
114
125
|
|
|
115
|
-
|
|
126
|
+
dispatched_job_uuids: set[str] = set()
|
|
116
127
|
|
|
117
128
|
async def enqueue_queued_job(queued_job: queued_job_t.QueuedJob) -> None:
|
|
118
129
|
try:
|
|
@@ -123,8 +134,22 @@ async def start_scheduler(
|
|
|
123
134
|
queued_job.queued_job_uuid, queued_job_t.JobStatus.FAILED
|
|
124
135
|
)
|
|
125
136
|
return
|
|
137
|
+
dispatched_job_uuids.add(queued_job.queued_job_uuid)
|
|
126
138
|
await worker.listen_queue.put(queued_job)
|
|
127
139
|
|
|
140
|
+
async def _dispatch_undispatched_queued_jobs() -> None:
|
|
141
|
+
undispatched_job_uuids = [
|
|
142
|
+
queued_job_uuid
|
|
143
|
+
for queued_job_uuid in datastore.list_queued_job_uuids()
|
|
144
|
+
if queued_job_uuid not in dispatched_job_uuids
|
|
145
|
+
]
|
|
146
|
+
if len(undispatched_job_uuids) == 0:
|
|
147
|
+
return
|
|
148
|
+
for queued_job in datastore.load_queued_jobs(
|
|
149
|
+
queued_job_uuids=undispatched_job_uuids
|
|
150
|
+
):
|
|
151
|
+
await enqueue_queued_job(queued_job)
|
|
152
|
+
|
|
128
153
|
async def _enqueue_or_deduplicate_job(
|
|
129
154
|
job_ref_name: str,
|
|
130
155
|
payload: queued_job_t.QueuedJobPayload,
|
|
@@ -172,30 +197,36 @@ async def start_scheduler(
|
|
|
172
197
|
)
|
|
173
198
|
return
|
|
174
199
|
|
|
175
|
-
if queued_job.status
|
|
176
|
-
datastore.remove_job_from_queue(command.queued_job_uuid)
|
|
200
|
+
if queued_job.status != queued_job_t.JobStatus.QUEUED:
|
|
177
201
|
await command.response_queue.put(
|
|
178
202
|
CommandCancelJobResponse(
|
|
179
|
-
status=CommandCancelJobStatus.
|
|
203
|
+
status=CommandCancelJobStatus.JOB_ALREADY_COMPLETED
|
|
180
204
|
)
|
|
181
205
|
)
|
|
206
|
+
return
|
|
182
207
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
threading.Timer(interval=5, function=delayed_exit).start()
|
|
192
|
-
|
|
208
|
+
datastore.remove_job_from_queue(command.queued_job_uuid)
|
|
209
|
+
if command.queued_job_uuid in running_job_uuids:
|
|
210
|
+
cancel_status = CommandCancelJobStatus.CANCELLED_WITH_RESTART
|
|
211
|
+
threading.Timer(
|
|
212
|
+
interval=RESTART_DELAY_SECS,
|
|
213
|
+
function=os._exit,
|
|
214
|
+
args=[RESTART_EXIT_CODE],
|
|
215
|
+
).start()
|
|
193
216
|
else:
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
)
|
|
217
|
+
dispatched_job_uuids.discard(command.queued_job_uuid)
|
|
218
|
+
cancel_status = CommandCancelJobStatus.CANCELLED_QUEUED
|
|
219
|
+
|
|
220
|
+
await command.response_queue.put(
|
|
221
|
+
CommandCancelJobResponse(status=cancel_status)
|
|
222
|
+
)
|
|
223
|
+
logger.log_debug(
|
|
224
|
+
"Job cancelled successfully.",
|
|
225
|
+
attributes={
|
|
226
|
+
"job.queued_job_uuid": command.queued_job_uuid,
|
|
227
|
+
"job.cancel_status": cancel_status,
|
|
228
|
+
},
|
|
229
|
+
)
|
|
199
230
|
|
|
200
231
|
async def _handle_retry_job_command(command: CommandRetryJob) -> None:
|
|
201
232
|
queued_job = datastore.retry_job(command.queued_job_uuid)
|
|
@@ -221,17 +252,21 @@ async def start_scheduler(
|
|
|
221
252
|
attributes={"job.queued_job_uuids": deleted_uuids},
|
|
222
253
|
)
|
|
223
254
|
|
|
224
|
-
|
|
225
|
-
await enqueue_queued_job(queued_job)
|
|
255
|
+
await _dispatch_undispatched_queued_jobs()
|
|
226
256
|
|
|
227
257
|
result_task: ResultTask = asyncio.create_task(result_queue.get())
|
|
228
258
|
command_task: CommandTask = asyncio.create_task(command_queue.get())
|
|
259
|
+
poll_task = asyncio.create_task(asyncio.sleep(QUEUED_JOB_POLL_INTERVAL_SECS))
|
|
229
260
|
shutdown_task: asyncio.Task[object] | None = None
|
|
230
261
|
if shutdown_event is not None:
|
|
231
262
|
shutdown_task = asyncio.create_task(shutdown_event.wait())
|
|
232
263
|
|
|
233
264
|
while True:
|
|
234
|
-
waitables: list[asyncio.Task[object]] = [
|
|
265
|
+
waitables: list[asyncio.Task[object]] = [
|
|
266
|
+
result_task,
|
|
267
|
+
command_task,
|
|
268
|
+
poll_task,
|
|
269
|
+
]
|
|
235
270
|
if shutdown_task is not None:
|
|
236
271
|
waitables.append(shutdown_task)
|
|
237
272
|
|
|
@@ -246,6 +281,7 @@ async def start_scheduler(
|
|
|
246
281
|
worker_task.cancel()
|
|
247
282
|
command_task.cancel()
|
|
248
283
|
result_task.cancel()
|
|
284
|
+
poll_task.cancel()
|
|
249
285
|
process_pool.shutdown(wait=False, cancel_futures=True)
|
|
250
286
|
return
|
|
251
287
|
if task == command_task:
|
|
@@ -275,4 +311,11 @@ async def start_scheduler(
|
|
|
275
311
|
queued_job_result.queued_job_uuid,
|
|
276
312
|
queued_job_t.JobStatus.FAILED,
|
|
277
313
|
)
|
|
314
|
+
dispatched_job_uuids.discard(queued_job_result.queued_job_uuid)
|
|
315
|
+
running_job_uuids.discard(queued_job_result.queued_job_uuid)
|
|
278
316
|
result_task = asyncio.create_task(result_queue.get())
|
|
317
|
+
elif task == poll_task:
|
|
318
|
+
await _dispatch_undispatched_queued_jobs()
|
|
319
|
+
poll_task = asyncio.create_task(
|
|
320
|
+
asyncio.sleep(QUEUED_JOB_POLL_INTERVAL_SECS)
|
|
321
|
+
)
|
|
@@ -21,17 +21,25 @@ class Worker:
|
|
|
21
21
|
listen_queue: ListenQueue,
|
|
22
22
|
result_queue: ResultQueue,
|
|
23
23
|
datastore: Datastore,
|
|
24
|
+
running_job_uuids: set[str],
|
|
24
25
|
) -> None:
|
|
25
26
|
self.process_pool = process_pool
|
|
26
27
|
self.listen_queue = listen_queue
|
|
27
28
|
self.result_queue = result_queue
|
|
28
29
|
self.datastore = datastore
|
|
30
|
+
self.running_job_uuids = running_job_uuids
|
|
29
31
|
|
|
30
32
|
async def run_worker_loop(self) -> None:
|
|
31
33
|
logger = Logger(get_current_span())
|
|
32
34
|
while True:
|
|
33
35
|
try:
|
|
34
36
|
queued_job = await self.listen_queue.get()
|
|
37
|
+
if (
|
|
38
|
+
self.datastore.get_queued_job(uuid=queued_job.queued_job_uuid)
|
|
39
|
+
is None
|
|
40
|
+
):
|
|
41
|
+
continue
|
|
42
|
+
self.running_job_uuids.add(queued_job.queued_job_uuid)
|
|
35
43
|
self.datastore.increment_num_attempts(queued_job.queued_job_uuid)
|
|
36
44
|
loop = asyncio.get_event_loop()
|
|
37
45
|
result = await loop.run_in_executor(
|
|
@@ -12,7 +12,12 @@ from typing import assert_never
|
|
|
12
12
|
from opentelemetry.trace import get_current_span
|
|
13
13
|
|
|
14
14
|
from uncountable.core.environment import get_local_admin_server_port
|
|
15
|
+
from uncountable.integration.db.connect import IntegrationDBService, create_db_engine
|
|
15
16
|
from uncountable.integration.entrypoint import main as cron_target
|
|
17
|
+
from uncountable.integration.ownership_lock import (
|
|
18
|
+
OwnershipOutcome,
|
|
19
|
+
hold_integration_ownership,
|
|
20
|
+
)
|
|
16
21
|
from uncountable.integration.queue_runner.command_server import (
|
|
17
22
|
CommandServerTimeout,
|
|
18
23
|
check_health,
|
|
@@ -259,15 +264,16 @@ def main() -> None:
|
|
|
259
264
|
)
|
|
260
265
|
)
|
|
261
266
|
|
|
262
|
-
|
|
267
|
+
shutdown_requested = False
|
|
263
268
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
269
|
+
def _sigterm_handler(signum: int, frame: object) -> None:
|
|
270
|
+
nonlocal shutdown_requested
|
|
271
|
+
shutdown_requested = True
|
|
272
|
+
|
|
273
|
+
def _is_shutdown_requested() -> bool:
|
|
274
|
+
return shutdown_requested
|
|
275
|
+
|
|
276
|
+
signal.signal(signal.SIGTERM, _sigterm_handler)
|
|
271
277
|
|
|
272
278
|
webhook_server_process = subprocess.Popen([
|
|
273
279
|
sys.executable,
|
|
@@ -284,59 +290,82 @@ def main() -> None:
|
|
|
284
290
|
)
|
|
285
291
|
)
|
|
286
292
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
+
def _run_owned_processes() -> None:
|
|
294
|
+
if _is_shutdown_requested():
|
|
295
|
+
return
|
|
296
|
+
_start_queue_runner()
|
|
297
|
+
if _is_shutdown_requested():
|
|
298
|
+
return
|
|
293
299
|
|
|
294
|
-
|
|
300
|
+
cron_process = multiprocessing.Process(target=cron_target)
|
|
301
|
+
cron_process.start()
|
|
302
|
+
add_process(
|
|
303
|
+
ProcessInfo(
|
|
304
|
+
key=CoreProcessKey(name=ProcessName.CRON_SERVER), process=cron_process
|
|
305
|
+
)
|
|
306
|
+
)
|
|
295
307
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
308
|
+
services_by_id: dict[str, ServiceToStart] = {
|
|
309
|
+
service_to_start.service.id: service_to_start
|
|
310
|
+
for service_to_start in select_enabled_services(load_profiles())
|
|
311
|
+
}
|
|
312
|
+
for service_to_start in services_by_id.values():
|
|
313
|
+
if _is_shutdown_requested():
|
|
314
|
+
return
|
|
315
|
+
_start_service(service_to_start)
|
|
299
316
|
|
|
300
|
-
|
|
317
|
+
try:
|
|
318
|
+
while not _is_shutdown_requested():
|
|
319
|
+
process_alarm = check_process_alarms(logger, processes=processes)
|
|
320
|
+
match process_alarm:
|
|
321
|
+
case ProcessAlarmRestart():
|
|
322
|
+
match process_alarm.process.key:
|
|
323
|
+
case CoreProcessKey() as core_key:
|
|
324
|
+
match core_key.name:
|
|
325
|
+
case ProcessName.QUEUE_RUNNER:
|
|
326
|
+
del processes[
|
|
327
|
+
CoreProcessKey(
|
|
328
|
+
name=ProcessName.QUEUE_RUNNER
|
|
329
|
+
)
|
|
330
|
+
]
|
|
331
|
+
_start_queue_runner()
|
|
332
|
+
case (
|
|
333
|
+
ProcessName.CRON_SERVER
|
|
334
|
+
| ProcessName.WEBHOOK_SERVER
|
|
335
|
+
):
|
|
336
|
+
raise NotImplementedError(
|
|
337
|
+
f"restarting {core_key.label} not yet implemented"
|
|
338
|
+
)
|
|
339
|
+
case _:
|
|
340
|
+
assert_never(core_key.name)
|
|
341
|
+
case ServiceProcessKey() as service_key:
|
|
342
|
+
del processes[service_key]
|
|
343
|
+
_start_service(services_by_id[service_key.service_id])
|
|
344
|
+
case _:
|
|
345
|
+
assert_never(process_alarm.process.key)
|
|
346
|
+
case ProcessAlarmShutdownAll():
|
|
347
|
+
handle_shutdown(logger, processes)
|
|
348
|
+
sys.exit(1)
|
|
349
|
+
case None:
|
|
350
|
+
pass
|
|
351
|
+
case _:
|
|
352
|
+
assert_never(process_alarm)
|
|
353
|
+
time.sleep(1)
|
|
354
|
+
except KeyboardInterrupt:
|
|
355
|
+
pass
|
|
301
356
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
_start_queue_runner()
|
|
315
|
-
case (
|
|
316
|
-
ProcessName.CRON_SERVER | ProcessName.WEBHOOK_SERVER
|
|
317
|
-
):
|
|
318
|
-
raise NotImplementedError(
|
|
319
|
-
f"restarting {core_key.label} not yet implemented"
|
|
320
|
-
)
|
|
321
|
-
case _:
|
|
322
|
-
assert_never(core_key.name)
|
|
323
|
-
case ServiceProcessKey() as service_key:
|
|
324
|
-
del processes[service_key]
|
|
325
|
-
_start_service(services_by_id[service_key.service_id])
|
|
326
|
-
case _:
|
|
327
|
-
assert_never(process_alarm.process.key)
|
|
328
|
-
case ProcessAlarmShutdownAll():
|
|
329
|
-
handle_shutdown(logger, processes)
|
|
330
|
-
sys.exit(1)
|
|
331
|
-
case None:
|
|
332
|
-
pass
|
|
333
|
-
case _:
|
|
334
|
-
assert_never(process_alarm)
|
|
335
|
-
time.sleep(1)
|
|
336
|
-
except KeyboardInterrupt:
|
|
337
|
-
pass
|
|
338
|
-
|
|
339
|
-
handle_shutdown(logger, processes=processes)
|
|
357
|
+
with hold_integration_ownership(
|
|
358
|
+
engine=create_db_engine(IntegrationDBService.RUNNER),
|
|
359
|
+
is_shutdown_requested=_is_shutdown_requested,
|
|
360
|
+
) as ownership_outcome:
|
|
361
|
+
match ownership_outcome:
|
|
362
|
+
case OwnershipOutcome.ACQUIRED:
|
|
363
|
+
_run_owned_processes()
|
|
364
|
+
case OwnershipOutcome.SHUTDOWN_REQUESTED:
|
|
365
|
+
pass
|
|
366
|
+
case _:
|
|
367
|
+
assert_never(ownership_outcome)
|
|
368
|
+
handle_shutdown(logger, processes=processes)
|
|
340
369
|
|
|
341
370
|
|
|
342
371
|
if __name__ == "__main__":
|
|
@@ -8,9 +8,12 @@ from uncountable.core.environment import (
|
|
|
8
8
|
get_http_server_port,
|
|
9
9
|
get_server_env,
|
|
10
10
|
)
|
|
11
|
+
from uncountable.integration.db.connect import IntegrationDBService, create_db_engine
|
|
11
12
|
from uncountable.integration.executors.script_executor import resolve_script_executor
|
|
12
13
|
from uncountable.integration.http_server import GenericHttpRequest, HttpException
|
|
13
|
-
from uncountable.integration.job import
|
|
14
|
+
from uncountable.integration.job import HttpJob
|
|
15
|
+
from uncountable.integration.job_queue import JobQueue
|
|
16
|
+
from uncountable.integration.queue_runner.datastore import create_datastore
|
|
14
17
|
from uncountable.integration.scan_profiles import load_profiles
|
|
15
18
|
from uncountable.integration.telemetry import Logger
|
|
16
19
|
from uncountable.types import job_definition_t
|
|
@@ -42,6 +45,7 @@ def register_route(
|
|
|
42
45
|
server_logger: Logger,
|
|
43
46
|
profile_meta: job_definition_t.ProfileMetadata,
|
|
44
47
|
job: HttpJobDefinition,
|
|
48
|
+
job_queue: JobQueue,
|
|
45
49
|
) -> None:
|
|
46
50
|
route = f"/{profile_meta.name}/{job.id}"
|
|
47
51
|
log_attributes = {
|
|
@@ -69,9 +73,9 @@ def register_route(
|
|
|
69
73
|
job_instance = resolve_script_executor(
|
|
70
74
|
executor=job.executor, profile_metadata=profile_meta
|
|
71
75
|
)
|
|
72
|
-
if not isinstance(job_instance,
|
|
76
|
+
if not isinstance(job_instance, HttpJob):
|
|
73
77
|
raise HttpException.configuration_error(
|
|
74
|
-
message="[internal] http job must descend from
|
|
78
|
+
message="[internal] http job must descend from HttpJob"
|
|
75
79
|
)
|
|
76
80
|
http_request = GenericHttpRequest(
|
|
77
81
|
body_base64=base64.b64encode(flask.request.get_data()).decode(),
|
|
@@ -81,8 +85,11 @@ def register_route(
|
|
|
81
85
|
job_instance.validate_request(
|
|
82
86
|
request=http_request, job_definition=job, profile_meta=profile_meta
|
|
83
87
|
)
|
|
84
|
-
http_response = job_instance.
|
|
85
|
-
request=http_request,
|
|
88
|
+
http_response = job_instance.respond_to_request(
|
|
89
|
+
request=http_request,
|
|
90
|
+
job_definition=job,
|
|
91
|
+
profile_meta=profile_meta,
|
|
92
|
+
job_queue=job_queue,
|
|
86
93
|
)
|
|
87
94
|
|
|
88
95
|
return flask.make_response(
|
|
@@ -121,6 +128,10 @@ def register_route(
|
|
|
121
128
|
def main() -> None:
|
|
122
129
|
app.add_url_rule("/health", "health", lambda: ("OK", 200))
|
|
123
130
|
|
|
131
|
+
job_queue = JobQueue(
|
|
132
|
+
create_datastore(create_db_engine(IntegrationDBService.RUNNER))
|
|
133
|
+
)
|
|
134
|
+
|
|
124
135
|
profiles = load_profiles()
|
|
125
136
|
for profile_metadata in profiles:
|
|
126
137
|
server_logger = Logger(get_current_span())
|
|
@@ -137,6 +148,7 @@ def main() -> None:
|
|
|
137
148
|
server_logger=server_logger,
|
|
138
149
|
profile_meta=profile_metadata,
|
|
139
150
|
job=job,
|
|
151
|
+
job_queue=job_queue,
|
|
140
152
|
)
|
|
141
153
|
case _:
|
|
142
154
|
assert_never(job)
|
uncountable/types/entity_t.py
CHANGED
|
@@ -313,6 +313,7 @@ __all__: list[str] = [
|
|
|
313
313
|
"user_group": "User Group",
|
|
314
314
|
"user_group_assignment": "User Group Assignment",
|
|
315
315
|
"user_list_set": "User List Set",
|
|
316
|
+
"user_preference": "User Preference",
|
|
316
317
|
"value_spec_property": "Value Spec Property",
|
|
317
318
|
"webhook": "Webhook",
|
|
318
319
|
"webhook_event": "Webhook Event",
|
|
@@ -610,6 +611,7 @@ class EntityType(StrEnum):
|
|
|
610
611
|
USER_GROUP = "user_group"
|
|
611
612
|
USER_GROUP_ASSIGNMENT = "user_group_assignment"
|
|
612
613
|
USER_LIST_SET = "user_list_set"
|
|
614
|
+
USER_PREFERENCE = "user_preference"
|
|
613
615
|
VALUE_SPEC_PROPERTY = "value_spec_property"
|
|
614
616
|
WEBHOOK = "webhook"
|
|
615
617
|
WEBHOOK_EVENT = "webhook_event"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: UncountablePythonSDK
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.191
|
|
4
4
|
Summary: Uncountable SDK
|
|
5
5
|
Project-URL: Homepage, https://github.com/uncountableinc/uncountable-python-sdk
|
|
6
6
|
Project-URL: Repository, https://github.com/uncountableinc/uncountable-python-sdk.git
|
|
@@ -104,7 +104,7 @@ pkgs/type_spec/emit_open_api_util.py,sha256=bTmRvrGP82-eB75hwf9ySI7pDEC87FNQTF18
|
|
|
104
104
|
pkgs/type_spec/emit_python.py,sha256=6ZGh9F73Eqb4W8-bHLYkV5k8snEAQH9aXL9_T4onblM,65465
|
|
105
105
|
pkgs/type_spec/emit_scss.py,sha256=6psmBAJyu7cvfQAIvIIMk2U6VFG_xeWy1Ea4afv0XyM,1497
|
|
106
106
|
pkgs/type_spec/emit_typescript.py,sha256=XRMMovJz_g2j0rpn1Nrz4uzHRCs6O6HLIxdEB1QMNIE,11939
|
|
107
|
-
pkgs/type_spec/emit_typescript_util.py,sha256=
|
|
107
|
+
pkgs/type_spec/emit_typescript_util.py,sha256=X3EUGcZ8qJ2vkYZSjeV774fTa19t4VuYXNRKrFU9WDs,17653
|
|
108
108
|
pkgs/type_spec/load_types.py,sha256=k5hWxFvhj8aT_9vAwtkjn9OdudQFK9IIUzZI7-Gpx2Y,5277
|
|
109
109
|
pkgs/type_spec/non_discriminated_union_exceptions.py,sha256=jgQ4Naz8A6FbhgfR3c9SI4IMvkkft0MT6CyQXWu9RB0,559
|
|
110
110
|
pkgs/type_spec/open_api_util.py,sha256=LX6ny4xNOWXQO3qWVM8elrSDTqWsj_7jFplax2POShE,7992
|
|
@@ -115,7 +115,7 @@ pkgs/type_spec/actions_registry/__main__.py,sha256=hWUnmiG68xeW5GhnIolfHOvPY2GI_
|
|
|
115
115
|
pkgs/type_spec/actions_registry/emit_python.py,sha256=atY1_Z9Ue6r1nBsMeoeX02vQCc_UpAnDLK7gdVNemZ4,3753
|
|
116
116
|
pkgs/type_spec/actions_registry/emit_typescript.py,sha256=XqmbX5OrgjdCGQf4-iNc_aZ7JiXgaVGt1wJcpAUh8SA,6787
|
|
117
117
|
pkgs/type_spec/parts/base.py.prepart,sha256=ju50TWzxO7qgLjRwJSGJYnKGiLBT1g_8aC-01-f2fhk,2286
|
|
118
|
-
pkgs/type_spec/parts/base.ts.prepart,sha256=
|
|
118
|
+
pkgs/type_spec/parts/base.ts.prepart,sha256=MOAkZgpF6KDWbIRIQODz8A-uXIcf04x3mNWG_hSfYpg,2203
|
|
119
119
|
pkgs/type_spec/type_info/__main__.py,sha256=TLNvCHGcmaj_8Sj5bAQNpuNaaw2dpDzoFDWZds0V4Qo,1002
|
|
120
120
|
pkgs/type_spec/type_info/emit_type_info.py,sha256=xoEFfK3H4r6w3Sur6-55I2efevdV9nKVHw1VtuCElR8,17990
|
|
121
121
|
pkgs/type_spec/ui_entry_actions/__init__.py,sha256=WiHE_BexOEZWbkkbD7EnFau1aMLNmfgQywG9PTQNCkw,135
|
|
@@ -139,14 +139,16 @@ uncountable/core/query/column.py,sha256=WR5VRX30kGrObMxxWhnaH-ueJkn_P-K73kz0GSfZ
|
|
|
139
139
|
uncountable/core/query/row.py,sha256=dy68YKvMz336nyOsj_g2oNTLKBuUnKRUz5n99sApfT0,1600
|
|
140
140
|
uncountable/core/query/types.py,sha256=xZgc0_O1PIxZoJWQhxkg3qnSvHj3yt-bv9QX1UCOFGw,481
|
|
141
141
|
uncountable/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
142
|
-
uncountable/integration/cli.py,sha256=
|
|
142
|
+
uncountable/integration/cli.py,sha256=0s0cY9FcYHqWwpR7eeffGgsyrECOFacn6jMRJSTbD4o,9419
|
|
143
143
|
uncountable/integration/construct_client.py,sha256=9c8ZqMjyXebsptiGkJbg53_af4TLOumY1hOQtIbC15w,2229
|
|
144
144
|
uncountable/integration/cron.py,sha256=6eH-kIs3sdYPCyb62_L2M7U_uQTdMTdwY5hreEJb0hw,887
|
|
145
145
|
uncountable/integration/entrypoint.py,sha256=seXMXxUPjBLzpP6UAB6fRVodkgGea6vnl2oy4FWvCyE,591
|
|
146
|
-
uncountable/integration/job.py,sha256=
|
|
146
|
+
uncountable/integration/job.py,sha256=gMcPvV_8mS1mtyYCg7fkRPB0Nj4K1VCD2KrTILRwUpE,9460
|
|
147
|
+
uncountable/integration/job_queue.py,sha256=6hnNWvifqtup2e3uG-EWKjcCNF-Xl4KW_432GaXBoAc,461
|
|
148
|
+
uncountable/integration/ownership_lock.py,sha256=UDv9MKWiDRkTMUlznBLSd-hqtQcrbXEBQ5aU7PBA0E8,2638
|
|
147
149
|
uncountable/integration/request_context.py,sha256=N_FJJxqvfUJ0yV9h3I3vFTGNJiDfyLYObczcYa44pw8,999
|
|
148
150
|
uncountable/integration/scan_profiles.py,sha256=Fl8vr2jhW9BmnatLHZSvMnruUUXYgnXVVlbftYetY9g,3647
|
|
149
|
-
uncountable/integration/scheduler.py,sha256=
|
|
151
|
+
uncountable/integration/scheduler.py,sha256=OvWK2VbqDM8LhuthVf90oJpCIRNDPnT4zyyi1TDx6wA,12425
|
|
150
152
|
uncountable/integration/server.py,sha256=wnqpowr3xdAXfi4gCTdfFBvmQVyTro439kqrXewBauM,6002
|
|
151
153
|
uncountable/integration/service.py,sha256=i3GSrNu7QzMfK1jOIKLdJQCM5Tzp9eF5nfWldj6Cx-M,2144
|
|
152
154
|
uncountable/integration/service_runner.py,sha256=u9J90fh4CA9nluvfuTUaF6MhCJc2VKMD4Ahmk2x6Hzc,1435
|
|
@@ -165,32 +167,32 @@ uncountable/integration/http_server/types.py,sha256=0J59HhEyntDjy0o37Qze0v0o-wC8
|
|
|
165
167
|
uncountable/integration/queue_runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
166
168
|
uncountable/integration/queue_runner/job_process.py,sha256=OONJ7aLsE80qIJ2ABLY3HYyKI7VdpBjI_pPQ3GCC13U,2586
|
|
167
169
|
uncountable/integration/queue_runner/job_runner.py,sha256=Yh9hYfEYOya3BR6ypsal3LKPKrrEb4VQwfGuh4qyU2w,6115
|
|
168
|
-
uncountable/integration/queue_runner/job_scheduler.py,sha256=
|
|
170
|
+
uncountable/integration/queue_runner/job_scheduler.py,sha256=1gdm0PRbjZBFlwWM0sLIkhVFlv9K0x5BjqRvbFMsZKA,12599
|
|
169
171
|
uncountable/integration/queue_runner/queue_runner.py,sha256=G3HhYwKqjx863C_Kjg7rWe5BXy9d-0jkzIf9xuKCYGo,1181
|
|
170
172
|
uncountable/integration/queue_runner/types.py,sha256=8HS6KnYMS_vc5XHeMpg0BFAQC-5P3QLzd-aDYDMMt3E,244
|
|
171
|
-
uncountable/integration/queue_runner/worker.py,sha256=
|
|
173
|
+
uncountable/integration/queue_runner/worker.py,sha256=b-tmOLNMaSOA3VQRtOvymBZYyZomq0kSA5LpQlWI_jM,2027
|
|
172
174
|
uncountable/integration/queue_runner/command_server/__init__.py,sha256=hMCDLWct8zW4B2a9BaIAsMhtaEgFlxONjeow-6nf6dg,675
|
|
173
|
-
uncountable/integration/queue_runner/command_server/command_client.py,sha256=
|
|
174
|
-
uncountable/integration/queue_runner/command_server/command_server.py,sha256
|
|
175
|
+
uncountable/integration/queue_runner/command_server/command_client.py,sha256=9WTKWCD6o3h9xSOJP4lK8oyGzlQgXeoOLO8D7fWJZy4,6508
|
|
176
|
+
uncountable/integration/queue_runner/command_server/command_server.py,sha256=ixcamAbYZ1R0C9-kV3aCUD0xKOov38bV7GW0UMyjcIM,8744
|
|
175
177
|
uncountable/integration/queue_runner/command_server/constants.py,sha256=7J9mQIAMOfV50wnwpn7HgrPFEi3Ritj6HwrGYwxGLoU,88
|
|
176
178
|
uncountable/integration/queue_runner/command_server/provision_webhook.py,sha256=TqH_6YjRZz8EHMKxOcCxvmK6vweR8nuY8ilWNEQoogk,1010
|
|
177
|
-
uncountable/integration/queue_runner/command_server/types.py,sha256=
|
|
179
|
+
uncountable/integration/queue_runner/command_server/types.py,sha256=PCEhvCMxLW1GlDjFHAXQmR5R3-0bd9To44XeOW3PLEQ,2119
|
|
178
180
|
uncountable/integration/queue_runner/command_server/protocol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
179
|
-
uncountable/integration/queue_runner/command_server/protocol/command_server.proto,sha256=
|
|
180
|
-
uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py,sha256=
|
|
181
|
-
uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi,sha256=
|
|
181
|
+
uncountable/integration/queue_runner/command_server/protocol/command_server.proto,sha256=YltIWS_vVyuOuaj0HgIYONU0aApIcyVkqM5PF0s9TSQ,1892
|
|
182
|
+
uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py,sha256=2vzQZ8qLnSvhw4FhFVo7CcgvkFDuQUywamuBE55U-Jw,6057
|
|
183
|
+
uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi,sha256=CUurj3pVwW4Zact1i7uOW4vjHa46j6kjQaQpXmnkqrk,6067
|
|
182
184
|
uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py,sha256=wzPYtvuccPVW1qEXaMxU-KB3ptnZYnjHsuit6L1Pk6U,17415
|
|
183
185
|
uncountable/integration/queue_runner/datastore/__init__.py,sha256=VdoQquDSbXvlRbluebD_XavVt4AdKvmvg-8hV1yTbXo,298
|
|
184
186
|
uncountable/integration/queue_runner/datastore/datastore_postgres.py,sha256=oKRwOWlxgodJyqKr-jDcP9VbXKKTrU26lI-7m4HFBuk,359
|
|
185
|
-
uncountable/integration/queue_runner/datastore/datastore_sql_base.py,sha256=
|
|
187
|
+
uncountable/integration/queue_runner/datastore/datastore_sql_base.py,sha256=UE2dc0QS2mx_tTVYxGAE0SXkRaXRhKEXlrqkWIb29nE,11390
|
|
186
188
|
uncountable/integration/queue_runner/datastore/datastore_sqlite.py,sha256=W9hWKKvZrp3JyaO_tmK-qZL3ACnMnCfYwvDSJHHd6qs,1197
|
|
187
189
|
uncountable/integration/queue_runner/datastore/factory.py,sha256=bv_7BxQpgjrggJKz5YDllgP_BihEJ0-CdpLvaG4ooj4,868
|
|
188
|
-
uncountable/integration/queue_runner/datastore/interface.py,sha256=
|
|
190
|
+
uncountable/integration/queue_runner/datastore/interface.py,sha256=xIToMWQmPELuEAznfQRbbNWyBjhr-4moeHP5NT5uzFs,1704
|
|
189
191
|
uncountable/integration/queue_runner/datastore/model.py,sha256=06_fGS0QTALEA_381I1zz63yXYRooaAVJdfYi0ubSd0,851
|
|
190
192
|
uncountable/integration/secret_retrieval/__init__.py,sha256=sisf8cxCYcJmCXCKjEPoT2xIxm1j-Ru9iTfMVxH6NM0,171
|
|
191
193
|
uncountable/integration/secret_retrieval/basic_secret.py,sha256=PfpeaEDwdi8nFlHUJnzCob9GYgGG5O_3mmcaxPPY6zI,3272
|
|
192
194
|
uncountable/integration/secret_retrieval/retrieve_secret.py,sha256=c0VkCvppWKXwgxdblQuj2WhRaEGBPa7vLDoS6mlNOAQ,3919
|
|
193
|
-
uncountable/integration/webhook_server/entrypoint.py,sha256=
|
|
195
|
+
uncountable/integration/webhook_server/entrypoint.py,sha256=1vk6gjMbp7sGyGtcZBXewarP4OT1dSjF70sf-jznIZQ,5874
|
|
194
196
|
uncountable/integration/webhook_server/gunicorn_config.py,sha256=F7XiS1z2BSfCMpwTmEFedk18WdGG5dwtCs_hDBV5TIE,232
|
|
195
197
|
uncountable/types/__init__.py,sha256=d1t7dHkmZ--ykXDKV_IDtWXD4MHCC0bWflT_yUjkRBw,14462
|
|
196
198
|
uncountable/types/async_batch.py,sha256=yCCWrrLQfxXVqZp-KskxLBNkNmuELdz4PJjx8ULppgs,662
|
|
@@ -218,7 +220,7 @@ uncountable/types/data_t.py,sha256=U_dIh5GcyaJlkrHEI4Qf4TOo1uJX07PYO70sYzzKtTo,2
|
|
|
218
220
|
uncountable/types/dataset_measurements.py,sha256=CVwWePUC02B_R_UB86yfNBwnclR-5xpZaiCs2jWtu2A,512
|
|
219
221
|
uncountable/types/dataset_measurements_t.py,sha256=RNGy6g-bjfnCeoYEbcqULC83bS4oZ46fbYKL9whYa8E,1697
|
|
220
222
|
uncountable/types/entity.py,sha256=Zclk1LYcRaYrMDhqyCjMSLEg0fE6_q8LHvV22Qvscgs,566
|
|
221
|
-
uncountable/types/entity_t.py,sha256=
|
|
223
|
+
uncountable/types/entity_t.py,sha256=4brCV8LvOBd8X-d_Jf4GW6quV_hEKiV5f8S5If1BXgY,31630
|
|
222
224
|
uncountable/types/experiment_groups.py,sha256=qUpFOx1AKgzaT_4khCOv5Xs6jwiQGbvHH-GUh3v1nv4,288
|
|
223
225
|
uncountable/types/experiment_groups_t.py,sha256=QjZBdjnbsD1D-VpPSSV6hBg-Q7mujjKvYtdMewtXNVU,712
|
|
224
226
|
uncountable/types/exports.py,sha256=VMmxUO2PpV1Y63hZ2AnVor4H-B6aswJ7YpSru_u89lU,334
|
|
@@ -445,7 +447,7 @@ uncountable/types/api/uploader/complete_async_parse.py,sha256=Os1HAubxTlbut6Gylj
|
|
|
445
447
|
uncountable/types/api/uploader/invoke_uploader.py,sha256=cmy7zl3AMF_AH5PCAB67rBeTE6JIcaUqPzm2NS4k_AE,1907
|
|
446
448
|
uncountable/types/api/user/__init__.py,sha256=gCgbynxG3jA8FQHzercKtrHKHkiIKr8APdZYUniAor8,55
|
|
447
449
|
uncountable/types/api/user/get_current_user_info.py,sha256=BT7bGp5SOHz3XdKWhhdZ6acnInafT2PgdJ-H4YH3W7c,1181
|
|
448
|
-
uncountablepythonsdk-0.0.
|
|
449
|
-
uncountablepythonsdk-0.0.
|
|
450
|
-
uncountablepythonsdk-0.0.
|
|
451
|
-
uncountablepythonsdk-0.0.
|
|
450
|
+
uncountablepythonsdk-0.0.191.dist-info/METADATA,sha256=dRu4VuL-ylGKivo1djoLKXA14upN9PpVWtbvDDar00E,2241
|
|
451
|
+
uncountablepythonsdk-0.0.191.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
452
|
+
uncountablepythonsdk-0.0.191.dist-info/top_level.txt,sha256=1UVGjAU-6hJY9qw2iJ7nCBeEwZ793AEN5ZfKX9A1uj4,31
|
|
453
|
+
uncountablepythonsdk-0.0.191.dist-info/RECORD,,
|
|
File without changes
|
{uncountablepythonsdk-0.0.190.dist-info → uncountablepythonsdk-0.0.191.dist-info}/top_level.txt
RENAMED
|
File without changes
|