UncountablePythonSDK 0.0.69__py3-none-any.whl → 0.0.71__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 (39) hide show
  1. {UncountablePythonSDK-0.0.69.dist-info → UncountablePythonSDK-0.0.71.dist-info}/METADATA +3 -1
  2. {UncountablePythonSDK-0.0.69.dist-info → UncountablePythonSDK-0.0.71.dist-info}/RECORD +39 -18
  3. examples/integration-server/jobs/materials_auto/example_cron.py +2 -2
  4. uncountable/core/environment.py +5 -1
  5. uncountable/integration/cli.py +1 -0
  6. uncountable/integration/cron.py +12 -28
  7. uncountable/integration/db/connect.py +12 -2
  8. uncountable/integration/db/session.py +25 -0
  9. uncountable/integration/entrypoint.py +6 -6
  10. uncountable/integration/executors/generic_upload_executor.py +5 -1
  11. uncountable/integration/job.py +44 -17
  12. uncountable/integration/queue_runner/__init__.py +0 -0
  13. uncountable/integration/queue_runner/command_server/__init__.py +24 -0
  14. uncountable/integration/queue_runner/command_server/command_client.py +68 -0
  15. uncountable/integration/queue_runner/command_server/command_server.py +64 -0
  16. uncountable/integration/queue_runner/command_server/protocol/__init__.py +0 -0
  17. uncountable/integration/queue_runner/command_server/protocol/command_server.proto +22 -0
  18. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.py +40 -0
  19. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2.pyi +38 -0
  20. uncountable/integration/queue_runner/command_server/protocol/command_server_pb2_grpc.py +129 -0
  21. uncountable/integration/queue_runner/command_server/types.py +52 -0
  22. uncountable/integration/queue_runner/datastore/__init__.py +3 -0
  23. uncountable/integration/queue_runner/datastore/datastore_sqlite.py +93 -0
  24. uncountable/integration/queue_runner/datastore/interface.py +19 -0
  25. uncountable/integration/queue_runner/datastore/model.py +17 -0
  26. uncountable/integration/queue_runner/job_scheduler.py +128 -0
  27. uncountable/integration/queue_runner/queue_runner.py +26 -0
  28. uncountable/integration/queue_runner/types.py +7 -0
  29. uncountable/integration/queue_runner/worker.py +109 -0
  30. uncountable/integration/scan_profiles.py +2 -0
  31. uncountable/integration/scheduler.py +40 -3
  32. uncountable/integration/webhook_server/entrypoint.py +27 -31
  33. uncountable/types/__init__.py +2 -0
  34. uncountable/types/api/recipes/get_recipes_data.py +1 -0
  35. uncountable/types/api/recipes/set_recipe_outputs.py +2 -0
  36. uncountable/types/queued_job.py +16 -0
  37. uncountable/types/queued_job_t.py +107 -0
  38. {UncountablePythonSDK-0.0.69.dist-info → UncountablePythonSDK-0.0.71.dist-info}/WHEEL +0 -0
  39. {UncountablePythonSDK-0.0.69.dist-info → UncountablePythonSDK-0.0.71.dist-info}/top_level.txt +0 -0
@@ -3,10 +3,17 @@ import subprocess
3
3
  import sys
4
4
  import time
5
5
  from dataclasses import dataclass
6
+ from datetime import datetime, timezone
6
7
 
7
8
  from opentelemetry.trace import get_current_span
8
9
 
10
+ from uncountable.core.environment import get_local_admin_server_port
9
11
  from uncountable.integration.entrypoint import main as cron_target
12
+ from uncountable.integration.queue_runner.command_server import (
13
+ CommandServerTimeout,
14
+ check_health,
15
+ )
16
+ from uncountable.integration.queue_runner.queue_runner import start_queue_runner
10
17
  from uncountable.integration.telemetry import Logger
11
18
 
12
19
  SHUTDOWN_TIMEOUT_SECS = 30
@@ -66,7 +73,7 @@ def handle_shutdown(logger: Logger, processes: list[ProcessInfo]) -> None:
66
73
  proc_info.process.kill()
67
74
 
68
75
 
69
- def check_process_health(logger: Logger, processes: list[ProcessInfo]) -> None:
76
+ def check_process_alive(logger: Logger, processes: list[ProcessInfo]) -> None:
70
77
  for proc_info in processes:
71
78
  if not proc_info.is_alive:
