UncountablePythonSDK 0.0.114__py3-none-any.whl → 0.0.116__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.
- examples/integration-server/jobs/materials_auto/example_http.py +35 -0
- examples/integration-server/jobs/materials_auto/example_instrument.py +38 -0
- examples/integration-server/jobs/materials_auto/profile.yaml +15 -0
- pkgs/type_spec/builder.py +18 -5
- pkgs/type_spec/config.py +26 -5
- pkgs/type_spec/cross_output_links.py +9 -7
- pkgs/type_spec/emit_open_api.py +9 -2
- pkgs/type_spec/emit_open_api_util.py +1 -0
- pkgs/type_spec/emit_python.py +4 -1
- pkgs/type_spec/emit_typescript.py +46 -30
- pkgs/type_spec/emit_typescript_util.py +16 -0
- pkgs/type_spec/load_types.py +1 -1
- pkgs/type_spec/open_api_util.py +9 -1
- pkgs/type_spec/parts/base.ts.prepart +1 -0
- pkgs/type_spec/ui_entry_actions/generate_ui_entry_actions.py +19 -5
- uncountable/core/environment.py +1 -1
- uncountable/integration/http_server/__init__.py +5 -0
- uncountable/integration/http_server/types.py +67 -0
- uncountable/integration/job.py +129 -5
- uncountable/integration/server.py +2 -2
- uncountable/integration/telemetry.py +1 -1
- uncountable/integration/webhook_server/entrypoint.py +37 -112
- uncountable/types/api/entity/create_or_update_entity.py +1 -0
- uncountable/types/api/entity/lookup_entity.py +15 -1
- uncountable/types/async_batch_processor.py +3 -0
- uncountable/types/client_base.py +52 -0
- uncountable/types/entity_t.py +8 -0
- uncountable/types/integration_server_t.py +2 -0
- uncountable/types/job_definition.py +2 -0
- uncountable/types/job_definition_t.py +25 -2
- {uncountablepythonsdk-0.0.114.dist-info → uncountablepythonsdk-0.0.116.dist-info}/METADATA +1 -1
- {uncountablepythonsdk-0.0.114.dist-info → uncountablepythonsdk-0.0.116.dist-info}/RECORD +34 -30
- {uncountablepythonsdk-0.0.114.dist-info → uncountablepythonsdk-0.0.116.dist-info}/WHEEL +0 -0
- {uncountablepythonsdk-0.0.114.dist-info → uncountablepythonsdk-0.0.116.dist-info}/top_level.txt +0 -0
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import re
|
|
2
3
|
from dataclasses import dataclass
|
|
3
4
|
from io import StringIO
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import assert_never
|
|
6
7
|
|
|
7
8
|
from main.base.types import (
|
|
9
|
+
base_t,
|
|
8
10
|
ui_entry_actions_t,
|
|
9
11
|
)
|
|
10
|
-
from pkgs.argument_parser import snake_to_camel_case
|
|
11
12
|
from pkgs.serialization_util import serialize_for_api
|
|
12
13
|
from pkgs.type_spec import emit_typescript_util
|
|
13
14
|
from pkgs.type_spec.builder import (
|
|
@@ -45,8 +46,8 @@ def ui_entry_variable_to_type_spec_type(
|
|
|
45
46
|
match variable:
|
|
46
47
|
case ui_entry_actions_t.UiEntryActionVariableString():
|
|
47
48
|
return BaseTypeName.s_string
|
|
48
|
-
case ui_entry_actions_t.
|
|
49
|
-
return "
|
|
49
|
+
case ui_entry_actions_t.UiEntryActionVariableSingleEntity():
|
|
50
|
+
return "ObjectId"
|
|
50
51
|
case _:
|
|
51
52
|
assert_never(variable)
|
|
52
53
|
|
|
@@ -157,6 +158,15 @@ def emit_entry_action_definition(
|
|
|
157
158
|
)
|
|
158
159
|
|
|
159
160
|
|
|
161
|
+
def _validate_input(input: ui_entry_actions_t.UiEntryActionVariable) -> None:
|
|
162
|
+
if "_" in input.vs_var_name:
|
|
163
|
+
raise ValueError(f"Expected camelCase for variable {input.vs_var_name}")
|
|
164
|
+
if not re.fullmatch(base_t.REF_NAME_STRICT_REGEX, input.vs_var_name):
|
|
165
|
+
raise ValueError(
|
|
166
|
+
f"Variable {input.vs_var_name} has invalid syntax. See REF_NAME_STRICT_REGEX"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
160
170
|
def emit_query_index(
|
|
161
171
|
ctx: emit_typescript_util.EmitTypescriptContext,
|
|
162
172
|
defn_infos: list[EntryActionTypeInfo],
|
|
@@ -182,6 +192,7 @@ def emit_query_index(
|
|
|
182
192
|
"type": BaseTypeName.s_object,
|
|
183
193
|
"properties": {
|
|
184
194
|
"value_spec_var": {"type": "String"},
|
|
195
|
+
"type": {"type": "ui_entry_actions.UiEntryActionDataType"},
|
|
185
196
|
"variable": {"type": "ui_entry_actions.UiEntryActionVariable"},
|
|
186
197
|
},
|
|
187
198
|
},
|
|
@@ -215,10 +226,12 @@ def emit_query_index(
|
|
|
215
226
|
for scope, defn in definitions.items():
|
|
216
227
|
inputs = []
|
|
217
228
|
outputs = []
|
|
218
|
-
for
|
|
229
|
+
for input in defn.inputs.values():
|
|
230
|
+
_validate_input(input)
|
|
219
231
|
inputs.append(
|
|
220
232
|
serialize_for_api({
|
|
221
|
-
"value_spec_var":
|
|
233
|
+
"value_spec_var": input.vs_var_name,
|
|
234
|
+
"type": input.type,
|
|
222
235
|
"variable": input,
|
|
223
236
|
})
|
|
224
237
|
)
|
|
@@ -269,6 +282,7 @@ def generate_entry_actions_typescript(
|
|
|
269
282
|
ctx = emit_typescript_util.EmitTypescriptContext(
|
|
270
283
|
out=definition_buffer,
|
|
271
284
|
namespace=index_namespace,
|
|
285
|
+
api_endpoints={},
|
|
272
286
|
)
|
|
273
287
|
builder.namespaces[index_namespace.name] = index_namespace
|
|
274
288
|
|
uncountable/core/environment.py
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import functools
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from flask.wrappers import Response
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HttpException(Exception):
|
|
9
|
+
error_code: int
|
|
10
|
+
message: str
|
|
11
|
+
|
|
12
|
+
def __init__(self, *, error_code: int, message: str) -> None:
|
|
13
|
+
self.error_code = error_code
|
|
14
|
+
self.message = message
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def payload_failed_signature() -> "HttpException":
|
|
18
|
+
return HttpException(
|
|
19
|
+
error_code=401, message="webhook payload did not match signature"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def no_signature_passed() -> "HttpException":
|
|
24
|
+
return HttpException(error_code=400, message="missing signature")
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def body_parse_error() -> "HttpException":
|
|
28
|
+
return HttpException(error_code=400, message="body parse error")
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def unknown_error() -> "HttpException":
|
|
32
|
+
return HttpException(error_code=500, message="internal server error")
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def configuration_error(
|
|
36
|
+
message: str = "internal configuration error",
|
|
37
|
+
) -> "HttpException":
|
|
38
|
+
return HttpException(error_code=500, message=message)
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
return f"[{self.error_code}]: {self.message}"
|
|
42
|
+
|
|
43
|
+
def make_error_response(self) -> Response:
|
|
44
|
+
return Response(
|
|
45
|
+
status=self.error_code, response={"error": {"message": str(self)}}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(kw_only=True, frozen=True)
|
|
50
|
+
class GenericHttpRequest:
|
|
51
|
+
body_base64: str
|
|
52
|
+
headers: dict[str, str]
|
|
53
|
+
|
|
54
|
+
@functools.cached_property
|
|
55
|
+
def body_bytes(self) -> bytes:
|
|
56
|
+
return base64.b64decode(self.body_base64)
|
|
57
|
+
|
|
58
|
+
@functools.cached_property
|
|
59
|
+
def body_text(self) -> str:
|
|
60
|
+
return self.body_bytes.decode()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(kw_only=True)
|
|
64
|
+
class GenericHttpResponse:
|
|
65
|
+
response: str
|
|
66
|
+
status_code: int
|
|
67
|
+
headers: dict[str, str] | None = None
|
uncountable/integration/job.py
CHANGED
|
@@ -1,15 +1,43 @@
|
|
|
1
1
|
import functools
|
|
2
|
+
import hmac
|
|
2
3
|
import typing
|
|
3
4
|
from abc import ABC, abstractmethod
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
|
|
7
|
+
import simplejson
|
|
8
|
+
|
|
6
9
|
from pkgs.argument_parser import CachedParser
|
|
10
|
+
from pkgs.serialization_util import serialize_for_api
|
|
7
11
|
from uncountable.core.async_batch import AsyncBatchProcessor
|
|
8
12
|
from uncountable.core.client import Client
|
|
13
|
+
from uncountable.core.environment import get_local_admin_server_port
|
|
9
14
|
from uncountable.core.file_upload import FileUpload
|
|
15
|
+
from uncountable.integration.http_server import (
|
|
16
|
+
GenericHttpRequest,
|
|
17
|
+
GenericHttpResponse,
|
|
18
|
+
HttpException,
|
|
19
|
+
)
|
|
20
|
+
from uncountable.integration.queue_runner.command_server.command_client import (
|
|
21
|
+
send_job_queue_message,
|
|
22
|
+
)
|
|
23
|
+
from uncountable.integration.queue_runner.command_server.types import (
|
|
24
|
+
CommandServerException,
|
|
25
|
+
)
|
|
26
|
+
from uncountable.integration.secret_retrieval.retrieve_secret import retrieve_secret
|
|
10
27
|
from uncountable.integration.telemetry import JobLogger
|
|
11
|
-
from uncountable.types import
|
|
12
|
-
|
|
28
|
+
from uncountable.types import (
|
|
29
|
+
base_t,
|
|
30
|
+
entity_t,
|
|
31
|
+
job_definition_t,
|
|
32
|
+
queued_job_t,
|
|
33
|
+
webhook_job_t,
|
|
34
|
+
)
|
|
35
|
+
from uncountable.types.job_definition_t import (
|
|
36
|
+
HttpJobDefinitionBase,
|
|
37
|
+
JobDefinition,
|
|
38
|
+
JobResult,
|
|
39
|
+
ProfileMetadata,
|
|
40
|
+
)
|
|
13
41
|
|
|
14
42
|
|
|
15
43
|
@dataclass(kw_only=True)
|
|
@@ -26,9 +54,6 @@ class JobArguments:
|
|
|
26
54
|
CronJobArguments = JobArguments
|
|
27
55
|
|
|
28
56
|
|
|
29
|
-
PT = typing.TypeVar("PT")
|
|
30
|
-
|
|
31
|
-
|
|
32
57
|
class Job[PT](ABC):
|
|
33
58
|
_unc_job_registered: bool = False
|
|
34
59
|
|
|
@@ -63,6 +88,51 @@ class CronJob(Job):
|
|
|
63
88
|
WPT = typing.TypeVar("WPT")
|
|
64
89
|
|
|
65
90
|
|
|
91
|
+
@dataclass(kw_only=True)
|
|
92
|
+
class WebhookResponse:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class CustomHttpJob(Job[GenericHttpRequest]):
|
|
97
|
+
@property
|
|
98
|
+
def payload_type(self) -> type[GenericHttpRequest]:
|
|
99
|
+
return GenericHttpRequest
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
@abstractmethod
|
|
103
|
+
def validate_request(
|
|
104
|
+
*,
|
|
105
|
+
request: GenericHttpRequest,
|
|
106
|
+
job_definition: HttpJobDefinitionBase,
|
|
107
|
+
profile_meta: ProfileMetadata,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""
|
|
110
|
+
Validate that the request is valid. If the request is invalid, raise an
|
|
111
|
+
exception.
|
|
112
|
+
"""
|
|
113
|
+
...
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
@abstractmethod
|
|
117
|
+
def handle_request(
|
|
118
|
+
*,
|
|
119
|
+
request: GenericHttpRequest,
|
|
120
|
+
job_definition: HttpJobDefinitionBase,
|
|
121
|
+
profile_meta: ProfileMetadata,
|
|
122
|
+
) -> GenericHttpResponse:
|
|
123
|
+
"""
|
|
124
|
+
Handle the request synchronously. Normally this should just enqueue a job
|
|
125
|
+
and return immediately (see WebhookJob as an example).
|
|
126
|
+
"""
|
|
127
|
+
...
|
|
128
|
+
|
|
129
|
+
def run_outer(self, args: JobArguments) -> JobResult:
|
|
130
|
+
args.logger.log_warning(
|
|
131
|
+
message=f"Unexpected call to run_outer for CustomHttpJob: {args.job_definition.id}"
|
|
132
|
+
)
|
|
133
|
+
return JobResult(success=False)
|
|
134
|
+
|
|
135
|
+
|
|
66
136
|
class WebhookJob[WPT](Job[webhook_job_t.WebhookEventPayload]):
|
|
67
137
|
@property
|
|
68
138
|
def payload_type(self) -> type[webhook_job_t.WebhookEventPayload]:
|
|
@@ -72,6 +142,60 @@ class WebhookJob[WPT](Job[webhook_job_t.WebhookEventPayload]):
|
|
|
72
142
|
@abstractmethod
|
|
73
143
|
def webhook_payload_type(self) -> type[WPT]: ...
|
|
74
144
|
|
|
145
|
+
@staticmethod
|
|
146
|
+
def validate_request(
|
|
147
|
+
*,
|
|
148
|
+
request: GenericHttpRequest,
|
|
149
|
+
job_definition: job_definition_t.HttpJobDefinitionBase,
|
|
150
|
+
profile_meta: ProfileMetadata,
|
|
151
|
+
) -> None:
|
|
152
|
+
assert isinstance(job_definition, job_definition_t.WebhookJobDefinition)
|
|
153
|
+
signature_key = retrieve_secret(
|
|
154
|
+
profile_metadata=profile_meta,
|
|
155
|
+
secret_retrieval=job_definition.signature_key_secret,
|
|
156
|
+
)
|
|
157
|
+
passed_signature = request.headers.get("Uncountable-Webhook-Signature")
|
|
158
|
+
if passed_signature is None:
|
|
159
|
+
raise HttpException.no_signature_passed()
|
|
160
|
+
|
|
161
|
+
request_body_signature = hmac.new(
|
|
162
|
+
signature_key.encode("utf-8"), msg=request.body_bytes, digestmod="sha256"
|
|
163
|
+
).hexdigest()
|
|
164
|
+
|
|
165
|
+
if request_body_signature != passed_signature:
|
|
166
|
+
raise HttpException.payload_failed_signature()
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def handle_request(
|
|
170
|
+
*,
|
|
171
|
+
request: GenericHttpRequest,
|
|
172
|
+
job_definition: job_definition_t.HttpJobDefinitionBase,
|
|
173
|
+
profile_meta: ProfileMetadata, # noqa: ARG004
|
|
174
|
+
) -> GenericHttpResponse:
|
|
175
|
+
try:
|
|
176
|
+
request_body = simplejson.loads(request.body_text)
|
|
177
|
+
webhook_payload = typing.cast(base_t.JsonValue, request_body)
|
|
178
|
+
except (simplejson.JSONDecodeError, ValueError) as e:
|
|
179
|
+
raise HttpException.body_parse_error() from e
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
send_job_queue_message(
|
|
183
|
+
job_ref_name=job_definition.id,
|
|
184
|
+
payload=queued_job_t.QueuedJobPayload(
|
|
185
|
+
invocation_context=queued_job_t.InvocationContextWebhook(
|
|
186
|
+
webhook_payload=webhook_payload
|
|
187
|
+
)
|
|
188
|
+
),
|
|
189
|
+
port=get_local_admin_server_port(),
|
|
190
|
+
)
|
|
191
|
+
except CommandServerException as e:
|
|
192
|
+
raise HttpException.unknown_error() from e
|
|
193
|
+
|
|
194
|
+
return GenericHttpResponse(
|
|
195
|
+
response=simplejson.dumps(serialize_for_api(WebhookResponse())),
|
|
196
|
+
status_code=200,
|
|
197
|
+
)
|
|
198
|
+
|
|
75
199
|
def run_outer(self, args: JobArguments) -> JobResult:
|
|
76
200
|
webhook_body = self.get_payload(args.payload)
|
|
77
201
|
inner_payload = CachedParser(self.webhook_payload_type).parse_api(
|
|
@@ -16,7 +16,7 @@ from uncountable.integration.telemetry import Logger
|
|
|
16
16
|
from uncountable.types import base_t, job_definition_t
|
|
17
17
|
from uncountable.types.job_definition_t import (
|
|
18
18
|
CronJobDefinition,
|
|
19
|
-
|
|
19
|
+
HttpJobDefinitionBase,
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
_MAX_APSCHEDULER_CONCURRENT_JOBS = 1
|
|
@@ -86,7 +86,7 @@ class IntegrationServer:
|
|
|
86
86
|
misfire_grace_time=None,
|
|
87
87
|
**job_opts,
|
|
88
88
|
)
|
|
89
|
-
case
|
|
89
|
+
case HttpJobDefinitionBase():
|
|
90
90
|
pass
|
|
91
91
|
case _:
|
|
92
92
|
assert_never(job_defn)
|
|
@@ -177,7 +177,7 @@ class JobLogger(Logger):
|
|
|
177
177
|
patched_attributes["job.definition.cron_spec"] = (
|
|
178
178
|
self.job_definition.cron_spec
|
|
179
179
|
)
|
|
180
|
-
case job_definition_t.
|
|
180
|
+
case job_definition_t.HttpJobDefinitionBase():
|
|
181
181
|
pass
|
|
182
182
|
case _:
|
|
183
183
|
assert_never(self.job_definition)
|
|
@@ -1,146 +1,71 @@
|
|
|
1
|
-
import
|
|
2
|
-
import typing
|
|
3
|
-
from dataclasses import dataclass
|
|
1
|
+
import base64
|
|
4
2
|
|
|
5
3
|
import flask
|
|
6
|
-
import simplejson
|
|
7
4
|
from flask.typing import ResponseReturnValue
|
|
8
|
-
from flask.wrappers import Response
|
|
9
5
|
from opentelemetry.trace import get_current_span
|
|
10
6
|
from uncountable.core.environment import (
|
|
11
|
-
|
|
7
|
+
get_http_server_port,
|
|
12
8
|
get_server_env,
|
|
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
9
|
)
|
|
10
|
+
from uncountable.integration.executors.script_executor import resolve_script_executor
|
|
11
|
+
from uncountable.integration.http_server import GenericHttpRequest, HttpException
|
|
12
|
+
from uncountable.integration.job import CustomHttpJob, WebhookJob
|
|
21
13
|
from uncountable.integration.scan_profiles import load_profiles
|
|
22
|
-
from uncountable.integration.secret_retrieval.retrieve_secret import retrieve_secret
|
|
23
14
|
from uncountable.integration.telemetry import Logger
|
|
24
|
-
from uncountable.types import
|
|
25
|
-
|
|
26
|
-
from pkgs.argument_parser import CachedParser
|
|
15
|
+
from uncountable.types import job_definition_t
|
|
27
16
|
|
|
28
17
|
app = flask.Flask(__name__)
|
|
29
18
|
|
|
30
19
|
|
|
31
|
-
@dataclass(kw_only=True)
|
|
32
|
-
class WebhookResponse:
|
|
33
|
-
pass
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
webhook_payload_parser = CachedParser(webhook_job_t.WebhookEventBody)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class WebhookException(BaseException):
|
|
40
|
-
error_code: int
|
|
41
|
-
message: str
|
|
42
|
-
|
|
43
|
-
def __init__(self, *, error_code: int, message: str) -> None:
|
|
44
|
-
self.error_code = error_code
|
|
45
|
-
self.message = message
|
|
46
|
-
|
|
47
|
-
@staticmethod
|
|
48
|
-
def payload_failed_signature() -> "WebhookException":
|
|
49
|
-
return WebhookException(
|
|
50
|
-
error_code=401, message="webhook payload did not match signature"
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
@staticmethod
|
|
54
|
-
def no_signature_passed() -> "WebhookException":
|
|
55
|
-
return WebhookException(error_code=400, message="missing signature")
|
|
56
|
-
|
|
57
|
-
@staticmethod
|
|
58
|
-
def body_parse_error() -> "WebhookException":
|
|
59
|
-
return WebhookException(error_code=400, message="body parse error")
|
|
60
|
-
|
|
61
|
-
@staticmethod
|
|
62
|
-
def unknown_error() -> "WebhookException":
|
|
63
|
-
return WebhookException(error_code=500, message="internal server error")
|
|
64
|
-
|
|
65
|
-
def __str__(self) -> str:
|
|
66
|
-
return f"[{self.error_code}]: {self.message}"
|
|
67
|
-
|
|
68
|
-
def make_error_response(self) -> Response:
|
|
69
|
-
return Response(
|
|
70
|
-
status=self.error_code, response={"error": {"message": str(self)}}
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def _parse_webhook_payload(
|
|
75
|
-
*, raw_request_body: bytes, signature_key: str, passed_signature: str
|
|
76
|
-
) -> base_t.JsonValue:
|
|
77
|
-
request_body_signature = hmac.new(
|
|
78
|
-
signature_key.encode("utf-8"), msg=raw_request_body, digestmod="sha256"
|
|
79
|
-
).hexdigest()
|
|
80
|
-
|
|
81
|
-
if request_body_signature != passed_signature:
|
|
82
|
-
raise WebhookException.payload_failed_signature()
|
|
83
|
-
|
|
84
|
-
try:
|
|
85
|
-
request_body = simplejson.loads(raw_request_body.decode())
|
|
86
|
-
return typing.cast(base_t.JsonValue, request_body)
|
|
87
|
-
except (simplejson.JSONDecodeError, ValueError) as e:
|
|
88
|
-
raise WebhookException.body_parse_error() from e
|
|
89
|
-
|
|
90
|
-
|
|
91
20
|
def register_route(
|
|
92
21
|
*,
|
|
93
22
|
server_logger: Logger,
|
|
94
23
|
profile_meta: job_definition_t.ProfileMetadata,
|
|
95
|
-
job: job_definition_t.
|
|
24
|
+
job: job_definition_t.HttpJobDefinitionBase,
|
|
96
25
|
) -> None:
|
|
97
26
|
route = f"/{profile_meta.name}/{job.id}"
|
|
98
27
|
|
|
99
|
-
def
|
|
28
|
+
def handle_request() -> ResponseReturnValue:
|
|
100
29
|
with server_logger.push_scope(route):
|
|
101
30
|
try:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
31
|
+
if not isinstance(job.executor, job_definition_t.JobExecutorScript):
|
|
32
|
+
raise HttpException.configuration_error(
|
|
33
|
+
message="[internal] http job must use a script executor"
|
|
34
|
+
)
|
|
35
|
+
job_instance = resolve_script_executor(
|
|
36
|
+
executor=job.executor, profile_metadata=profile_meta
|
|
105
37
|
)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
38
|
+
if not isinstance(job_instance, (CustomHttpJob, WebhookJob)):
|
|
39
|
+
raise HttpException.configuration_error(
|
|
40
|
+
message="[internal] http job must descend from CustomHttpJob"
|
|
41
|
+
)
|
|
42
|
+
http_request = GenericHttpRequest(
|
|
43
|
+
body_base64=base64.b64encode(flask.request.get_data()).decode(),
|
|
44
|
+
headers=dict(flask.request.headers),
|
|
109
45
|
)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
signature_key=signature_key,
|
|
116
|
-
passed_signature=passed_signature,
|
|
46
|
+
job_instance.validate_request(
|
|
47
|
+
request=http_request, job_definition=job, profile_meta=profile_meta
|
|
48
|
+
)
|
|
49
|
+
http_response = job_instance.handle_request(
|
|
50
|
+
request=http_request, job_definition=job, profile_meta=profile_meta
|
|
117
51
|
)
|
|
118
52
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
)
|
|
126
|
-
),
|
|
127
|
-
port=get_local_admin_server_port(),
|
|
128
|
-
)
|
|
129
|
-
except CommandServerException as e:
|
|
130
|
-
raise WebhookException.unknown_error() from e
|
|
131
|
-
|
|
132
|
-
return flask.jsonify(WebhookResponse())
|
|
133
|
-
except WebhookException as e:
|
|
53
|
+
return flask.make_response(
|
|
54
|
+
http_response.response,
|
|
55
|
+
http_response.status_code,
|
|
56
|
+
http_response.headers,
|
|
57
|
+
)
|
|
58
|
+
except HttpException as e:
|
|
134
59
|
server_logger.log_exception(e)
|
|
135
60
|
return e.make_error_response()
|
|
136
61
|
except Exception as e:
|
|
137
62
|
server_logger.log_exception(e)
|
|
138
|
-
return
|
|
63
|
+
return HttpException.unknown_error().make_error_response()
|
|
139
64
|
|
|
140
65
|
app.add_url_rule(
|
|
141
66
|
route,
|
|
142
|
-
endpoint=f"
|
|
143
|
-
view_func=
|
|
67
|
+
endpoint=f"handle_request_{job.id}",
|
|
68
|
+
view_func=handle_request,
|
|
144
69
|
methods=["POST"],
|
|
145
70
|
)
|
|
146
71
|
|
|
@@ -152,7 +77,7 @@ def main() -> None:
|
|
|
152
77
|
for profile_metadata in profiles:
|
|
153
78
|
server_logger = Logger(get_current_span())
|
|
154
79
|
for job in profile_metadata.jobs:
|
|
155
|
-
if isinstance(job, job_definition_t.
|
|
80
|
+
if isinstance(job, job_definition_t.HttpJobDefinitionBase):
|
|
156
81
|
register_route(
|
|
157
82
|
server_logger=server_logger, profile_meta=profile_metadata, job=job
|
|
158
83
|
)
|
|
@@ -164,7 +89,7 @@ main()
|
|
|
164
89
|
if __name__ == "__main__":
|
|
165
90
|
app.run(
|
|
166
91
|
host="0.0.0.0",
|
|
167
|
-
port=
|
|
92
|
+
port=get_http_server_port(),
|
|
168
93
|
debug=get_server_env() == "playground",
|
|
169
94
|
exclude_patterns=[],
|
|
170
95
|
)
|
|
@@ -35,6 +35,7 @@ class Arguments:
|
|
|
35
35
|
definition_key: identifier_t.IdentifierKey
|
|
36
36
|
field_values: list[field_values_t.FieldArgumentValue]
|
|
37
37
|
entity_key: identifier_t.IdentifierKey | None = None
|
|
38
|
+
on_create_init_field_values: list[field_values_t.FieldArgumentValue] | None = None
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
# DO NOT MODIFY -- This file is generated by type_spec
|
|
@@ -21,6 +21,7 @@ __all__: list[str] = [
|
|
|
21
21
|
"Data",
|
|
22
22
|
"ENDPOINT_METHOD",
|
|
23
23
|
"ENDPOINT_PATH",
|
|
24
|
+
"LookupEntityCompositeFieldValues",
|
|
24
25
|
"LookupEntityFieldValue",
|
|
25
26
|
"LookupEntityQuery",
|
|
26
27
|
"LookupEntityQueryBase",
|
|
@@ -35,6 +36,7 @@ ENDPOINT_PATH = "api/external/entity/lookup_entity"
|
|
|
35
36
|
# DO NOT MODIFY -- This file is generated by type_spec
|
|
36
37
|
class LookupEntityQueryType(StrEnum):
|
|
37
38
|
FIELD_VALUE = "field_value"
|
|
39
|
+
COMPOSITE_FIELD_VALUES = "composite_field_values"
|
|
38
40
|
|
|
39
41
|
|
|
40
42
|
# DO NOT MODIFY -- This file is generated by type_spec
|
|
@@ -69,14 +71,26 @@ class LookupEntityFieldValue:
|
|
|
69
71
|
value: LookupFieldArgumentValue
|
|
70
72
|
|
|
71
73
|
|
|
74
|
+
# DO NOT MODIFY -- This file is generated by type_spec
|
|
75
|
+
@serial_class(
|
|
76
|
+
named_type_path="sdk.api.entity.lookup_entity.LookupEntityCompositeFieldValues",
|
|
77
|
+
parse_require={"type"},
|
|
78
|
+
)
|
|
79
|
+
@dataclasses.dataclass(slots=base_t.ENABLE_SLOTS, kw_only=True) # type: ignore[literal-required]
|
|
80
|
+
class LookupEntityCompositeFieldValues:
|
|
81
|
+
type: typing.Literal[LookupEntityQueryType.COMPOSITE_FIELD_VALUES] = LookupEntityQueryType.COMPOSITE_FIELD_VALUES
|
|
82
|
+
values: list[LookupFieldArgumentValue]
|
|
83
|
+
|
|
84
|
+
|
|
72
85
|
# DO NOT MODIFY -- This file is generated by type_spec
|
|
73
86
|
LookupEntityQuery = typing.Annotated[
|
|
74
|
-
|
|
87
|
+
LookupEntityFieldValue | LookupEntityCompositeFieldValues,
|
|
75
88
|
serial_union_annotation(
|
|
76
89
|
named_type_path="sdk.api.entity.lookup_entity.LookupEntityQuery",
|
|
77
90
|
discriminator="type",
|
|
78
91
|
discriminator_map={
|
|
79
92
|
"field_value": LookupEntityFieldValue,
|
|
93
|
+
"composite_field_values": LookupEntityCompositeFieldValues,
|
|
80
94
|
},
|
|
81
95
|
),
|
|
82
96
|
]
|
|
@@ -258,10 +258,12 @@ class AsyncBatchProcessorBase(ABC):
|
|
|
258
258
|
definition_key: identifier_t.IdentifierKey,
|
|
259
259
|
field_values: list[field_values_t.FieldArgumentValue],
|
|
260
260
|
entity_key: identifier_t.IdentifierKey | None = None,
|
|
261
|
+
on_create_init_field_values: list[field_values_t.FieldArgumentValue] | None = None,
|
|
261
262
|
depends_on: list[str] | None = None,
|
|
262
263
|
) -> async_batch_t.QueuedAsyncBatchRequest:
|
|
263
264
|
"""Creates or updates field values for an entity
|
|
264
265
|
|
|
266
|
+
:param on_create_init_field_values: Field values set only when the entity is created (will be ignored if entity already exists)
|
|
265
267
|
:param depends_on: A list of batch reference keys to process before processing this request
|
|
266
268
|
"""
|
|
267
269
|
args = create_or_update_entity_t.Arguments(
|
|
@@ -269,6 +271,7 @@ class AsyncBatchProcessorBase(ABC):
|
|
|
269
271
|
entity_type=entity_type,
|
|
270
272
|
definition_key=definition_key,
|
|
271
273
|
field_values=field_values,
|
|
274
|
+
on_create_init_field_values=on_create_init_field_values,
|
|
272
275
|
)
|
|
273
276
|
json_data = serialize_for_api(args)
|
|
274
277
|
|