UncountablePythonSDK 0.0.59__py3-none-any.whl → 0.0.60__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.
- {UncountablePythonSDK-0.0.59.dist-info → UncountablePythonSDK-0.0.60.dist-info}/METADATA +2 -1
- {UncountablePythonSDK-0.0.59.dist-info → UncountablePythonSDK-0.0.60.dist-info}/RECORD +18 -15
- uncountable/core/client.py +1 -1
- uncountable/core/environment.py +24 -0
- uncountable/integration/cron.py +6 -18
- uncountable/integration/executors/executors.py +33 -1
- uncountable/integration/executors/generic_upload_executor.py +1 -1
- uncountable/integration/job.py +23 -5
- uncountable/integration/server.py +4 -1
- uncountable/integration/telemetry.py +56 -18
- uncountable/integration/webhook_server/entrypoint.py +164 -0
- uncountable/types/__init__.py +2 -0
- uncountable/types/entity_t.py +2 -0
- uncountable/types/job_definition_t.py +2 -1
- uncountable/types/webhook_job.py +9 -0
- uncountable/types/webhook_job_t.py +33 -0
- uncountable/core/version.py +0 -11
- {UncountablePythonSDK-0.0.59.dist-info → UncountablePythonSDK-0.0.60.dist-info}/WHEEL +0 -0
- {UncountablePythonSDK-0.0.59.dist-info → UncountablePythonSDK-0.0.60.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: UncountablePythonSDK
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.60
|
|
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
|
|
@@ -33,6 +33,7 @@ Requires-Dist: opentelemetry-exporter-otlp-proto-http ==1.*
|
|
|
33
33
|
Requires-Dist: opentelemetry-sdk ==1.*
|
|
34
34
|
Requires-Dist: paramiko ==3.*
|
|
35
35
|
Requires-Dist: boto3 ==1.*
|
|
36
|
+
Requires-Dist: flask ==3.*
|
|
36
37
|
Provides-Extra: test
|
|
37
38
|
Requires-Dist: mypy ==1.* ; extra == 'test'
|
|
38
39
|
Requires-Dist: ruff ==0.* ; extra == 'test'
|
|
@@ -74,27 +74,28 @@ uncountable/__init__.py,sha256=8l8XWNCKsu7TG94c-xa2KHpDegvxDC2FyQISdWC763Y,89
|
|
|
74
74
|
uncountable/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
75
75
|
uncountable/core/__init__.py,sha256=RFv0kO6rKFf1PtBPu83hCGmxqkJamRtsgQ9_-ztw7tA,341
|
|
76
76
|
uncountable/core/async_batch.py,sha256=Gur0VOS0AH2ugwvk65hwoX-iqwQAAyJaejY_LyAZZPo,1210
|
|
77
|
-
uncountable/core/client.py,sha256=
|
|
77
|
+
uncountable/core/client.py,sha256=UaeQx8Tdif2BMIKO5FOHdpaCr6ySGmEplXSDT9wQls8,10626
|
|
78
|
+
uncountable/core/environment.py,sha256=4rLWeUIDYB6w1iqrAiSEaD_OqMoo_ed3xcjz5jM4HGU,572
|
|
78
79
|
uncountable/core/file_upload.py,sha256=qR7BBBWVxFNrb1_WICreo3dkZygE9lcE1fmZCQrDZU0,3469
|
|
79
80
|
uncountable/core/types.py,sha256=s2CjqYJpsmbC7xMwxxT7kJ_V9bwokrjjWVVjpMcQpKI,333
|
|
80
|
-
uncountable/core/version.py,sha256=SqQIHLhiVZXQBeOwygS2FRZ4WEO27JmWhse0lKm7fgU,274
|
|
81
81
|
uncountable/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
82
82
|
uncountable/integration/construct_client.py,sha256=I2XTamht13vs-JYkV4PpNS_Pc4FJm-KVYqNNvxI4qNk,1916
|
|
83
|
-
uncountable/integration/cron.py,sha256=
|
|
83
|
+
uncountable/integration/cron.py,sha256=U6aVIKwLy11n2_ez4MQo5XNZsGjb_Ykh582WG02SWCs,1424
|
|
84
84
|
uncountable/integration/entrypoint.py,sha256=wgOXhTzErttRjOzV4rS4psZW5qUKIa5ez89QndQl61k,785
|
|
85
|
-
uncountable/integration/job.py,sha256=
|
|
85
|
+
uncountable/integration/job.py,sha256=UrsZHWXKE2wD5M3lFKJCXvDdWj7QMDtAREKv6RBnT3Q,1548
|
|
86
86
|
uncountable/integration/scan_profiles.py,sha256=3o_h5Ta8ZQEX1epWyXhtyEof0M1b7MobVG7bRKcsFuM,1166
|
|
87
|
-
uncountable/integration/server.py,sha256=
|
|
88
|
-
uncountable/integration/telemetry.py,sha256=
|
|
87
|
+
uncountable/integration/server.py,sha256=EhzfOyXVNOKUmS5_XN4TZVSke1n1vnGLfuDrVUlo1j4,4528
|
|
88
|
+
uncountable/integration/telemetry.py,sha256=9vIyV_4Hfxdh-EEP5dH3SalQOSCHjXchwgSB34M2ndI,7127
|
|
89
89
|
uncountable/integration/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
90
90
|
uncountable/integration/db/connect.py,sha256=YtQHJ1DBGPhxKFRCfiXqohOYUceKSxMVOJ88aPI48Ug,181
|
|
91
91
|
uncountable/integration/executors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
92
|
-
uncountable/integration/executors/executors.py,sha256=
|
|
93
|
-
uncountable/integration/executors/generic_upload_executor.py,sha256=
|
|
92
|
+
uncountable/integration/executors/executors.py,sha256=CbwatKkHrLhnqYr_nsBjr0KYeOn8KqijxrZPHbxChBY,1961
|
|
93
|
+
uncountable/integration/executors/generic_upload_executor.py,sha256=PpxZmGUac4PtwlEJi7-S7scSa3y6HrdBPs_p2QDyqZs,10286
|
|
94
94
|
uncountable/integration/executors/script_executor.py,sha256=OmSBOtU48G3mqza9c2lCm84pGGyaDk-ZBJCx3RsdJXc,846
|
|
95
95
|
uncountable/integration/secret_retrieval/__init__.py,sha256=3QXVj35w8rRMxVvmmsViFYDi3lcb3g70incfalOEm6o,87
|
|
96
96
|
uncountable/integration/secret_retrieval/retrieve_secret.py,sha256=eoPWbkUtCn_63A4TFlK_nvEDvfm4u2fiOoglmAkBG3U,3004
|
|
97
|
-
uncountable/
|
|
97
|
+
uncountable/integration/webhook_server/entrypoint.py,sha256=gmbGgHpQqrxrfE4GHl-uXNNjET8QVO7Ym5usjyY8MgU,5749
|
|
98
|
+
uncountable/types/__init__.py,sha256=zejeAhEYwf5Jbs-n7S9Wr6NPFXfhWIFdyARB1v40scc,8390
|
|
98
99
|
uncountable/types/async_batch.py,sha256=_OhT25_dEVts_z_n1kqfJH3xlZg3btLqR6TNkfFLlXE,609
|
|
99
100
|
uncountable/types/async_batch_processor.py,sha256=obVzN-PcYLV2pHScszfCGjSq6-Xc34WM1ysx6Fv6tZk,11293
|
|
100
101
|
uncountable/types/async_batch_t.py,sha256=ipSGz93O1KB-WE2dvlvflTKS51rJrf3bJkUojyxos7I,2193
|
|
@@ -110,7 +111,7 @@ uncountable/types/client_config_t.py,sha256=_HdS37gMSTIiD4qLnW9dIgt8_Rt5A6xhwMGG
|
|
|
110
111
|
uncountable/types/curves.py,sha256=W6uMpG5SyW1MS82szNpxkFEn1MnxNpBFyFbQb2Ysfng,366
|
|
111
112
|
uncountable/types/curves_t.py,sha256=TDpsThz4lKmiBmS9b4ItUSCp64TGv8-qDkxb4B2RoTo,1314
|
|
112
113
|
uncountable/types/entity.py,sha256=3XhLteFDRDZvHejDuYh-KvB65hpwrBygljFfiUcOAM8,315
|
|
113
|
-
uncountable/types/entity_t.py,sha256=
|
|
114
|
+
uncountable/types/entity_t.py,sha256=D-Z92DPWObIYdqaQE4UDQX2RB3GBUuyAgcAkKaSBuWw,14599
|
|
114
115
|
uncountable/types/experiment_groups.py,sha256=_0OXcPzSAbkE-rfKt5tPx178YJ4pcEKZvrCxUHgDnvw,309
|
|
115
116
|
uncountable/types/experiment_groups_t.py,sha256=0IGAXwkYiwdjj6aFjLMihxwauACQTyuRU_1usJTeUg4,593
|
|
116
117
|
uncountable/types/field_values.py,sha256=uuIWX-xmfvcinYPdfkWJeb56zzQY01mc9rmotMPMh24,503
|
|
@@ -128,7 +129,7 @@ uncountable/types/input_attributes_t.py,sha256=wE1ekiQfb72Z9VpF5SHipKJkgaJFUHJrN
|
|
|
128
129
|
uncountable/types/inputs.py,sha256=6RIEFfCxLqpeHEGOpu63O4i8zPogjGeB7wiV_rPBw_g,404
|
|
129
130
|
uncountable/types/inputs_t.py,sha256=RW7gF9zTOwByu-nMTMVuBabLOuWKx4O1nvfgvx_R55o,1611
|
|
130
131
|
uncountable/types/job_definition.py,sha256=HXfaYl5Nafm9C0teQLBtqzroe1HlfKJtfGVm2-40hvg,1937
|
|
131
|
-
uncountable/types/job_definition_t.py,sha256=
|
|
132
|
+
uncountable/types/job_definition_t.py,sha256=frtTpcJJCNXzdDlz8OWLMBFvsnS9nmqJVn9l7fBI7uc,7616
|
|
132
133
|
uncountable/types/outputs.py,sha256=sUZx_X-TKCZtLm1YCEH8OISX9DdPlv9ZuUfM3-askCc,281
|
|
133
134
|
uncountable/types/outputs_t.py,sha256=2aORUOr0ls1ZYo-ddkWax3D1ZndmQsWtHfJxpYozlhg,656
|
|
134
135
|
uncountable/types/overrides.py,sha256=Mv-smwK1B3pvbt48fNOiqkeQn9wMgYlBFJKUBOJqceE,431
|
|
@@ -163,6 +164,8 @@ uncountable/types/units.py,sha256=R_TBhxWCIWSSXK9J3S0Omtj3t5BZNK9C80MyqFjMO7k,27
|
|
|
163
164
|
uncountable/types/units_t.py,sha256=w_k7SQ_J_-XTGY-BMcWC5sQi2z3uInqtrRUPHgniuew,559
|
|
164
165
|
uncountable/types/users.py,sha256=YEk8v0vDOBFvmOQMQw7MAOicSGzMui8Hb9hdFX2Vw3E,275
|
|
165
166
|
uncountable/types/users_t.py,sha256=ATsF1EGKw49jfcspf9OWHL0tSgOR76x80XloKk624fk,582
|
|
167
|
+
uncountable/types/webhook_job.py,sha256=Y8IVmL311Hd4Jvdl1d7ND_OCygyC0dAoV-_E4A-lI6A,363
|
|
168
|
+
uncountable/types/webhook_job_t.py,sha256=MM-0epv4nIp4lgtLBTr-gs51PVjBuAMqpHfCPXDnYc0,847
|
|
166
169
|
uncountable/types/workflows.py,sha256=uSZWsdDe2jpXnBnRunEen7_7pbKBrE0ds_ez98EzyPg,353
|
|
167
170
|
uncountable/types/workflows_t.py,sha256=Nz9z_eerEDEP_Ieuz7gQEv1SabkFdCiZmu6RL8w3kyE,831
|
|
168
171
|
uncountable/types/api/__init__.py,sha256=gCgbynxG3jA8FQHzercKtrHKHkiIKr8APdZYUniAor8,55
|
|
@@ -244,7 +247,7 @@ uncountable/types/api/triggers/__init__.py,sha256=gCgbynxG3jA8FQHzercKtrHKHkiIKr
|
|
|
244
247
|
uncountable/types/api/triggers/run_trigger.py,sha256=_Rpha9nxXI3Xr17CrGDtofg4HZ81x2lt0rMZ6As0qfE,893
|
|
245
248
|
uncountable/types/api/uploader/__init__.py,sha256=gCgbynxG3jA8FQHzercKtrHKHkiIKr8APdZYUniAor8,55
|
|
246
249
|
uncountable/types/api/uploader/invoke_uploader.py,sha256=Rc77y5q-3R9-SNQgm8P35zKaW2D1Hbtm7PDixnOn1G0,1025
|
|
247
|
-
UncountablePythonSDK-0.0.
|
|
248
|
-
UncountablePythonSDK-0.0.
|
|
249
|
-
UncountablePythonSDK-0.0.
|
|
250
|
-
UncountablePythonSDK-0.0.
|
|
250
|
+
UncountablePythonSDK-0.0.60.dist-info/METADATA,sha256=bKGoizXKBfPpFqoKEjCvCBGjvN49fQf77Gn7pP25u0o,1961
|
|
251
|
+
UncountablePythonSDK-0.0.60.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
|
|
252
|
+
UncountablePythonSDK-0.0.60.dist-info/top_level.txt,sha256=1UVGjAU-6hJY9qw2iJ7nCBeEwZ793AEN5ZfKX9A1uj4,31
|
|
253
|
+
UncountablePythonSDK-0.0.60.dist-info/RECORD,,
|
uncountable/core/client.py
CHANGED
|
@@ -14,7 +14,7 @@ from requests.exceptions import JSONDecodeError
|
|
|
14
14
|
from pkgs.argument_parser import CachedParser
|
|
15
15
|
from pkgs.serialization_util import serialize_for_api
|
|
16
16
|
from pkgs.serialization_util.serialization_helpers import JsonValue
|
|
17
|
-
from uncountable.core.
|
|
17
|
+
from uncountable.core.environment import get_version
|
|
18
18
|
from uncountable.integration.telemetry import JobLogger
|
|
19
19
|
from uncountable.types.client_base import APIRequest, ClientMethods
|
|
20
20
|
from uncountable.types.client_config import ClientConfigOptions
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import os
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@functools.cache
|
|
7
|
+
def get_version() -> str:
|
|
8
|
+
try:
|
|
9
|
+
version_str = version("UncountablePythonSDK")
|
|
10
|
+
except PackageNotFoundError:
|
|
11
|
+
version_str = "unknown"
|
|
12
|
+
return version_str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_integration_env() -> str | None:
|
|
16
|
+
return os.environ.get("UNC_INTEGRATION_ENV")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_webhook_server_port() -> int:
|
|
20
|
+
return int(os.environ.get("UNC_WEBHOOK_SERVER_PORT") or 5001)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_otel_enabled() -> bool:
|
|
24
|
+
return os.environ.get("UNC_OTEL_ENABLED") == "true"
|
uncountable/integration/cron.py
CHANGED
|
@@ -3,7 +3,7 @@ from dataclasses import dataclass
|
|
|
3
3
|
from pkgs.argument_parser import CachedParser
|
|
4
4
|
from uncountable.core.async_batch import AsyncBatchProcessor
|
|
5
5
|
from uncountable.integration.construct_client import construct_uncountable_client
|
|
6
|
-
from uncountable.integration.executors.executors import
|
|
6
|
+
from uncountable.integration.executors.executors import execute_job
|
|
7
7
|
from uncountable.integration.job import CronJobArguments
|
|
8
8
|
from uncountable.integration.telemetry import JobLogger
|
|
9
9
|
from uncountable.types.job_definition_t import JobDefinition, ProfileMetadata
|
|
@@ -36,20 +36,8 @@ def cron_job_executor(**kwargs: dict) -> None:
|
|
|
36
36
|
logger=job_logger,
|
|
37
37
|
)
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
job_logger.log_info("running job")
|
|
45
|
-
|
|
46
|
-
job.run(args=args)
|
|
47
|
-
|
|
48
|
-
if batch_processor.current_queue_size() != 0:
|
|
49
|
-
batch_processor.send()
|
|
50
|
-
|
|
51
|
-
submitted_batch_job_ids = batch_processor.get_submitted_job_ids()
|
|
52
|
-
job_logger.log_info(
|
|
53
|
-
"completed job",
|
|
54
|
-
attributes={"submitted_batch_job_ids": submitted_batch_job_ids},
|
|
55
|
-
)
|
|
39
|
+
execute_job(
|
|
40
|
+
args=args,
|
|
41
|
+
profile_metadata=args_passed.profile_metadata,
|
|
42
|
+
job_definition=args_passed.definition,
|
|
43
|
+
)
|
|
@@ -2,7 +2,7 @@ from typing import assert_never
|
|
|
2
2
|
|
|
3
3
|
from uncountable.integration.executors.generic_upload_executor import GenericUploadJob
|
|
4
4
|
from uncountable.integration.executors.script_executor import resolve_script_executor
|
|
5
|
-
from uncountable.integration.job import Job
|
|
5
|
+
from uncountable.integration.job import Job, JobArguments
|
|
6
6
|
from uncountable.types import job_definition_t
|
|
7
7
|
|
|
8
8
|
|
|
@@ -22,3 +22,35 @@ def resolve_executor(
|
|
|
22
22
|
data_source=job_executor.data_source,
|
|
23
23
|
)
|
|
24
24
|
assert_never(job_executor)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def execute_job(
|
|
28
|
+
*,
|
|
29
|
+
job_definition: job_definition_t.JobDefinition,
|
|
30
|
+
profile_metadata: job_definition_t.ProfileMetadata,
|
|
31
|
+
args: JobArguments,
|
|
32
|
+
) -> job_definition_t.JobResult:
|
|
33
|
+
with args.logger.push_scope(job_definition.name) as job_logger:
|
|
34
|
+
job = resolve_executor(job_definition.executor, profile_metadata)
|
|
35
|
+
|
|
36
|
+
job_logger.log_info("running job")
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
result = job.run_outer(args=args)
|
|
40
|
+
except Exception as e:
|
|
41
|
+
job_logger.log_exception(e)
|
|
42
|
+
return job_definition_t.JobResult(success=False)
|
|
43
|
+
|
|
44
|
+
if args.batch_processor.current_queue_size() != 0:
|
|
45
|
+
args.batch_processor.send()
|
|
46
|
+
|
|
47
|
+
submitted_batch_job_ids = args.batch_processor.get_submitted_job_ids()
|
|
48
|
+
job_logger.log_info(
|
|
49
|
+
"completed job",
|
|
50
|
+
attributes={
|
|
51
|
+
"submitted_batch_job_ids": submitted_batch_job_ids,
|
|
52
|
+
"success": result.success,
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return result
|
|
@@ -226,7 +226,7 @@ class GenericUploadJob(Job):
|
|
|
226
226
|
|
|
227
227
|
return S3Session(s3_config=s3_config)
|
|
228
228
|
|
|
229
|
-
def
|
|
229
|
+
def run_outer(self, args: JobArguments) -> JobResult:
|
|
230
230
|
client = args.client
|
|
231
231
|
batch_processor = args.batch_processor
|
|
232
232
|
logger = args.logger
|
uncountable/integration/job.py
CHANGED
|
@@ -4,10 +4,11 @@ from dataclasses import dataclass
|
|
|
4
4
|
from uncountable.core.async_batch import AsyncBatchProcessor
|
|
5
5
|
from uncountable.core.client import Client
|
|
6
6
|
from uncountable.integration.telemetry import JobLogger
|
|
7
|
+
from uncountable.types import webhook_job_t
|
|
7
8
|
from uncountable.types.job_definition_t import JobDefinition, JobResult, ProfileMetadata
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
@dataclass
|
|
11
|
+
@dataclass(kw_only=True)
|
|
11
12
|
class JobArgumentsBase:
|
|
12
13
|
job_definition: JobDefinition
|
|
13
14
|
profile_metadata: ProfileMetadata
|
|
@@ -16,27 +17,44 @@ class JobArgumentsBase:
|
|
|
16
17
|
logger: JobLogger
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
@dataclass
|
|
20
|
+
@dataclass(kw_only=True)
|
|
20
21
|
class CronJobArguments(JobArgumentsBase):
|
|
21
|
-
# can imagine passing additional data such as in the sftp or webhook cases
|
|
22
22
|
pass
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
@dataclass(kw_only=True)
|
|
26
|
+
class WebhookJobArguments(JobArgumentsBase):
|
|
27
|
+
payload: webhook_job_t.WebhookEventBody
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
JobArguments = CronJobArguments | WebhookJobArguments
|
|
26
31
|
|
|
27
32
|
|
|
28
33
|
class Job(ABC):
|
|
29
34
|
_unc_job_registered: bool = False
|
|
30
35
|
|
|
31
36
|
@abstractmethod
|
|
32
|
-
def
|
|
37
|
+
def run_outer(self, args: JobArguments) -> JobResult: ...
|
|
33
38
|
|
|
34
39
|
|
|
35
40
|
class CronJob(Job):
|
|
41
|
+
def run_outer(self, args: JobArguments) -> JobResult:
|
|
42
|
+
assert isinstance(args, CronJobArguments)
|
|
43
|
+
return self.run(args)
|
|
44
|
+
|
|
36
45
|
@abstractmethod
|
|
37
46
|
def run(self, args: CronJobArguments) -> JobResult: ...
|
|
38
47
|
|
|
39
48
|
|
|
49
|
+
class WebhookJob(Job):
|
|
50
|
+
def run_outer(self, args: JobArguments) -> JobResult:
|
|
51
|
+
assert isinstance(args, WebhookJobArguments)
|
|
52
|
+
return self.run(args)
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def run(self, args: WebhookJobArguments) -> JobResult: ...
|
|
56
|
+
|
|
57
|
+
|
|
40
58
|
def register_job(cls: type[Job]) -> type[Job]:
|
|
41
59
|
cls._unc_job_registered = True
|
|
42
60
|
return cls
|
|
@@ -19,6 +19,7 @@ from uncountable.types.job_definition_t import (
|
|
|
19
19
|
CronJobDefinition,
|
|
20
20
|
JobDefinition,
|
|
21
21
|
ProfileMetadata,
|
|
22
|
+
WebhookJobDefinition,
|
|
22
23
|
)
|
|
23
24
|
|
|
24
25
|
_MAX_APSCHEDULER_CONCURRENT_JOBS = 1
|
|
@@ -97,8 +98,10 @@ class IntegrationServer:
|
|
|
97
98
|
kwargs=job_kwargs,
|
|
98
99
|
**job_opts,
|
|
99
100
|
)
|
|
101
|
+
case WebhookJobDefinition():
|
|
102
|
+
pass
|
|
100
103
|
case _:
|
|
101
|
-
assert_never(job_defn
|
|
104
|
+
assert_never(job_defn)
|
|
102
105
|
|
|
103
106
|
def serve_forever(self) -> None:
|
|
104
107
|
signal.pause()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
import os
|
|
3
3
|
import time
|
|
4
|
+
import typing
|
|
4
5
|
from contextlib import contextmanager
|
|
5
6
|
from enum import StrEnum
|
|
6
7
|
from typing import Generator, assert_never, cast
|
|
@@ -16,9 +17,13 @@ from opentelemetry.sdk.trace import TracerProvider
|
|
|
16
17
|
from opentelemetry.sdk.trace.export import (
|
|
17
18
|
SimpleSpanProcessor,
|
|
18
19
|
)
|
|
19
|
-
from opentelemetry.trace import DEFAULT_TRACE_OPTIONS, Tracer
|
|
20
|
+
from opentelemetry.trace import DEFAULT_TRACE_OPTIONS, Span, Tracer
|
|
20
21
|
|
|
21
|
-
from uncountable.core.
|
|
22
|
+
from uncountable.core.environment import (
|
|
23
|
+
get_integration_env,
|
|
24
|
+
get_otel_enabled,
|
|
25
|
+
get_version,
|
|
26
|
+
)
|
|
22
27
|
from uncountable.types import base_t, job_definition_t
|
|
23
28
|
|
|
24
29
|
|
|
@@ -35,7 +40,7 @@ def get_otel_resource() -> Resource:
|
|
|
35
40
|
unc_version = os.environ.get("UNC_VERSION")
|
|
36
41
|
if unc_version is not None:
|
|
37
42
|
attributes["service.version"] = unc_version
|
|
38
|
-
unc_env =
|
|
43
|
+
unc_env = get_integration_env()
|
|
39
44
|
if unc_env is not None:
|
|
40
45
|
attributes["deployment.environment"] = unc_env
|
|
41
46
|
resource = Resource.create(attributes=_cast_attributes(attributes))
|
|
@@ -45,7 +50,8 @@ def get_otel_resource() -> Resource:
|
|
|
45
50
|
@functools.cache
|
|
46
51
|
def get_otel_tracer() -> Tracer:
|
|
47
52
|
provider = TracerProvider(resource=get_otel_resource())
|
|
48
|
-
|
|
53
|
+
if get_otel_enabled():
|
|
54
|
+
provider.add_span_processor(SimpleSpanProcessor(OTLPSpanExporter()))
|
|
49
55
|
trace.set_tracer_provider(provider)
|
|
50
56
|
return provider.get_tracer("integration.telemetry")
|
|
51
57
|
|
|
@@ -54,7 +60,8 @@ def get_otel_tracer() -> Tracer:
|
|
|
54
60
|
def get_otel_logger() -> OTELLogger:
|
|
55
61
|
provider = LoggerProvider(resource=get_otel_resource())
|
|
56
62
|
provider.add_log_record_processor(BatchLogRecordProcessor(ConsoleLogExporter()))
|
|
57
|
-
|
|
63
|
+
if get_otel_enabled():
|
|
64
|
+
provider.add_log_record_processor(BatchLogRecordProcessor(OTLPLogExporter()))
|
|
58
65
|
_logs.set_logger_provider(provider)
|
|
59
66
|
return provider.get_logger("integration.telemetry")
|
|
60
67
|
|
|
@@ -66,8 +73,19 @@ class LogSeverity(StrEnum):
|
|
|
66
73
|
|
|
67
74
|
|
|
68
75
|
class Logger:
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
current_span: Span | None = None
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def current_span_id(self) -> int | None:
|
|
80
|
+
if self.current_span is None:
|
|
81
|
+
return None
|
|
82
|
+
return self.current_span.get_span_context().span_id
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def current_trace_id(self) -> int | None:
|
|
86
|
+
if self.current_span is None:
|
|
87
|
+
return None
|
|
88
|
+
return self.current_span.get_span_context().trace_id
|
|
71
89
|
|
|
72
90
|
def _patch_attributes(self, attributes: Attributes | None) -> Attributes:
|
|
73
91
|
return attributes or {}
|
|
@@ -84,6 +102,8 @@ class Logger:
|
|
|
84
102
|
span_id=self.current_span_id,
|
|
85
103
|
trace_id=self.current_trace_id,
|
|
86
104
|
trace_flags=DEFAULT_TRACE_OPTIONS,
|
|
105
|
+
severity_number=_logs.SeverityNumber.UNSPECIFIED,
|
|
106
|
+
resource=get_otel_resource(),
|
|
87
107
|
)
|
|
88
108
|
otel_logger.emit(log_record)
|
|
89
109
|
|
|
@@ -104,6 +124,33 @@ class Logger:
|
|
|
104
124
|
message=message, severity=LogSeverity.ERROR, attributes=attributes
|
|
105
125
|
)
|
|
106
126
|
|
|
127
|
+
def log_exception(
|
|
128
|
+
self,
|
|
129
|
+
exception: BaseException,
|
|
130
|
+
*,
|
|
131
|
+
message: str | None = None,
|
|
132
|
+
attributes: Attributes | None = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
patched_attributes = self._patch_attributes(attributes)
|
|
135
|
+
if self.current_span is not None:
|
|
136
|
+
self.current_span.record_exception(
|
|
137
|
+
exception=exception, attributes=patched_attributes
|
|
138
|
+
)
|
|
139
|
+
self.log_error(
|
|
140
|
+
message=f"error: {message}\nexception: {exception}",
|
|
141
|
+
attributes=patched_attributes,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
@contextmanager
|
|
145
|
+
def push_scope(
|
|
146
|
+
self, scope_name: str, *, attributes: Attributes | None = None
|
|
147
|
+
) -> Generator[typing.Self, None, None]:
|
|
148
|
+
with get_otel_tracer().start_as_current_span(
|
|
149
|
+
scope_name, attributes=self._patch_attributes(attributes)
|
|
150
|
+
) as span:
|
|
151
|
+
self.current_span = span
|
|
152
|
+
yield self
|
|
153
|
+
|
|
107
154
|
|
|
108
155
|
class JobLogger(Logger):
|
|
109
156
|
def __init__(
|
|
@@ -129,6 +176,8 @@ class JobLogger(Logger):
|
|
|
129
176
|
patched_attributes["job.definition.cron_spec"] = (
|
|
130
177
|
self.job_definition.cron_spec
|
|
131
178
|
)
|
|
179
|
+
case job_definition_t.WebhookJobDefinition():
|
|
180
|
+
pass
|
|
132
181
|
case _:
|
|
133
182
|
assert_never(self.job_definition)
|
|
134
183
|
patched_attributes["job.definition.executor.type"] = (
|
|
@@ -146,14 +195,3 @@ class JobLogger(Logger):
|
|
|
146
195
|
case _:
|
|
147
196
|
assert_never(self.job_definition.executor)
|
|
148
197
|
return _cast_attributes(patched_attributes)
|
|
149
|
-
|
|
150
|
-
@contextmanager
|
|
151
|
-
def push_scope(
|
|
152
|
-
self, scope_name: str, *, attributes: Attributes | None = None
|
|
153
|
-
) -> Generator["JobLogger", None, None]:
|
|
154
|
-
with get_otel_tracer().start_as_current_span(
|
|
155
|
-
scope_name, attributes=self._patch_attributes(attributes)
|
|
156
|
-
) as span:
|
|
157
|
-
self.current_span_id = span.get_span_context().span_id
|
|
158
|
-
self.current_trace_id = span.get_span_context().trace_id
|
|
159
|
-
yield self
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import hmac
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
import flask
|
|
5
|
+
import simplejson
|
|
6
|
+
from flask.typing import ResponseReturnValue
|
|
7
|
+
from flask.wrappers import Response
|
|
8
|
+
from uncountable.core.async_batch import AsyncBatchProcessor
|
|
9
|
+
from uncountable.core.environment import get_integration_env, get_webhook_server_port
|
|
10
|
+
from uncountable.integration.construct_client import construct_uncountable_client
|
|
11
|
+
from uncountable.integration.executors.executors import execute_job
|
|
12
|
+
from uncountable.integration.job import WebhookJobArguments
|
|
13
|
+
from uncountable.integration.scan_profiles import load_profiles
|
|
14
|
+
from uncountable.integration.secret_retrieval.retrieve_secret import retrieve_secret
|
|
15
|
+
from uncountable.integration.telemetry import JobLogger, Logger
|
|
16
|
+
from uncountable.types import job_definition_t, webhook_job_t
|
|
17
|
+
|
|
18
|
+
from pkgs.argument_parser import CachedParser
|
|
19
|
+
|
|
20
|
+
app = flask.Flask(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(kw_only=True)
|
|
24
|
+
class WebhookResponse:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
webhook_payload_parser = CachedParser(webhook_job_t.WebhookEventBody)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class WebhookException(BaseException):
|
|
32
|
+
error_code: int
|
|
33
|
+
message: str
|
|
34
|
+
|
|
35
|
+
def __init__(self, *, error_code: int, message: str) -> None:
|
|
36
|
+
self.error_code = error_code
|
|
37
|
+
self.message = message
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def payload_failed_signature() -> "WebhookException":
|
|
41
|
+
return WebhookException(
|
|
42
|
+
error_code=401, message="webhook payload did not match signature"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def no_signature_passed() -> "WebhookException":
|
|
47
|
+
return WebhookException(error_code=400, message="missing signature")
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def body_parse_error() -> "WebhookException":
|
|
51
|
+
return WebhookException(error_code=400, message="body parse error")
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def unknown_error() -> "WebhookException":
|
|
55
|
+
return WebhookException(error_code=500, message="internal server error")
|
|
56
|
+
|
|
57
|
+
def __str__(self) -> str:
|
|
58
|
+
return f"[{self.error_code}]: {self.message}"
|
|
59
|
+
|
|
60
|
+
def make_error_response(self) -> Response:
|
|
61
|
+
return Response(
|
|
62
|
+
status=self.error_code, response={"error": {"message": str(self)}}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _parse_webhook_payload(
|
|
67
|
+
*, raw_request_body: bytes, signature_key: str, passed_signature: str
|
|
68
|
+
) -> webhook_job_t.WebhookEventBody:
|
|
69
|
+
request_body_signature = hmac.new(
|
|
70
|
+
signature_key.encode("utf-8"), msg=raw_request_body, digestmod="sha256"
|
|
71
|
+
).hexdigest()
|
|
72
|
+
|
|
73
|
+
if request_body_signature != passed_signature:
|
|
74
|
+
raise WebhookException.payload_failed_signature()
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
request_body = simplejson.loads(raw_request_body.decode())
|
|
78
|
+
return webhook_payload_parser.parse_api(request_body)
|
|
79
|
+
except (simplejson.JSONDecodeError, ValueError) as e:
|
|
80
|
+
raise WebhookException.body_parse_error() from e
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def register_route(
|
|
84
|
+
*,
|
|
85
|
+
server_logger: Logger,
|
|
86
|
+
profile_meta: job_definition_t.ProfileMetadata,
|
|
87
|
+
job: job_definition_t.WebhookJobDefinition,
|
|
88
|
+
) -> None:
|
|
89
|
+
route = f"/{profile_meta.name}/{job.id}"
|
|
90
|
+
|
|
91
|
+
@app.route(route, methods=["POST"])
|
|
92
|
+
def handle_webhook() -> ResponseReturnValue:
|
|
93
|
+
with server_logger.push_scope(route):
|
|
94
|
+
try:
|
|
95
|
+
signature_key = retrieve_secret(
|
|
96
|
+
profile_metadata=profile_meta,
|
|
97
|
+
secret_retrieval=job.signature_key_secret,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
passed_signature = flask.request.headers.get(
|
|
101
|
+
"Uncountable-Webhook-Signature"
|
|
102
|
+
)
|
|
103
|
+
if passed_signature is None:
|
|
104
|
+
raise WebhookException.no_signature_passed()
|
|
105
|
+
|
|
106
|
+
webhook_payload = _parse_webhook_payload(
|
|
107
|
+
raw_request_body=flask.request.data,
|
|
108
|
+
signature_key=signature_key,
|
|
109
|
+
passed_signature=passed_signature,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
job_logger = JobLogger(
|
|
113
|
+
profile_metadata=profile_metadata, job_definition=job
|
|
114
|
+
)
|
|
115
|
+
client = construct_uncountable_client(
|
|
116
|
+
profile_meta=profile_meta, job_logger=job_logger
|
|
117
|
+
)
|
|
118
|
+
execute_job(
|
|
119
|
+
job_definition=job,
|
|
120
|
+
profile_metadata=profile_meta,
|
|
121
|
+
args=WebhookJobArguments(
|
|
122
|
+
job_definition=job,
|
|
123
|
+
profile_metadata=profile_metadata,
|
|
124
|
+
client=client,
|
|
125
|
+
batch_processor=AsyncBatchProcessor(client=client),
|
|
126
|
+
logger=job_logger,
|
|
127
|
+
payload=webhook_payload,
|
|
128
|
+
),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return flask.jsonify(WebhookResponse())
|
|
132
|
+
except WebhookException as e:
|
|
133
|
+
server_logger.log_exception(e)
|
|
134
|
+
return e.make_error_response()
|
|
135
|
+
except Exception as e:
|
|
136
|
+
server_logger.log_exception(e)
|
|
137
|
+
return WebhookException.unknown_error().make_error_response()
|
|
138
|
+
|
|
139
|
+
server_logger.log_info(f"job {job.id} webhook registered at: {route}")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
profiles = load_profiles()
|
|
143
|
+
for profile in profiles:
|
|
144
|
+
server_logger = Logger()
|
|
145
|
+
profile_metadata = job_definition_t.ProfileMetadata(
|
|
146
|
+
name=profile.name,
|
|
147
|
+
auth_retrieval=profile.definition.auth_retrieval,
|
|
148
|
+
base_url=profile.definition.base_url,
|
|
149
|
+
client_options=profile.definition.client_options,
|
|
150
|
+
)
|
|
151
|
+
for job in profile.definition.jobs:
|
|
152
|
+
if isinstance(job, job_definition_t.WebhookJobDefinition):
|
|
153
|
+
register_route(
|
|
154
|
+
server_logger=server_logger, profile_meta=profile_metadata, job=job
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
app.run(
|
|
160
|
+
host="0.0.0.0",
|
|
161
|
+
port=get_webhook_server_port(),
|
|
162
|
+
debug=get_integration_env() == "playground",
|
|
163
|
+
exclude_patterns=[],
|
|
164
|
+
)
|
uncountable/types/__init__.py
CHANGED
|
@@ -97,6 +97,7 @@ from .api.recipes import unlock_recipes as unlock_recipes_t
|
|
|
97
97
|
from .api.material_families import update_entity_material_families as update_entity_material_families_t
|
|
98
98
|
from .api.field_options import upsert_field_options as upsert_field_options_t
|
|
99
99
|
from . import users_t as users_t
|
|
100
|
+
from . import webhook_job_t as webhook_job_t
|
|
100
101
|
from . import workflows_t as workflows_t
|
|
101
102
|
|
|
102
103
|
|
|
@@ -195,5 +196,6 @@ __all__: list[str] = [
|
|
|
195
196
|
"update_entity_material_families_t",
|
|
196
197
|
"upsert_field_options_t",
|
|
197
198
|
"users_t",
|
|
199
|
+
"webhook_job_t",
|
|
198
200
|
"workflows_t",
|
|
199
201
|
]
|
uncountable/types/entity_t.py
CHANGED
|
@@ -25,6 +25,7 @@ __all__: list[str] = [
|
|
|
25
25
|
"analytical_method_category": "Analytical Method Category",
|
|
26
26
|
"annotation_type": "Annotation Type",
|
|
27
27
|
"approval": "Approval",
|
|
28
|
+
"async_job": "Async Job",
|
|
28
29
|
"calculation": "Calculation",
|
|
29
30
|
"calendar": "Calendar",
|
|
30
31
|
"calendar_event": "Calendar Event",
|
|
@@ -176,6 +177,7 @@ class EntityType(StrEnum):
|
|
|
176
177
|
ANALYTICAL_METHOD_CATEGORY = "analytical_method_category"
|
|
177
178
|
ANNOTATION_TYPE = "annotation_type"
|
|
178
179
|
APPROVAL = "approval"
|
|
180
|
+
ASYNC_JOB = "async_job"
|
|
179
181
|
CALCULATION = "calculation"
|
|
180
182
|
CALENDAR = "calendar"
|
|
181
183
|
CALENDAR_EVENT = "calendar_event"
|
|
@@ -191,11 +191,12 @@ class WebhookJobDefinition(JobDefinitionBase):
|
|
|
191
191
|
|
|
192
192
|
# DO NOT MODIFY -- This file is generated by type_spec
|
|
193
193
|
JobDefinition = typing.Annotated[
|
|
194
|
-
typing.Union[CronJobDefinition],
|
|
194
|
+
typing.Union[CronJobDefinition, WebhookJobDefinition],
|
|
195
195
|
serial_union_annotation(
|
|
196
196
|
discriminator="type",
|
|
197
197
|
discriminator_map={
|
|
198
198
|
"cron": CronJobDefinition,
|
|
199
|
+
"webhook": WebhookJobDefinition,
|
|
199
200
|
},
|
|
200
201
|
),
|
|
201
202
|
]
|
|
@@ -0,0 +1,9 @@
|
|
|
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 .webhook_job_t import WebhookEventPayload as WebhookEventPayload
|
|
8
|
+
from .webhook_job_t import WebhookEventBody as WebhookEventBody
|
|
9
|
+
# DO NOT MODIFY -- This file is generated by type_spec
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
import dataclasses
|
|
11
|
+
from pkgs.serialization import serial_class
|
|
12
|
+
from . import base_t
|
|
13
|
+
|
|
14
|
+
__all__: list[str] = [
|
|
15
|
+
"WebhookEventBody",
|
|
16
|
+
"WebhookEventPayload",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# DO NOT MODIFY -- This file is generated by type_spec
|
|
21
|
+
@serial_class(
|
|
22
|
+
unconverted_values={"data"},
|
|
23
|
+
)
|
|
24
|
+
@dataclasses.dataclass(kw_only=True)
|
|
25
|
+
class WebhookEventPayload:
|
|
26
|
+
data: dict[str, base_t.JsonValue]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# DO NOT MODIFY -- This file is generated by type_spec
|
|
30
|
+
@dataclasses.dataclass(kw_only=True)
|
|
31
|
+
class WebhookEventBody(WebhookEventPayload):
|
|
32
|
+
event_id: str
|
|
33
|
+
# DO NOT MODIFY -- This file is generated by type_spec
|
uncountable/core/version.py
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import functools
|
|
2
|
-
from importlib.metadata import PackageNotFoundError, version
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
@functools.cache
|
|
6
|
-
def get_version() -> str:
|
|
7
|
-
try:
|
|
8
|
-
version_str = version("UncountablePythonSDK")
|
|
9
|
-
except PackageNotFoundError:
|
|
10
|
-
version_str = "unknown"
|
|
11
|
-
return version_str
|
|
File without changes
|
{UncountablePythonSDK-0.0.59.dist-info → UncountablePythonSDK-0.0.60.dist-info}/top_level.txt
RENAMED
|
File without changes
|