UncountablePythonSDK 0.0.163__py3-none-any.whl → 0.0.166__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.
- examples/integration-server/pyproject.toml +1 -1
- pkgs/argument_parser/__init__.py +1 -0
- pkgs/argument_parser/argument_parser.py +22 -2
- pkgs/argument_parser/parser_error.py +37 -2
- pkgs/serialization_util/__init__.py +2 -0
- pkgs/serialization_util/serialization_helpers.py +25 -0
- pkgs/type_spec/type_info/emit_type_info.py +4 -5
- uncountable/core/client.py +3 -4
- uncountable/integration/construct_client.py +13 -19
- uncountable/integration/queue_runner/worker.py +50 -3
- uncountable/integration/telemetry.py +32 -1
- uncountable/integration/webhook_server/entrypoint.py +27 -3
- uncountable/types/__init__.py +4 -0
- uncountable/types/api/output_parameters/__init__.py +1 -0
- uncountable/types/api/output_parameters/swap_output_condition_parameters.py +150 -0
- uncountable/types/async_batch_processor.py +67 -24
- uncountable/types/async_batch_t.py +1 -0
- uncountable/types/client_base.py +121 -90
- uncountable/types/entity_t.py +22 -2
- uncountable/types/request_headers.py +9 -0
- uncountable/types/request_headers_t.py +33 -0
- {uncountablepythonsdk-0.0.163.dist-info → uncountablepythonsdk-0.0.166.dist-info}/METADATA +1 -1
- {uncountablepythonsdk-0.0.163.dist-info → uncountablepythonsdk-0.0.166.dist-info}/RECORD +25 -21
- {uncountablepythonsdk-0.0.163.dist-info → uncountablepythonsdk-0.0.166.dist-info}/WHEEL +0 -0
- {uncountablepythonsdk-0.0.163.dist-info → uncountablepythonsdk-0.0.166.dist-info}/top_level.txt +0 -0
pkgs/argument_parser/__init__.py
CHANGED
|
@@ -11,6 +11,7 @@ from .argument_parser import is_union as is_union
|
|
|
11
11
|
from .case_convert import camel_to_snake_case as camel_to_snake_case
|
|
12
12
|
from .case_convert import kebab_to_pascal_case as kebab_to_pascal_case
|
|
13
13
|
from .case_convert import snake_to_camel_case as snake_to_camel_case
|
|
14
|
+
from .parser_error import ParserEnumError as ParserEnumError
|
|
14
15
|
from .parser_error import ParserError as ParserError
|
|
15
16
|
from .parser_error import ParserExtraFieldsError as ParserExtraFieldsError
|
|
16
17
|
from .parser_error import ParserTypeError as ParserTypeError
|
|
@@ -26,7 +26,12 @@ from pkgs.serialization import (
|
|
|
26
26
|
from ._is_enum import is_string_enum_class
|
|
27
27
|
from ._is_namedtuple import is_namedtuple_type
|
|
28
28
|
from .case_convert import camel_to_snake_case, snake_to_camel_case
|
|
29
|
-
from .parser_error import
|
|
29
|
+
from .parser_error import (
|
|
30
|
+
ParserEnumError,
|
|
31
|
+
ParserError,
|
|
32
|
+
ParserExtraFieldsError,
|
|
33
|
+
ParserTypeError,
|
|
34
|
+
)
|
|
30
35
|
|
|
31
36
|
T = typing.TypeVar("T")
|
|
32
37
|
ParserFunction = typing.Callable[[typing.Any], T]
|
|
@@ -500,9 +505,24 @@ def _build_parser_inner(
|
|
|
500
505
|
|
|
501
506
|
return parse_as_numeric_type
|
|
502
507
|
|
|
503
|
-
if parsed_type in (dict, bool)
|
|
508
|
+
if parsed_type in (dict, bool):
|
|
504
509
|
return lambda value: parsed_type(value) # type: ignore
|
|
505
510
|
|
|
511
|
+
if is_string_enum_class(parsed_type):
|
|
512
|
+
assert issubclass(parsed_type, Enum)
|
|
513
|
+
enum_values: set[str] = set(member.value for member in parsed_type)
|
|
514
|
+
|
|
515
|
+
def parse_enum(value: typing.Any) -> T:
|
|
516
|
+
if not isinstance(value, str) or value not in enum_values:
|
|
517
|
+
raise ParserEnumError(
|
|
518
|
+
value=value,
|
|
519
|
+
enum_type=parsed_type,
|
|
520
|
+
valid_values=enum_values,
|
|
521
|
+
)
|
|
522
|
+
return parsed_type(value)
|
|
523
|
+
|
|
524
|
+
return parse_enum
|
|
525
|
+
|
|
506
526
|
if parsed_type is MissingSentryType:
|
|
507
527
|
|
|
508
528
|
def error(value: typing.Any) -> T:
|
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import reprlib
|
|
3
4
|
import typing
|
|
4
5
|
|
|
5
6
|
_UNSET = object()
|
|
7
|
+
_VALUE_REPR_MAX_LENGTH = 120
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _format_value(value: object) -> str:
|
|
11
|
+
value_repr = reprlib.Repr(
|
|
12
|
+
maxstring=_VALUE_REPR_MAX_LENGTH, maxother=_VALUE_REPR_MAX_LENGTH
|
|
13
|
+
).repr(value)
|
|
14
|
+
if len(value_repr) <= _VALUE_REPR_MAX_LENGTH:
|
|
15
|
+
return value_repr
|
|
16
|
+
return f"{value_repr[: _VALUE_REPR_MAX_LENGTH - 3]}..."
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _format_message(message: str, value: object) -> str:
|
|
20
|
+
if value is _UNSET:
|
|
21
|
+
return message
|
|
22
|
+
return f"{message} (value: {_format_value(value)})"
|
|
6
23
|
|
|
7
24
|
|
|
8
25
|
class ParserError(ValueError):
|
|
@@ -22,6 +39,9 @@ class ParserError(ValueError):
|
|
|
22
39
|
self.field_name = field_name
|
|
23
40
|
super().__init__(message)
|
|
24
41
|
|
|
42
|
+
def __str__(self) -> str:
|
|
43
|
+
return _format_message(self.error_message, self.value)
|
|
44
|
+
|
|
25
45
|
def error_chain(self) -> list[ParserError]:
|
|
26
46
|
errors: list[ParserError] = []
|
|
27
47
|
current: BaseException | None = self
|
|
@@ -35,14 +55,14 @@ class ParserError(ValueError):
|
|
|
35
55
|
current: BaseException | None = self
|
|
36
56
|
while current is not None:
|
|
37
57
|
if isinstance(current, ParserError):
|
|
38
|
-
messages.append(current.error_message)
|
|
58
|
+
messages.append(_format_message(current.error_message, current.value))
|
|
39
59
|
current = current.__cause__
|
|
40
60
|
else:
|
|
41
61
|
messages.append(str(current))
|
|
42
62
|
break
|
|
43
63
|
|
|
44
64
|
if len(messages) <= 1:
|
|
45
|
-
return self.error_message
|
|
65
|
+
return _format_message(self.error_message, self.value)
|
|
46
66
|
|
|
47
67
|
return " → ".join(messages)
|
|
48
68
|
|
|
@@ -57,4 +77,19 @@ class ParserExtraFieldsError(ParserError):
|
|
|
57
77
|
)
|
|
58
78
|
|
|
59
79
|
|
|
80
|
+
class ParserEnumError(ParserError):
|
|
81
|
+
valid_values: set[str]
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
*,
|
|
86
|
+
value: object,
|
|
87
|
+
enum_type: type,
|
|
88
|
+
valid_values: set[str],
|
|
89
|
+
) -> None:
|
|
90
|
+
self.valid_values = valid_values
|
|
91
|
+
message = f"'{value}' is not a valid string enum of type {enum_type.__name__}."
|
|
92
|
+
super().__init__(message, value=value, expected_type=enum_type)
|
|
93
|
+
|
|
94
|
+
|
|
60
95
|
class ParserTypeError(ParserError): ...
|
|
@@ -3,12 +3,14 @@ from .dataclasses import dict_fields as dict_fields
|
|
|
3
3
|
from .dataclasses import iterate_fields as iterate_fields
|
|
4
4
|
from .serialization_helpers import (
|
|
5
5
|
JsonValue,
|
|
6
|
+
convert_decimals_to_strings,
|
|
6
7
|
serialize_for_api,
|
|
7
8
|
serialize_for_storage,
|
|
8
9
|
serialize_for_storage_dict,
|
|
9
10
|
)
|
|
10
11
|
|
|
11
12
|
__all__: list[str] = [
|
|
13
|
+
"convert_decimals_to_strings",
|
|
12
14
|
"convert_dict_to_snake_case",
|
|
13
15
|
"serialize_for_api",
|
|
14
16
|
"serialize_for_storage",
|
|
@@ -14,6 +14,7 @@ from typing import (
|
|
|
14
14
|
Protocol,
|
|
15
15
|
TypeVar,
|
|
16
16
|
Union,
|
|
17
|
+
assert_never,
|
|
17
18
|
overload,
|
|
18
19
|
)
|
|
19
20
|
|
|
@@ -37,6 +38,30 @@ else:
|
|
|
37
38
|
T = TypeVar("T")
|
|
38
39
|
|
|
39
40
|
|
|
41
|
+
# IMPROVE: this match should be exhaustive over JsonValue, but mypy can't verify
|
|
42
|
+
# that Mapping()/Sequence() fully narrow the TYPE_CHECKING definition of JsonValue.
|
|
43
|
+
def convert_decimals_to_strings(obj: JsonValue) -> JsonValue:
|
|
44
|
+
match obj:
|
|
45
|
+
case Decimal():
|
|
46
|
+
return str(obj)
|
|
47
|
+
case (
|
|
48
|
+
str()
|
|
49
|
+
| int()
|
|
50
|
+
| float()
|
|
51
|
+
| bool()
|
|
52
|
+
| None
|
|
53
|
+
| datetime.datetime()
|
|
54
|
+
| datetime.date()
|
|
55
|
+
):
|
|
56
|
+
return obj
|
|
57
|
+
case Mapping():
|
|
58
|
+
return {k: convert_decimals_to_strings(v) for k, v in obj.items()}
|
|
59
|
+
case Sequence():
|
|
60
|
+
return [convert_decimals_to_strings(v) for v in obj]
|
|
61
|
+
case _ as unreachable:
|
|
62
|
+
assert_never(unreachable)
|
|
63
|
+
|
|
64
|
+
|
|
40
65
|
class Dataclass(Protocol):
|
|
41
66
|
__dataclass_fields__: ClassVar[dict] # type: ignore[type-arg,unused-ignore]
|
|
42
67
|
|
|
@@ -144,11 +144,10 @@ def asdict_for_yaml_dump(dataclass_instance: Any) -> Any:
|
|
|
144
144
|
def emit_type_info_python(build: builder.SpecBuilder, output: str) -> None:
|
|
145
145
|
type_map = _build_map_all(build, python=True)
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
util.rewrite_file(f"{output}/type_map.yaml", yaml_content)
|
|
147
|
+
for namespace, data in type_map.namespaces.items():
|
|
148
|
+
serial = serialize_for_storage(asdict_for_yaml_dump(data))
|
|
149
|
+
yaml_content = yaml.dump(serial, default_flow_style=False, sort_keys=True)
|
|
150
|
+
util.rewrite_file(f"{output}/namespace_{namespace}.yaml", yaml_content)
|
|
152
151
|
|
|
153
152
|
|
|
154
153
|
@dataclasses.dataclass
|
uncountable/core/client.py
CHANGED
|
@@ -24,13 +24,12 @@ from uncountable.types import download_file_t
|
|
|
24
24
|
from uncountable.types.client_base import APIRequest, ClientMethods
|
|
25
25
|
from uncountable.types.client_config import ClientConfigOptions
|
|
26
26
|
from uncountable.types.client_config_t import RequestContext
|
|
27
|
+
from uncountable.types.request_headers_t import InternalRequestHeaders, VersionHeaders
|
|
27
28
|
|
|
28
29
|
from .file_upload import FileUpload, FileUploader, UploadedFile
|
|
29
30
|
from .types import AuthDetailsAll, AuthDetailsApiKey, AuthDetailsOAuth
|
|
30
31
|
|
|
31
32
|
DT = typing.TypeVar("DT")
|
|
32
|
-
UNC_REQUEST_ID_HEADER = "X-UNC-REQUEST-ID"
|
|
33
|
-
UNC_SDK_VERSION_HEADER = "X-UNC-SDK-VERSION"
|
|
34
33
|
|
|
35
34
|
|
|
36
35
|
class EndpointMethod(StrEnum):
|
|
@@ -328,8 +327,8 @@ class Client(ClientMethods):
|
|
|
328
327
|
self, *, api_request: APIRequest, request_id: str
|
|
329
328
|
) -> HTTPRequest:
|
|
330
329
|
headers = self._build_auth_headers()
|
|
331
|
-
headers[
|
|
332
|
-
headers[
|
|
330
|
+
headers[InternalRequestHeaders.REQUEST_ID] = request_id
|
|
331
|
+
headers[VersionHeaders.UNC_SDK_VERSION] = get_version()
|
|
333
332
|
if self._cfg.request_context is not None:
|
|
334
333
|
if self._cfg.request_context.user_locale is not None:
|
|
335
334
|
headers["X-User-Locale"] = self._cfg.request_context.user_locale
|
|
@@ -30,34 +30,28 @@ def _construct_auth_details(profile_meta: ProfileMetadata) -> AuthDetailsAll:
|
|
|
30
30
|
return AuthDetailsApiKey(api_id=api_id, api_secret_key=api_key)
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def _construct_client_config(
|
|
34
|
-
*,
|
|
35
|
-
profile_meta: ProfileMetadata,
|
|
36
|
-
job_logger: JobLogger,
|
|
37
|
-
request_context: RequestContext | None = None,
|
|
38
|
-
) -> ClientConfig:
|
|
39
|
-
if profile_meta.client_options is None:
|
|
40
|
-
return ClientConfig(logger=job_logger, request_context=request_context)
|
|
41
|
-
return ClientConfig(
|
|
42
|
-
allow_insecure_tls=profile_meta.client_options.allow_insecure_tls,
|
|
43
|
-
extra_headers=profile_meta.client_options.extra_headers,
|
|
44
|
-
logger=job_logger,
|
|
45
|
-
request_context=request_context,
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
|
|
49
33
|
def construct_uncountable_client(
|
|
50
34
|
profile_meta: ProfileMetadata,
|
|
51
35
|
logger: JobLogger,
|
|
52
36
|
*,
|
|
53
37
|
request_context: RequestContext | None = None,
|
|
38
|
+
headers: dict[str, str] | None = None,
|
|
54
39
|
) -> Client:
|
|
40
|
+
extra_headers = {}
|
|
41
|
+
allow_insecure_tls = False
|
|
42
|
+
if profile_meta.client_options is not None:
|
|
43
|
+
extra_headers.update(profile_meta.client_options.extra_headers or {})
|
|
44
|
+
allow_insecure_tls = profile_meta.client_options.allow_insecure_tls
|
|
45
|
+
|
|
46
|
+
extra_headers.update(headers or {})
|
|
47
|
+
|
|
55
48
|
return Client(
|
|
56
49
|
base_url=profile_meta.base_url,
|
|
57
50
|
auth_details=_construct_auth_details(profile_meta),
|
|
58
|
-
config=
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
config=ClientConfig(
|
|
52
|
+
allow_insecure_tls=allow_insecure_tls,
|
|
53
|
+
extra_headers=extra_headers,
|
|
54
|
+
logger=logger,
|
|
61
55
|
request_context=request_context,
|
|
62
56
|
),
|
|
63
57
|
app_base_url=profile_meta.app_base_url,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import pathlib
|
|
2
4
|
import sys
|
|
3
5
|
import typing
|
|
4
6
|
from concurrent.futures import ProcessPoolExecutor
|
|
@@ -23,7 +25,13 @@ from uncountable.integration.queue_runner.datastore.interface import Datastore
|
|
|
23
25
|
from uncountable.integration.queue_runner.types import ListenQueue, ResultQueue
|
|
24
26
|
from uncountable.integration.scan_profiles import load_profiles
|
|
25
27
|
from uncountable.integration.telemetry import JobLogger, Logger, get_otel_tracer
|
|
26
|
-
from uncountable.types import
|
|
28
|
+
from uncountable.types import (
|
|
29
|
+
base_t,
|
|
30
|
+
client_config_t,
|
|
31
|
+
job_definition_t,
|
|
32
|
+
queued_job_t,
|
|
33
|
+
request_headers_t,
|
|
34
|
+
)
|
|
27
35
|
|
|
28
36
|
|
|
29
37
|
class Worker:
|
|
@@ -99,14 +107,48 @@ def _resolve_queued_job_request_context(
|
|
|
99
107
|
return None
|
|
100
108
|
|
|
101
109
|
|
|
110
|
+
_CGROUP_V2_MEMORY_MAX = pathlib.Path("/sys/fs/cgroup/memory.max")
|
|
111
|
+
_CGROUP_V1_MEMORY_LIMIT = pathlib.Path("/sys/fs/cgroup/memory/memory.limit_in_bytes")
|
|
112
|
+
|
|
113
|
+
_MEMORY_LIMIT_FRACTION_CGROUP = 0.75
|
|
114
|
+
_MEMORY_LIMIT_FRACTION_HOST = 0.9
|
|
115
|
+
|
|
116
|
+
_logger = logging.getLogger(__name__)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _get_cgroup_memory_limit_bytes() -> int | None:
|
|
120
|
+
for cgroup_path in (_CGROUP_V2_MEMORY_MAX, _CGROUP_V1_MEMORY_LIMIT):
|
|
121
|
+
if not cgroup_path.exists():
|
|
122
|
+
continue
|
|
123
|
+
raw = cgroup_path.read_text().strip()
|
|
124
|
+
if raw == "max":
|
|
125
|
+
# cgroups v2 "max" means no limit is set
|
|
126
|
+
return None
|
|
127
|
+
value = int(raw)
|
|
128
|
+
# cgroups v1 uses a very large sentinel for "no limit"
|
|
129
|
+
if value >= 2**63 - 1:
|
|
130
|
+
return None
|
|
131
|
+
return value
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
102
135
|
def run_queued_job(
|
|
103
136
|
queued_job: queued_job_t.QueuedJob,
|
|
104
137
|
) -> job_definition_t.JobResult:
|
|
105
138
|
with get_otel_tracer().start_as_current_span(name="run_queued_job") as span:
|
|
106
139
|
if resource is not None:
|
|
107
|
-
|
|
108
|
-
|
|
140
|
+
cgroup_limit = _get_cgroup_memory_limit_bytes()
|
|
141
|
+
if cgroup_limit is not None:
|
|
142
|
+
limit_bytes = int(cgroup_limit * _MEMORY_LIMIT_FRACTION_CGROUP)
|
|
143
|
+
else:
|
|
144
|
+
total_mem = psutil.virtual_memory().total
|
|
145
|
+
limit_bytes = int(total_mem * _MEMORY_LIMIT_FRACTION_HOST)
|
|
109
146
|
resource.setrlimit(resource.RLIMIT_AS, (limit_bytes, limit_bytes))
|
|
147
|
+
_logger.info(
|
|
148
|
+
"RLIMIT_AS set to %.2f GiB (source: %s)",
|
|
149
|
+
limit_bytes / (1024**3),
|
|
150
|
+
"cgroup" if cgroup_limit is not None else "host",
|
|
151
|
+
)
|
|
110
152
|
|
|
111
153
|
job_details = get_registered_job_details(queued_job.job_ref_name)
|
|
112
154
|
job_logger = JobLogger(
|
|
@@ -123,6 +165,11 @@ def run_queued_job(
|
|
|
123
165
|
profile_meta=job_details.profile_metadata,
|
|
124
166
|
logger=job_logger,
|
|
125
167
|
request_context=request_context,
|
|
168
|
+
headers={
|
|
169
|
+
request_headers_t.IntegrationServerHeaders.JOB_UUID: queued_job.queued_job_uuid,
|
|
170
|
+
request_headers_t.IntegrationServerHeaders.JOB_ID: job_details.job_definition.id,
|
|
171
|
+
request_headers_t.IntegrationServerHeaders.JOB_NAME: job_details.job_definition.name,
|
|
172
|
+
},
|
|
126
173
|
)
|
|
127
174
|
batch_processor = AsyncBatchProcessor(client=client)
|
|
128
175
|
|
|
@@ -220,9 +220,15 @@ class Logger:
|
|
|
220
220
|
|
|
221
221
|
|
|
222
222
|
class PerJobResourceTracker:
|
|
223
|
-
def __init__(
|
|
223
|
+
def __init__(
|
|
224
|
+
self,
|
|
225
|
+
logger: "JobLogger",
|
|
226
|
+
sample_interval: float = 0.5,
|
|
227
|
+
log_interval: float = 30.0,
|
|
228
|
+
) -> None:
|
|
224
229
|
self.logger = logger
|
|
225
230
|
self.sample_interval = sample_interval
|
|
231
|
+
self.log_interval = log_interval
|
|
226
232
|
self._process = psutil.Process(os.getpid())
|
|
227
233
|
self._stop_event = threading.Event()
|
|
228
234
|
self._thread: threading.Thread | None = None
|
|
@@ -233,15 +239,40 @@ class PerJobResourceTracker:
|
|
|
233
239
|
self.start_wall_time: float | None = None
|
|
234
240
|
self.end_wall_time: float | None = None
|
|
235
241
|
|
|
242
|
+
def _current_stats(self) -> Attributes:
|
|
243
|
+
assert self.start_cpu_times is not None
|
|
244
|
+
assert self.start_wall_time is not None
|
|
245
|
+
current_cpu_times = self._process.cpu_times()
|
|
246
|
+
cpu_user = current_cpu_times.user - self.start_cpu_times.user
|
|
247
|
+
cpu_sys = current_cpu_times.system - self.start_cpu_times.system
|
|
248
|
+
cpu_total = cpu_user + cpu_sys
|
|
249
|
+
elapsed = time.monotonic() - self.start_wall_time
|
|
250
|
+
return {
|
|
251
|
+
"cpu_user_s": round(cpu_user, 3),
|
|
252
|
+
"cpu_system_s": round(cpu_sys, 3),
|
|
253
|
+
"cpu_total_s": round(cpu_total, 3),
|
|
254
|
+
"wall_time_s": round(elapsed, 3),
|
|
255
|
+
"current_rss_mb": round(self._process.memory_info().rss / (1024 * 1024), 2),
|
|
256
|
+
"peak_rss_mb": round(self.max_rss / (1024 * 1024), 2),
|
|
257
|
+
}
|
|
258
|
+
|
|
236
259
|
def start(self) -> None:
|
|
237
260
|
self.start_cpu_times = self._process.cpu_times()
|
|
238
261
|
self.start_wall_time = time.monotonic()
|
|
239
262
|
|
|
240
263
|
def _monitor() -> None:
|
|
264
|
+
last_log_time = time.monotonic()
|
|
241
265
|
try:
|
|
242
266
|
while not self._stop_event.is_set():
|
|
243
267
|
rss = self._process.memory_info().rss
|
|
244
268
|
self.max_rss = max(self.max_rss, rss)
|
|
269
|
+
now = time.monotonic()
|
|
270
|
+
if now - last_log_time >= self.log_interval:
|
|
271
|
+
self.logger.log_info(
|
|
272
|
+
"Job resource usage (periodic)",
|
|
273
|
+
attributes=self._current_stats(),
|
|
274
|
+
)
|
|
275
|
+
last_log_time = now
|
|
245
276
|
time.sleep(self.sample_interval)
|
|
246
277
|
except Exception:
|
|
247
278
|
self._stop_event.set()
|
|
@@ -24,8 +24,22 @@ def register_route(
|
|
|
24
24
|
job: job_definition_t.HttpJobDefinitionBase,
|
|
25
25
|
) -> None:
|
|
26
26
|
route = f"/{profile_meta.name}/{job.id}"
|
|
27
|
+
log_attributes = {
|
|
28
|
+
"profile.name": profile_meta.name,
|
|
29
|
+
"profile.base_url": profile_meta.base_url,
|
|
30
|
+
"job.name": job.name,
|
|
31
|
+
"job.id": job.id,
|
|
32
|
+
"http.route": route,
|
|
33
|
+
}
|
|
27
34
|
|
|
28
35
|
def handle_request() -> ResponseReturnValue:
|
|
36
|
+
method = flask.request.method
|
|
37
|
+
url = flask.request.url
|
|
38
|
+
request_attributes = {
|
|
39
|
+
**log_attributes,
|
|
40
|
+
"http.method": method,
|
|
41
|
+
"http.url": url,
|
|
42
|
+
}
|
|
29
43
|
with server_logger.push_scope(route):
|
|
30
44
|
try:
|
|
31
45
|
if not isinstance(job.executor, job_definition_t.JobExecutorScript):
|
|
@@ -56,10 +70,18 @@ def register_route(
|
|
|
56
70
|
http_response.headers,
|
|
57
71
|
)
|
|
58
72
|
except HttpException as e:
|
|
59
|
-
server_logger.log_exception(
|
|
73
|
+
server_logger.log_exception(
|
|
74
|
+
e,
|
|
75
|
+
message=f"HTTP error on {method} {url}",
|
|
76
|
+
attributes=request_attributes,
|
|
77
|
+
)
|
|
60
78
|
return e.make_error_response()
|
|
61
79
|
except Exception as e:
|
|
62
|
-
server_logger.log_exception(
|
|
80
|
+
server_logger.log_exception(
|
|
81
|
+
e,
|
|
82
|
+
message=f"Unexpected error on {method} {url}",
|
|
83
|
+
attributes=request_attributes,
|
|
84
|
+
)
|
|
63
85
|
return HttpException.unknown_error().make_error_response()
|
|
64
86
|
|
|
65
87
|
app.add_url_rule(
|
|
@@ -69,7 +91,9 @@ def register_route(
|
|
|
69
91
|
methods=["POST"],
|
|
70
92
|
)
|
|
71
93
|
|
|
72
|
-
server_logger.log_info(
|
|
94
|
+
server_logger.log_info(
|
|
95
|
+
f"job {job.id} webhook registered at: {route}", attributes=log_attributes
|
|
96
|
+
)
|
|
73
97
|
|
|
74
98
|
|
|
75
99
|
def main() -> None:
|
uncountable/types/__init__.py
CHANGED
|
@@ -108,6 +108,7 @@ from . import recipes_t as recipes_t
|
|
|
108
108
|
from .api.integrations import register_sockets_token as register_sockets_token_t
|
|
109
109
|
from .api.recipes import remove_recipe_from_project as remove_recipe_from_project_t
|
|
110
110
|
from .api.recipe_links import remove_recipe_link as remove_recipe_link_t
|
|
111
|
+
from . import request_headers_t as request_headers_t
|
|
111
112
|
from .api.entity import resolve_entity_ids as resolve_entity_ids_t
|
|
112
113
|
from .api.outputs import resolve_output_conditions as resolve_output_conditions_t
|
|
113
114
|
from . import response_t as response_t
|
|
@@ -134,6 +135,7 @@ from .api.entity import set_values as set_values_t
|
|
|
134
135
|
from . import sockets_t as sockets_t
|
|
135
136
|
from . import specs_t as specs_t
|
|
136
137
|
from . import step_relationships_t as step_relationships_t
|
|
138
|
+
from .api.output_parameters import swap_output_condition_parameters as swap_output_condition_parameters_t
|
|
137
139
|
from .api.entity import transition_entity_phase as transition_entity_phase_t
|
|
138
140
|
from .api.recipes import unarchive_recipes as unarchive_recipes_t
|
|
139
141
|
from . import units_t as units_t
|
|
@@ -258,6 +260,7 @@ __all__: list[str] = [
|
|
|
258
260
|
"register_sockets_token_t",
|
|
259
261
|
"remove_recipe_from_project_t",
|
|
260
262
|
"remove_recipe_link_t",
|
|
263
|
+
"request_headers_t",
|
|
261
264
|
"resolve_entity_ids_t",
|
|
262
265
|
"resolve_output_conditions_t",
|
|
263
266
|
"response_t",
|
|
@@ -284,6 +287,7 @@ __all__: list[str] = [
|
|
|
284
287
|
"sockets_t",
|
|
285
288
|
"specs_t",
|
|
286
289
|
"step_relationships_t",
|
|
290
|
+
"swap_output_condition_parameters_t",
|
|
287
291
|
"transition_entity_phase_t",
|
|
288
292
|
"unarchive_recipes_t",
|
|
289
293
|
"units_t",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# DO NOT MODIFY -- This file is generated by type_spec
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# DO NOT MODIFY -- This file is generated by type_spec
|
|
2
|
+
# ruff: noqa: E402 Q003
|
|
3
|
+
# fmt: off
|
|
4
|
+
# isort: skip_file
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import typing # noqa: F401
|
|
7
|
+
import datetime # noqa: F401
|
|
8
|
+
from decimal import Decimal # noqa: F401
|
|
9
|
+
from enum import StrEnum
|
|
10
|
+
import dataclasses
|
|
11
|
+
from pkgs.serialization import serial_class
|
|
12
|
+
from pkgs.serialization import serial_union_annotation
|
|
13
|
+
from ... import async_batch_t
|
|
14
|
+
from ... import base_t
|
|
15
|
+
from ... import identifier_t
|
|
16
|
+
|
|
17
|
+
__all__: list[str] = [
|
|
18
|
+
"AllInProjectSwapScope",
|
|
19
|
+
"Arguments",
|
|
20
|
+
"ConditionParameterDefinition",
|
|
21
|
+
"Data",
|
|
22
|
+
"ENDPOINT_METHOD",
|
|
23
|
+
"ENDPOINT_PATH",
|
|
24
|
+
"NewOutputCondition",
|
|
25
|
+
"NewOutputConditionExisting",
|
|
26
|
+
"NewOutputConditionFromDefinition",
|
|
27
|
+
"NewOutputConditionType",
|
|
28
|
+
"SwapScope",
|
|
29
|
+
"SwapScopeType",
|
|
30
|
+
"TargetedSwapScope",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
ENDPOINT_METHOD = "POST"
|
|
34
|
+
ENDPOINT_PATH = "api/external/output_parameters/swap_output_condition_parameters"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# DO NOT MODIFY -- This file is generated by type_spec
|
|
38
|
+
@serial_class(
|
|
39
|
+
named_type_path="sdk.api.output_parameters.swap_output_condition_parameters.ConditionParameterDefinition",
|
|
40
|
+
)
|
|
41
|
+
@dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
|
|
42
|
+
class ConditionParameterDefinition:
|
|
43
|
+
condition_parameter_key: identifier_t.IdentifierKey
|
|
44
|
+
value_numeric: Decimal | None = None
|
|
45
|
+
value_text: str | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# DO NOT MODIFY -- This file is generated by type_spec
|
|
49
|
+
class NewOutputConditionType(StrEnum):
|
|
50
|
+
EXISTING = "existing"
|
|
51
|
+
DEFINITION = "definition"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# DO NOT MODIFY -- This file is generated by type_spec
|
|
55
|
+
@serial_class(
|
|
56
|
+
named_type_path="sdk.api.output_parameters.swap_output_condition_parameters.NewOutputConditionExisting",
|
|
57
|
+
parse_require={"type"},
|
|
58
|
+
)
|
|
59
|
+
@dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
|
|
60
|
+
class NewOutputConditionExisting:
|
|
61
|
+
type: typing.Literal[NewOutputConditionType.EXISTING] = NewOutputConditionType.EXISTING
|
|
62
|
+
condition_key: identifier_t.IdentifierKey
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# DO NOT MODIFY -- This file is generated by type_spec
|
|
66
|
+
@serial_class(
|
|
67
|
+
named_type_path="sdk.api.output_parameters.swap_output_condition_parameters.NewOutputConditionFromDefinition",
|
|
68
|
+
parse_require={"type"},
|
|
69
|
+
)
|
|
70
|
+
@dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
|
|
71
|
+
class NewOutputConditionFromDefinition:
|
|
72
|
+
type: typing.Literal[NewOutputConditionType.DEFINITION] = NewOutputConditionType.DEFINITION
|
|
73
|
+
parameters: list[ConditionParameterDefinition]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# DO NOT MODIFY -- This file is generated by type_spec
|
|
77
|
+
NewOutputCondition = typing.Annotated[
|
|
78
|
+
NewOutputConditionExisting | NewOutputConditionFromDefinition,
|
|
79
|
+
serial_union_annotation(
|
|
80
|
+
named_type_path="sdk.api.output_parameters.swap_output_condition_parameters.NewOutputCondition",
|
|
81
|
+
discriminator="type",
|
|
82
|
+
discriminator_map={
|
|
83
|
+
"existing": NewOutputConditionExisting,
|
|
84
|
+
"definition": NewOutputConditionFromDefinition,
|
|
85
|
+
},
|
|
86
|
+
),
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# DO NOT MODIFY -- This file is generated by type_spec
|
|
91
|
+
class SwapScopeType(StrEnum):
|
|
92
|
+
TARGETED = "targeted"
|
|
93
|
+
ALL_IN_PROJECT = "all_in_project"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# DO NOT MODIFY -- This file is generated by type_spec
|
|
97
|
+
@serial_class(
|
|
98
|
+
named_type_path="sdk.api.output_parameters.swap_output_condition_parameters.TargetedSwapScope",
|
|
99
|
+
parse_require={"type"},
|
|
100
|
+
)
|
|
101
|
+
@dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
|
|
102
|
+
class TargetedSwapScope:
|
|
103
|
+
type: typing.Literal[SwapScopeType.TARGETED] = SwapScopeType.TARGETED
|
|
104
|
+
recipe_keys: list[identifier_t.IdentifierKey]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# DO NOT MODIFY -- This file is generated by type_spec
|
|
108
|
+
@serial_class(
|
|
109
|
+
named_type_path="sdk.api.output_parameters.swap_output_condition_parameters.AllInProjectSwapScope",
|
|
110
|
+
parse_require={"type"},
|
|
111
|
+
)
|
|
112
|
+
@dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
|
|
113
|
+
class AllInProjectSwapScope:
|
|
114
|
+
type: typing.Literal[SwapScopeType.ALL_IN_PROJECT] = SwapScopeType.ALL_IN_PROJECT
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# DO NOT MODIFY -- This file is generated by type_spec
|
|
118
|
+
SwapScope = typing.Annotated[
|
|
119
|
+
TargetedSwapScope | AllInProjectSwapScope,
|
|
120
|
+
serial_union_annotation(
|
|
121
|
+
named_type_path="sdk.api.output_parameters.swap_output_condition_parameters.SwapScope",
|
|
122
|
+
discriminator="type",
|
|
123
|
+
discriminator_map={
|
|
124
|
+
"targeted": TargetedSwapScope,
|
|
125
|
+
"all_in_project": AllInProjectSwapScope,
|
|
126
|
+
},
|
|
127
|
+
),
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# DO NOT MODIFY -- This file is generated by type_spec
|
|
132
|
+
@serial_class(
|
|
133
|
+
named_type_path="sdk.api.output_parameters.swap_output_condition_parameters.Arguments",
|
|
134
|
+
)
|
|
135
|
+
@dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
|
|
136
|
+
class Arguments:
|
|
137
|
+
scope: SwapScope
|
|
138
|
+
output_key: identifier_t.IdentifierKey
|
|
139
|
+
new_condition: NewOutputCondition
|
|
140
|
+
old_condition_key: identifier_t.IdentifierKey | None = None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# DO NOT MODIFY -- This file is generated by type_spec
|
|
144
|
+
@serial_class(
|
|
145
|
+
named_type_path="sdk.api.output_parameters.swap_output_condition_parameters.Data",
|
|
146
|
+
)
|
|
147
|
+
@dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
|
|
148
|
+
class Data(async_batch_t.AsyncBatchActionReturn):
|
|
149
|
+
pass
|
|
150
|
+
# DO NOT MODIFY -- This file is generated by type_spec
|