72
79
  logger.log_error(
@@ -76,6 +83,25 @@ def check_process_health(logger: Logger, processes: list[ProcessInfo]) -> None:
76
83
  sys.exit(1)
77
84
 
78
85
 
86
+ def _wait_queue_runner_online() -> None:
87
+ _MAX_QUEUE_RUNNER_HEALTH_CHECKS = 10
88
+ _QUEUE_RUNNER_HEALTH_CHECK_DELAY_SECS = 1
89
+
90
+ num_attempts = 0
91
+ before = datetime.now(timezone.utc)
92
+ while num_attempts < _MAX_QUEUE_RUNNER_HEALTH_CHECKS:
93
+ try:
94
+ if check_health(port=get_local_admin_server_port()):
95
+ return
96
+ except CommandServerTimeout:
97
+ pass
98
+ num_attempts += 1
99
+ time.sleep(_QUEUE_RUNNER_HEALTH_CHECK_DELAY_SECS)
100
+ after = datetime.now(timezone.utc)
101
+ duration_secs = (after - before).seconds
102
+ raise Exception(f"queue runner failed to come online after {duration_secs} seconds")
103
+
104
+
79
105
  def main() -> None:
80
106
  logger = Logger(get_current_span())
81
107
  processes: list[ProcessInfo] = []
@@ -84,7 +110,18 @@ def main() -> None:
84
110
  processes.append(process)
85
111
  logger.log_info(f"started process {process.name}")
86
112
 
87
- cron_process = multiprocessing.Process(target=cron_target, args={"blocking": True})
113
+ runner_process = multiprocessing.Process(target=start_queue_runner)
114
+ runner_process.start()
115
+ add_process(ProcessInfo(name="queue runner", process=runner_process))
116
+
117
+ try:
118
+ _wait_queue_runner_online()
119
+ except Exception as e:
120
+ logger.log_exception(e)
121
+ handle_shutdown(logger, processes=processes)
122
+ return
123
+
124
+ cron_process = multiprocessing.Process(target=cron_target)
88
125
  cron_process.start()
89
126
  add_process(ProcessInfo(name="cron server", process=cron_process))
90
127
 
@@ -98,7 +135,7 @@ def main() -> None:
98
135
 
99
136
  try:
100
137
  while True:
101
- check_process_health(logger, processes=processes)
138
+ check_process_alive(logger, processes=processes)
102
139
  time.sleep(1)
103
140
  except KeyboardInterrupt:
104
141
  handle_shutdown(logger, processes=processes)
@@ -1,4 +1,5 @@
1
1
  import hmac
2
+ import typing
2
3
  from dataclasses import dataclass
3
4
 
4
5
  import flask
@@ -6,15 +7,21 @@ import simplejson
6
7
  from flask.typing import ResponseReturnValue
7
8
  from flask.wrappers import Response
8
9
  from opentelemetry.trace import get_current_span
9
- from uncountable.core.async_batch import AsyncBatchProcessor
10
- from uncountable.core.environment import get_integration_env, get_webhook_server_port
11
- from uncountable.integration.construct_client import construct_uncountable_client
12
- from uncountable.integration.executors.executors import execute_job
13
- from uncountable.integration.job import WebhookJobArguments
10
+ from uncountable.core.environment import (
11
+ get_integration_env,
12
+ get_local_admin_server_port,
13
+ get_webhook_server_port,
14
+ )
15
+ from uncountable.integration.queue_runner.command_server.command_client import (
16
+ send_job_queue_message,
17
+ )
18
+ from uncountable.integration.queue_runner.command_server.types import (
19
+ CommandServerException,
20
+ )
14
21
  from uncountable.integration.scan_profiles import load_profiles
15
22
  from uncountable.integration.secret_retrieval.retrieve_secret import retrieve_secret
16
- from uncountable.integration.telemetry import JobLogger, Logger, get_otel_tracer
17
- from uncountable.types import job_definition_t, webhook_job_t
23
+ from uncountable.integration.telemetry import Logger
24
+ from uncountable.types import base_t, job_definition_t, queued_job_t, webhook_job_t
18
25
 
19
26
  from pkgs.argument_parser import CachedParser
20
27
 
@@ -66,7 +73,7 @@ class WebhookException(BaseException):
66
73
 
67
74
  def _parse_webhook_payload(
68
75
  *, raw_request_body: bytes, signature_key: str, passed_signature: str
69
- ) -> webhook_job_t.WebhookEventBody:
76
+ ) -> base_t.JsonValue:
70
77
  request_body_signature = hmac.new(
71
78
  signature_key.encode("utf-8"), msg=raw_request_body, digestmod="sha256"
72
79
  ).hexdigest()
@@ -76,7 +83,7 @@ def _parse_webhook_payload(
76
83
 
77
84
  try:
78
85
  request_body = simplejson.loads(raw_request_body.decode())
79
- return webhook_payload_parser.parse_api(request_body)
86
+ return typing.cast(base_t.JsonValue, request_body)
80
87
  except (simplejson.JSONDecodeError, ValueError) as e:
81
88
  raise WebhookException.body_parse_error() from e
82
89
 
@@ -110,31 +117,20 @@ def register_route(
110
117
  passed_signature=passed_signature,
111
118
  )
112
119
 
113
- with get_otel_tracer().start_as_current_span(
114
- "webhook_executor"
115
- ) as span:
116
- job_logger = JobLogger(
117
- profile_metadata=profile_meta,
118
- job_definition=job,
119
- base_span=span,
120
- )
121
- client = construct_uncountable_client(
122
- profile_meta=profile_meta, job_logger=job_logger
123
- )
124
- execute_job(
125
- job_definition=job,
126
- profile_metadata=profile_meta,
127
- args=WebhookJobArguments(
128
- job_definition=job,
129
- profile_metadata=profile_meta,
130
- client=client,
131
- batch_processor=AsyncBatchProcessor(client=client),
132
- logger=job_logger,
133
- payload=webhook_payload,
120
+ try:
121
+ send_job_queue_message(
122
+ job_ref_name=job.id,
123
+ payload=queued_job_t.QueuedJobPayload(
124
+ invocation_context=queued_job_t.InvocationContextWebhook(
125
+ webhook_payload=webhook_payload
126
+ )
134
127
  ),
128
+ port=get_local_admin_server_port(),
135
129
  )
130
+ except CommandServerException as e:
131
+ raise WebhookException.unknown_error() from e
136
132
 
137
- return flask.jsonify(WebhookResponse())
133
+ return flask.jsonify(WebhookResponse())
138
134
  except WebhookException as e:
139
135
  server_logger.log_exception(e)
140
136
  return e.make_error_response()
@@ -63,6 +63,7 @@ from . import overrides_t as overrides_t
63
63
  from . import permissions_t as permissions_t
64
64
  from . import phases_t as phases_t
65
65
  from . import post_base_t as post_base_t
66
+ from . import queued_job_t as queued_job_t
66
67
  from . import recipe_identifiers_t as recipe_identifiers_t
67
68
  from . import recipe_inputs_t as recipe_inputs_t
68
69
  from . import recipe_links_t as recipe_links_t
@@ -163,6 +164,7 @@ __all__: list[str] = [
163
164
  "permissions_t",
164
165
  "phases_t",
165
166
  "post_base_t",
167
+ "queued_job_t",
166
168
  "recipe_identifiers_t",
167
169
  "recipe_inputs_t",
168
170
  "recipe_links_t",
@@ -97,6 +97,7 @@ class RecipeInput:
97
97
  curve_id: typing.Optional[base_t.ObjectId]
98
98
  actual_quantity_json: base_t.JsonValue
99
99
  behavior: str
100
+ ingredient_role_id: typing.Optional[base_t.ObjectId]
100
101
  quantity_dec: typing.Optional[Decimal] = None
101
102
  actual_quantity_dec: typing.Optional[Decimal] = None
102
103
 
@@ -10,6 +10,7 @@ from decimal import Decimal # noqa: F401
10
10
  import dataclasses
11
11
  from pkgs.serialization import serial_class
12
12
  from ... import base_t
13
+ from ... import field_values_t
13
14
  from ... import recipes_t
14
15
  from ... import response_t
15
16
 
@@ -47,6 +48,7 @@ class RecipeOutputValue:
47
48
  value_str: typing.Optional[str] = None
48
49
  value_curve: typing.Optional[CurveValues] = None
49
50
  formatting: typing.Optional[recipes_t.RecipeAttributeFormatting] = None
51
+ field_values: typing.Optional[list[typing.Union[field_values_t.ArgumentValueRefName, field_values_t.ArgumentValueId]]] = None
50
52
 
51
53
 
52
54
  # DO NOT MODIFY -- This file is generated by type_spec
@@ -0,0 +1,16 @@
1
+ # flake8: noqa: F821
2
+ # ruff: noqa: E402 Q003
3
+ # fmt: off
4
+ # isort: skip_file
5
+ # DO NOT MODIFY -- This file is generated by type_spec
6
+ # Kept only for SDK backwards compatibility
7
+ from .queued_job_t import InvocationContextType as InvocationContextType
8
+ from .queued_job_t import InvocationContextBase as InvocationContextBase
9
+ from .queued_job_t import InvocationContextCron as InvocationContextCron
10
+ from .queued_job_t import InvocationContextManual as InvocationContextManual
11
+ from .queued_job_t import InvocationContextWebhook as InvocationContextWebhook
12
+ from .queued_job_t import InvocationContext as InvocationContext
13
+ from .queued_job_t import QueuedJobPayload as QueuedJobPayload
14
+ from .queued_job_t import QueuedJobResult as QueuedJobResult
15
+ from .queued_job_t import QueuedJob as QueuedJob
16
+ # DO NOT MODIFY -- This file is generated by type_spec
@@ -0,0 +1,107 @@
1
+ # DO NOT MODIFY -- This file is generated by type_spec
2
+ # flake8: noqa: F821
3
+ # ruff: noqa: E402 Q003
4
+ # fmt: off
5
+ # isort: skip_file
6
+ from __future__ import annotations
7
+ import typing # noqa: F401
8
+ import datetime # noqa: F401
9
+ from decimal import Decimal # noqa: F401
10
+ from pkgs.strenum_compat import StrEnum
11
+ import dataclasses
12
+ from pkgs.serialization import serial_class
13
+ from pkgs.serialization import serial_union_annotation
14
+ from . import base_t
15
+ from . import job_definition_t
16
+
17
+ __all__: list[str] = [
18
+ "InvocationContext",
19
+ "InvocationContextBase",
20
+ "InvocationContextCron",
21
+ "InvocationContextManual",
22
+ "InvocationContextType",
23
+ "InvocationContextWebhook",
24
+ "QueuedJob",
25
+ "QueuedJobPayload",
26
+ "QueuedJobResult",
27
+ ]
28
+
29
+
30
+ # DO NOT MODIFY -- This file is generated by type_spec
31
+ class InvocationContextType(StrEnum):
32
+ CRON = "cron"
33
+ MANUAL = "manual"
34
+ WEBHOOK = "webhook"
35
+
36
+
37
+ # DO NOT MODIFY -- This file is generated by type_spec
38
+ @dataclasses.dataclass(kw_only=True)
39
+ class InvocationContextBase:
40
+ type: InvocationContextType
41
+
42
+
43
+ # DO NOT MODIFY -- This file is generated by type_spec
44
+ @serial_class(
45
+ parse_require={"type"},
46
+ )
47
+ @dataclasses.dataclass(kw_only=True)
48
+ class InvocationContextCron(InvocationContextBase):
49
+ type: typing.Literal[InvocationContextType.CRON] = InvocationContextType.CRON
50
+
51
+
52
+ # DO NOT MODIFY -- This file is generated by type_spec
53
+ @serial_class(
54
+ parse_require={"type"},
55
+ )
56
+ @dataclasses.dataclass(kw_only=True)
57
+ class InvocationContextManual(InvocationContextBase):
58
+ type: typing.Literal[InvocationContextType.MANUAL] = InvocationContextType.MANUAL
59
+
60
+
61
+ # DO NOT MODIFY -- This file is generated by type_spec
62
+ @serial_class(
63
+ unconverted_values={"webhook_payload"},
64
+ parse_require={"type"},
65
+ )
66
+ @dataclasses.dataclass(kw_only=True)
67
+ class InvocationContextWebhook(InvocationContextBase):
68
+ type: typing.Literal[InvocationContextType.WEBHOOK] = InvocationContextType.WEBHOOK
69
+ webhook_payload: base_t.JsonValue
70
+
71
+
72
+ # DO NOT MODIFY -- This file is generated by type_spec
73
+ InvocationContext = typing.Annotated[
74
+ typing.Union[InvocationContextCron, InvocationContextManual, InvocationContextWebhook],
75
+ serial_union_annotation(
76
+ discriminator="type",
77
+ discriminator_map={
78
+ "cron": InvocationContextCron,
79
+ "manual": InvocationContextManual,
80
+ "webhook": InvocationContextWebhook,
81
+ },
82
+ ),
83
+ ]
84
+
85
+
86
+ # DO NOT MODIFY -- This file is generated by type_spec
87
+ @dataclasses.dataclass(kw_only=True)
88
+ class QueuedJobPayload:
89
+ invocation_context: InvocationContext
90
+
91
+
92
+ # DO NOT MODIFY -- This file is generated by type_spec
93
+ @dataclasses.dataclass(kw_only=True)
94
+ class QueuedJobResult:
95
+ queued_job_uuid: str
96
+ job_result: job_definition_t.JobResult
97
+
98
+
99
+ # DO NOT MODIFY -- This file is generated by type_spec
100
+ @dataclasses.dataclass(kw_only=True)
101
+ class QueuedJob:
102
+ queued_job_uuid: str
103
+ job_ref_name: str
104
+ num_attempts: int
105
+ submitted_at: datetime.datetime
106
+ payload: QueuedJobPayload
107
+ # DO NOT MODIFY -- This file is generated by type_spec