intuned-runtime 1.0.0__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.
- cli/__init__.py +45 -0
- cli/commands/__init__.py +25 -0
- cli/commands/ai_source/__init__.py +4 -0
- cli/commands/ai_source/ai_source.py +10 -0
- cli/commands/ai_source/deploy.py +64 -0
- cli/commands/browser/__init__.py +3 -0
- cli/commands/browser/save_state.py +32 -0
- cli/commands/init.py +127 -0
- cli/commands/project/__init__.py +20 -0
- cli/commands/project/auth_session/__init__.py +5 -0
- cli/commands/project/auth_session/check.py +118 -0
- cli/commands/project/auth_session/create.py +96 -0
- cli/commands/project/auth_session/load.py +39 -0
- cli/commands/project/project.py +10 -0
- cli/commands/project/run.py +340 -0
- cli/commands/project/run_interface.py +265 -0
- cli/commands/project/type_check.py +86 -0
- cli/commands/project/upgrade.py +92 -0
- cli/commands/publish_packages.py +264 -0
- cli/logger.py +19 -0
- cli/utils/ai_source_project.py +31 -0
- cli/utils/code_tree.py +83 -0
- cli/utils/run_apis.py +147 -0
- cli/utils/unix_socket.py +55 -0
- intuned_runtime-1.0.0.dist-info/LICENSE +42 -0
- intuned_runtime-1.0.0.dist-info/METADATA +113 -0
- intuned_runtime-1.0.0.dist-info/RECORD +58 -0
- intuned_runtime-1.0.0.dist-info/WHEEL +4 -0
- intuned_runtime-1.0.0.dist-info/entry_points.txt +3 -0
- runtime/__init__.py +3 -0
- runtime/backend_functions/__init__.py +5 -0
- runtime/backend_functions/_call_backend_function.py +86 -0
- runtime/backend_functions/get_auth_session_parameters.py +30 -0
- runtime/browser/__init__.py +3 -0
- runtime/browser/launch_chromium.py +212 -0
- runtime/browser/storage_state.py +106 -0
- runtime/context/__init__.py +5 -0
- runtime/context/context.py +51 -0
- runtime/env.py +13 -0
- runtime/errors/__init__.py +21 -0
- runtime/errors/auth_session_errors.py +9 -0
- runtime/errors/run_api_errors.py +120 -0
- runtime/errors/trace_errors.py +3 -0
- runtime/helpers/__init__.py +5 -0
- runtime/helpers/extend_payload.py +9 -0
- runtime/helpers/extend_timeout.py +13 -0
- runtime/helpers/get_auth_session_parameters.py +14 -0
- runtime/py.typed +0 -0
- runtime/run/__init__.py +3 -0
- runtime/run/intuned_settings.py +38 -0
- runtime/run/playwright_constructs.py +19 -0
- runtime/run/run_api.py +233 -0
- runtime/run/traces.py +36 -0
- runtime/types/__init__.py +15 -0
- runtime/types/payload.py +7 -0
- runtime/types/run_types.py +177 -0
- runtime_helpers/__init__.py +5 -0
- runtime_helpers/py.typed +0 -0
@@ -0,0 +1,340 @@
|
|
1
|
+
import asyncio
|
2
|
+
import functools
|
3
|
+
import json
|
4
|
+
import os
|
5
|
+
import sys
|
6
|
+
from collections import Counter
|
7
|
+
from datetime import datetime
|
8
|
+
from enum import StrEnum
|
9
|
+
from importlib import import_module
|
10
|
+
from typing import Any
|
11
|
+
from typing import cast
|
12
|
+
|
13
|
+
import arguably
|
14
|
+
from dotenv import find_dotenv
|
15
|
+
from dotenv import load_dotenv
|
16
|
+
from more_termcolor import bold # type: ignore
|
17
|
+
from more_termcolor import cyan # type: ignore
|
18
|
+
from more_termcolor import green # type: ignore
|
19
|
+
from more_termcolor import italic # type: ignore
|
20
|
+
from more_termcolor import on_blue # type: ignore
|
21
|
+
from more_termcolor import underline # type: ignore
|
22
|
+
from more_termcolor import yellow # type: ignore
|
23
|
+
|
24
|
+
from runtime.run.intuned_settings import load_intuned_settings
|
25
|
+
from runtime.types import Payload
|
26
|
+
from runtime.types.run_types import Auth
|
27
|
+
from runtime.types.run_types import StateSession
|
28
|
+
from runtime.types.run_types import StorageState
|
29
|
+
|
30
|
+
from ...utils.run_apis import run_api_for_cli
|
31
|
+
from ...utils.run_apis import RunResultData
|
32
|
+
|
33
|
+
|
34
|
+
class Mode(StrEnum):
|
35
|
+
full = "full"
|
36
|
+
single = "single"
|
37
|
+
sample = "sample"
|
38
|
+
ide = "ide"
|
39
|
+
|
40
|
+
|
41
|
+
@arguably.command # type: ignore
|
42
|
+
async def project__run(
|
43
|
+
*,
|
44
|
+
api_name: str = "default",
|
45
|
+
mode: Mode = Mode.sample,
|
46
|
+
params: str | None = None,
|
47
|
+
params_path: str | None = None,
|
48
|
+
sample_config_str: str | None = None,
|
49
|
+
concurrent: int = 1,
|
50
|
+
no_headless: bool | None = False,
|
51
|
+
cdp_address: str | None = None,
|
52
|
+
output_file_id: str | None = None,
|
53
|
+
auth_session_path: str | None = None,
|
54
|
+
auth_session_parameters: str | None = None,
|
55
|
+
):
|
56
|
+
"""
|
57
|
+
Runs the current project. Project must contain an "api" directory with API functions.
|
58
|
+
|
59
|
+
Args:
|
60
|
+
api_name (str): The name of the API to run.
|
61
|
+
mode (Mode): The mode in which to run the app.
|
62
|
+
Defaults to "sample".
|
63
|
+
Sample will run a sample of the extended payloads.
|
64
|
+
Full will also run all extended payloads.
|
65
|
+
Single will only run the specified API.
|
66
|
+
IDE will only run the specified API with changes to the outputs and files to fit running in the IDE.
|
67
|
+
params (str | None): Parameters to pass to the API as a JSON string.
|
68
|
+
params_path (str | None): Path to a JSON file containing parameters to pass to the API.
|
69
|
+
sample_config_str (str | None): [--sample-config] A JSON string where keys are API names and values are the number of times to run the API in sample mode.
|
70
|
+
no_headless (bool): Disable headless mode.
|
71
|
+
concurrent (int | None): [-c/--concurrent] Number of concurrent runs to allow.
|
72
|
+
cdp_address (str | None): Chrome DevTools Protocol address to connect to.
|
73
|
+
output_file_id (str | None): (IDE mode only) The output file id to save the result in
|
74
|
+
auth_session_path (str | None): Path to the auth session file.
|
75
|
+
auth_session_parameters (str | None): JSON string containing auth session parameters.
|
76
|
+
Returns:
|
77
|
+
None
|
78
|
+
|
79
|
+
"""
|
80
|
+
|
81
|
+
if sample_config_str and mode != Mode.sample:
|
82
|
+
raise ValueError("Cannot provide sample_config in non-sample mode")
|
83
|
+
if params and params_path:
|
84
|
+
raise ValueError("Cannot provide both params and params_path")
|
85
|
+
if params_path and not os.path.exists(params_path):
|
86
|
+
raise ValueError(f"params_path does not exist: {params_path}")
|
87
|
+
|
88
|
+
try:
|
89
|
+
if params:
|
90
|
+
params_json = json.loads(params) if params else None
|
91
|
+
elif params_path:
|
92
|
+
with open(params_path) as f:
|
93
|
+
params_json = json.load(f)
|
94
|
+
else:
|
95
|
+
params_json = None
|
96
|
+
except json.JSONDecodeError as e:
|
97
|
+
raise ValueError(f"Invalid JSON in params: {e}") from e
|
98
|
+
except FileNotFoundError as e:
|
99
|
+
raise ValueError(f"Invalid params_path: {e}") from e
|
100
|
+
|
101
|
+
auth_session: StorageState | None = None
|
102
|
+
try:
|
103
|
+
if auth_session_path:
|
104
|
+
with open(auth_session_path) as f:
|
105
|
+
auth_session = StorageState(**json.load(f))
|
106
|
+
except json.JSONDecodeError as e:
|
107
|
+
raise ValueError(f"Invalid JSON in auth session: {e}") from e
|
108
|
+
except FileNotFoundError as e:
|
109
|
+
raise ValueError(f"Invalid auth-session-path: {e}") from e
|
110
|
+
|
111
|
+
if concurrent <= 0:
|
112
|
+
raise ValueError("Concurrent must be greater than 0")
|
113
|
+
|
114
|
+
dotenv = find_dotenv(usecwd=True)
|
115
|
+
if dotenv:
|
116
|
+
load_dotenv(dotenv, override=True)
|
117
|
+
|
118
|
+
headless = not no_headless
|
119
|
+
|
120
|
+
api_to_run: Payload = {
|
121
|
+
"api": api_name,
|
122
|
+
"parameters": params_json or {},
|
123
|
+
}
|
124
|
+
|
125
|
+
timestamp: str = datetime.now().strftime("%Y-%m-%d_%H:%M")
|
126
|
+
run_results_dir = os.path.join("run_results", f"run_{mode}_{timestamp}")
|
127
|
+
n = 1
|
128
|
+
while os.path.exists(run_results_dir):
|
129
|
+
run_results_dir = os.path.join("run_results", f"run_{mode}_{timestamp}_{n}")
|
130
|
+
n += 1
|
131
|
+
os.makedirs(run_results_dir, exist_ok=True)
|
132
|
+
|
133
|
+
sys.path.append(os.path.join(os.getcwd()))
|
134
|
+
|
135
|
+
if auth_session_parameters:
|
136
|
+
try:
|
137
|
+
auth_session_parameters_json = json.loads(auth_session_parameters)
|
138
|
+
except json.JSONDecodeError as e:
|
139
|
+
raise ValueError(f"Invalid JSON in auth session parameters: {e}") from e
|
140
|
+
else:
|
141
|
+
auth_session_parameters_json = None
|
142
|
+
|
143
|
+
if mode == Mode.ide:
|
144
|
+
await run_api_for_ide_mode(
|
145
|
+
api_name=api_name,
|
146
|
+
params=params_json,
|
147
|
+
headless=headless,
|
148
|
+
cdp_address=cdp_address,
|
149
|
+
output_file_id=output_file_id,
|
150
|
+
session=auth_session,
|
151
|
+
auth_session_parameters=auth_session_parameters_json,
|
152
|
+
)
|
153
|
+
return
|
154
|
+
|
155
|
+
settings = await load_intuned_settings()
|
156
|
+
if auth_session is None and settings.auth_sessions.enabled:
|
157
|
+
raise ValueError("Auth session is required when auth sessions are enabled")
|
158
|
+
|
159
|
+
run_payloads_and_extend_with_configs = functools.partial(
|
160
|
+
_run_payloads_and_extend,
|
161
|
+
payload=api_to_run,
|
162
|
+
headless=headless,
|
163
|
+
concurrent=concurrent,
|
164
|
+
cdp_address=cdp_address,
|
165
|
+
session=auth_session,
|
166
|
+
auth_session_parameters=auth_session_parameters_json,
|
167
|
+
)
|
168
|
+
|
169
|
+
if mode == Mode.single:
|
170
|
+
print(bold("Running in single mode"))
|
171
|
+
api_runs = run_payloads_and_extend_with_configs(extend_config={api_name: 1})
|
172
|
+
elif mode == Mode.full:
|
173
|
+
print(bold("Running in full mode"))
|
174
|
+
api_runs = run_payloads_and_extend_with_configs(concurrent=concurrent)
|
175
|
+
elif mode == Mode.sample:
|
176
|
+
sample_config: dict[str, int] = (
|
177
|
+
json.loads(sample_config_str)
|
178
|
+
if sample_config_str
|
179
|
+
else {
|
180
|
+
"default": 1,
|
181
|
+
"list": 3,
|
182
|
+
"details": 15,
|
183
|
+
}
|
184
|
+
)
|
185
|
+
print(
|
186
|
+
bold("Running in sample mode with config:"),
|
187
|
+
", ".join([f"{cyan(k)}: {v}" for k, v in sample_config.items()]),
|
188
|
+
)
|
189
|
+
api_runs = run_payloads_and_extend_with_configs(extend_config=sample_config)
|
190
|
+
|
191
|
+
print(italic(f"Results will be saved in {cyan(os.path.abspath(run_results_dir))}"))
|
192
|
+
|
193
|
+
_check_intuned_sdk()
|
194
|
+
|
195
|
+
iteration = 1
|
196
|
+
async for result_data in api_runs:
|
197
|
+
prefix = f"{iteration}_" if mode != Mode.single else ""
|
198
|
+
success = result_data["output"]["success"]
|
199
|
+
run_api_name = result_data["input"]["api"]
|
200
|
+
with open(
|
201
|
+
os.path.join(run_results_dir, f"{prefix}{run_api_name}_{"success" if success else "fail"}.json"), "w"
|
202
|
+
) as f:
|
203
|
+
json.dump(result_data, f, indent=2)
|
204
|
+
iteration += 1
|
205
|
+
|
206
|
+
print(green(bold("🏁 Done")))
|
207
|
+
print(italic(f"Results saved in {cyan(os.path.abspath(run_results_dir))}"), flush=True)
|
208
|
+
|
209
|
+
|
210
|
+
def _check_intuned_sdk():
|
211
|
+
try:
|
212
|
+
import_module("intuned_sdk")
|
213
|
+
except:
|
214
|
+
print(yellow(f"Warning: {bold('intuned_sdk')} could not be imported!"))
|
215
|
+
|
216
|
+
|
217
|
+
async def _run_payloads_and_extend(
|
218
|
+
payload: Payload,
|
219
|
+
headless: bool,
|
220
|
+
extend_config: dict[str, int] | None = None,
|
221
|
+
concurrent: int = 1,
|
222
|
+
cdp_address: str | None = None,
|
223
|
+
session: StorageState | None = None,
|
224
|
+
auth_session_parameters: dict[str, Any] | None = None,
|
225
|
+
):
|
226
|
+
counter = Counter(extend_config)
|
227
|
+
payloads_lists = [[payload]]
|
228
|
+
|
229
|
+
tasks: set[asyncio.Task[RunResultData]] = set()
|
230
|
+
|
231
|
+
while len(payloads_lists) > 0 or len(tasks) > 0:
|
232
|
+
if len(payloads_lists) == 0 or len(tasks) >= concurrent:
|
233
|
+
done, tasks = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
|
234
|
+
for task in done:
|
235
|
+
result_data = task.result()
|
236
|
+
|
237
|
+
payloads_lists.append(result_data["output"].get("extended_payloads", []))
|
238
|
+
yield task.result()
|
239
|
+
payloads = payloads_lists.pop(0)
|
240
|
+
if len(payloads) == 0:
|
241
|
+
continue
|
242
|
+
payload_to_run = payloads.pop(0)
|
243
|
+
print(payload_to_run)
|
244
|
+
payloads_lists.append(payloads)
|
245
|
+
if extend_config:
|
246
|
+
if counter[payload_to_run["api"]] <= 0:
|
247
|
+
if payload_to_run["api"] in extend_config:
|
248
|
+
print(
|
249
|
+
italic(
|
250
|
+
f"Skipping {green(payload_to_run['api'])} " f"(> {extend_config[payload_to_run['api']]})"
|
251
|
+
)
|
252
|
+
)
|
253
|
+
continue
|
254
|
+
counter[payload_to_run["api"]] -= 1
|
255
|
+
task = asyncio.create_task(
|
256
|
+
run_api_for_cli(
|
257
|
+
payload_to_run,
|
258
|
+
headless=headless,
|
259
|
+
cdp_address=cdp_address,
|
260
|
+
auth=Auth(
|
261
|
+
run_check=True,
|
262
|
+
session=StateSession(state=session),
|
263
|
+
)
|
264
|
+
if session is not None
|
265
|
+
else None,
|
266
|
+
auth_session_parameters=auth_session_parameters,
|
267
|
+
)
|
268
|
+
)
|
269
|
+
tasks.add(task)
|
270
|
+
|
271
|
+
|
272
|
+
async def run_api_for_ide_mode(
|
273
|
+
*,
|
274
|
+
api_name: str,
|
275
|
+
params: Any,
|
276
|
+
headless: bool,
|
277
|
+
cdp_address: str | None,
|
278
|
+
output_file_id: str | None,
|
279
|
+
session: StorageState | None = None,
|
280
|
+
auth_session_parameters: dict[str, Any] | None = None,
|
281
|
+
):
|
282
|
+
print(bold(f"Running {green(api_name)}"))
|
283
|
+
result_data = await run_api_for_cli(
|
284
|
+
{
|
285
|
+
"api": api_name,
|
286
|
+
"parameters": params,
|
287
|
+
},
|
288
|
+
headless=headless,
|
289
|
+
cdp_address=cdp_address,
|
290
|
+
print_output=False,
|
291
|
+
auth=Auth(
|
292
|
+
session=StateSession(
|
293
|
+
state=session,
|
294
|
+
),
|
295
|
+
run_check=False,
|
296
|
+
)
|
297
|
+
if session is not None
|
298
|
+
else None,
|
299
|
+
auth_session_parameters=auth_session_parameters,
|
300
|
+
)
|
301
|
+
|
302
|
+
if not result_data["output"]["success"]:
|
303
|
+
error = cast(list[str], result_data["output"].get("error", []))
|
304
|
+
print("\n".join(error[1:]))
|
305
|
+
sys.exit(1)
|
306
|
+
|
307
|
+
result = result_data["output"].get("result")
|
308
|
+
|
309
|
+
is_response_json = isinstance(result, dict)
|
310
|
+
if is_response_json:
|
311
|
+
result = cast(dict[Any, Any], result)
|
312
|
+
has_more_than_5_keys = len(result) > 5
|
313
|
+
has_nested_json = any(isinstance(v, dict) for v in result.values())
|
314
|
+
should_write_to_file = has_more_than_5_keys or has_nested_json
|
315
|
+
elif isinstance(result, list):
|
316
|
+
should_write_to_file = True
|
317
|
+
else:
|
318
|
+
should_write_to_file = False
|
319
|
+
|
320
|
+
results_dir = "/tmp/run-results"
|
321
|
+
if should_write_to_file and output_file_id is not None:
|
322
|
+
print(underline(on_blue(f"Click to Open: Results saved (Run: {output_file_id})")))
|
323
|
+
|
324
|
+
os.makedirs(results_dir, exist_ok=True)
|
325
|
+
with open(os.path.join(results_dir, f"{output_file_id}.json"), "w") as f:
|
326
|
+
json.dump(result_data, f, indent=2)
|
327
|
+
else:
|
328
|
+
print(bold("📦 Result:"), green(json.dumps(result) if isinstance(result, (dict, list)) else str(result)))
|
329
|
+
|
330
|
+
extended_payloads = result_data["output"].get("extended_payloads", [])
|
331
|
+
has_payloads_to_append = len(extended_payloads) > 0
|
332
|
+
|
333
|
+
if has_payloads_to_append and output_file_id is not None:
|
334
|
+
os.makedirs(results_dir, exist_ok=True)
|
335
|
+
print(underline(on_blue(f"Click to Open: payloads to append (Run: {output_file_id})")))
|
336
|
+
with open(os.path.join(results_dir, f"{output_file_id}-payloads-to-append.json"), "w") as f:
|
337
|
+
json.dump(extended_payloads, f, indent=2)
|
338
|
+
elif has_payloads_to_append:
|
339
|
+
print(bold("➕ Extended payloads:"), green(str(extended_payloads)))
|
340
|
+
print("This will only take effect if you run within a job or queue.")
|
@@ -0,0 +1,265 @@
|
|
1
|
+
import asyncio
|
2
|
+
import os
|
3
|
+
import signal
|
4
|
+
import socket
|
5
|
+
import time
|
6
|
+
from collections.abc import AsyncGenerator
|
7
|
+
from typing import Any
|
8
|
+
from typing import cast
|
9
|
+
from typing import Literal
|
10
|
+
|
11
|
+
import arguably
|
12
|
+
from pydantic import Field
|
13
|
+
from pydantic import ValidationError
|
14
|
+
|
15
|
+
from runtime.backend_functions import get_auth_session_parameters
|
16
|
+
from runtime.context import IntunedContext
|
17
|
+
from runtime.errors.run_api_errors import InternalInvalidInputError
|
18
|
+
from runtime.errors.run_api_errors import RunApiError
|
19
|
+
from runtime.run.run_api import import_function_from_api_dir
|
20
|
+
from runtime.run.run_api import run_api
|
21
|
+
from runtime.types.run_types import CamelBaseModel
|
22
|
+
from runtime.types.run_types import RunApiParameters
|
23
|
+
from runtime.types.run_types import RunAutomationSuccessResult
|
24
|
+
|
25
|
+
from ...utils.unix_socket import JSONUnixSocket
|
26
|
+
|
27
|
+
throttle_time = 60
|
28
|
+
|
29
|
+
|
30
|
+
class StartMessage(CamelBaseModel):
|
31
|
+
type: Literal["start"] = "start"
|
32
|
+
parameters: RunApiParameters
|
33
|
+
|
34
|
+
|
35
|
+
class NextMessageParameters(CamelBaseModel):
|
36
|
+
value: str
|
37
|
+
|
38
|
+
|
39
|
+
class NextMessage(CamelBaseModel):
|
40
|
+
type: Literal["next"] = "next"
|
41
|
+
parameters: NextMessageParameters
|
42
|
+
|
43
|
+
|
44
|
+
class AbortMessage(CamelBaseModel):
|
45
|
+
type: Literal["abort"] = "abort"
|
46
|
+
parameters: dict[str, Any] = {}
|
47
|
+
|
48
|
+
|
49
|
+
class TokenUpdateMessageParameters(CamelBaseModel):
|
50
|
+
functionsToken: str
|
51
|
+
|
52
|
+
|
53
|
+
class TokenUpdateMessage(CamelBaseModel):
|
54
|
+
type: Literal["tokenUpdate"] = "tokenUpdate"
|
55
|
+
parameters: TokenUpdateMessageParameters
|
56
|
+
|
57
|
+
|
58
|
+
class PingMessage(CamelBaseModel):
|
59
|
+
type: Literal["ping"] = "ping"
|
60
|
+
parameters: dict[str, Any] = {}
|
61
|
+
|
62
|
+
|
63
|
+
Message = StartMessage | NextMessage | AbortMessage | TokenUpdateMessage | PingMessage
|
64
|
+
|
65
|
+
|
66
|
+
class MessageWrapper(CamelBaseModel):
|
67
|
+
message: Message = Field(
|
68
|
+
discriminator="type",
|
69
|
+
)
|
70
|
+
|
71
|
+
|
72
|
+
@arguably.command # type: ignore
|
73
|
+
async def project__run_interface(
|
74
|
+
socket_path: str,
|
75
|
+
):
|
76
|
+
"""
|
77
|
+
Runs the current project. Project must contain an "api" directory with API functions.
|
78
|
+
|
79
|
+
Args:
|
80
|
+
socket_path (str): Path to the socket file.
|
81
|
+
|
82
|
+
"""
|
83
|
+
|
84
|
+
# create unix socket client of type socket.socket
|
85
|
+
if not socket_path:
|
86
|
+
raise Exception("socket_path is required")
|
87
|
+
|
88
|
+
timeout_timestamp = time.time()
|
89
|
+
client = UDASClient(socket_path)
|
90
|
+
connected = await client.connect()
|
91
|
+
if not connected:
|
92
|
+
raise Exception("Failed to connect to UDAS")
|
93
|
+
|
94
|
+
run_api_task: asyncio.Task[RunAutomationSuccessResult] | None = cast(
|
95
|
+
asyncio.Task[RunAutomationSuccessResult] | None, None
|
96
|
+
)
|
97
|
+
|
98
|
+
def done(exitCode: int = 0):
|
99
|
+
client.close()
|
100
|
+
exit(exitCode)
|
101
|
+
|
102
|
+
def interrupt_signal_handler():
|
103
|
+
async def _impl():
|
104
|
+
if run_api_task is not None:
|
105
|
+
run_api_task.cancel()
|
106
|
+
# wait for graceful exit, if not, force exit
|
107
|
+
await asyncio.sleep(60)
|
108
|
+
done(1)
|
109
|
+
|
110
|
+
asyncio.create_task(_impl())
|
111
|
+
|
112
|
+
loop = asyncio.get_event_loop()
|
113
|
+
loop.add_signal_handler(signal.SIGTERM, interrupt_signal_handler)
|
114
|
+
loop.add_signal_handler(signal.SIGINT, interrupt_signal_handler)
|
115
|
+
|
116
|
+
messages_generator = client.receive_messages()
|
117
|
+
|
118
|
+
async def receive_messages():
|
119
|
+
message: Any = None
|
120
|
+
try:
|
121
|
+
message = await messages_generator.__anext__()
|
122
|
+
validated_message = MessageWrapper(message=message)
|
123
|
+
return validated_message.message
|
124
|
+
except StopAsyncIteration:
|
125
|
+
return None
|
126
|
+
except ValidationError as e:
|
127
|
+
print("Validation error", message, e)
|
128
|
+
return InternalInvalidInputError(
|
129
|
+
"Invalid input", {key: str(value) for key, value in e.__dict__.items() if not key.startswith("_")}
|
130
|
+
)
|
131
|
+
|
132
|
+
run_api_task: asyncio.Task[RunAutomationSuccessResult] | None = cast(
|
133
|
+
asyncio.Task[RunAutomationSuccessResult] | None, None
|
134
|
+
)
|
135
|
+
|
136
|
+
async def handle_message(message: Message):
|
137
|
+
nonlocal run_api_task
|
138
|
+
if message.type == "start":
|
139
|
+
|
140
|
+
async def extend_timeout():
|
141
|
+
nonlocal timeout_timestamp
|
142
|
+
if time.time() - timeout_timestamp < throttle_time:
|
143
|
+
return
|
144
|
+
timeout_timestamp = time.time()
|
145
|
+
await client.send_message({"type": "extend"})
|
146
|
+
|
147
|
+
IntunedContext.current().functions_token = message.parameters.functions_token
|
148
|
+
IntunedContext.current().extend_timeout = extend_timeout
|
149
|
+
IntunedContext.current().get_auth_session_parameters = get_auth_session_parameters
|
150
|
+
IntunedContext.current().run_context = message.parameters.context
|
151
|
+
run_api_task = asyncio.create_task(
|
152
|
+
run_api(
|
153
|
+
message.parameters,
|
154
|
+
import_function=lambda file_path, automation_name=None: import_function_from_api_dir(
|
155
|
+
automation_function_name=automation_name, file_path=file_path, base_dir=os.getcwd()
|
156
|
+
),
|
157
|
+
),
|
158
|
+
)
|
159
|
+
return
|
160
|
+
|
161
|
+
elif message.type == "abort":
|
162
|
+
if run_api_task is not None:
|
163
|
+
run_api_task.cancel()
|
164
|
+
return
|
165
|
+
elif message.type == "tokenUpdate":
|
166
|
+
IntunedContext.current().functions_token = message.parameters.functionsToken
|
167
|
+
return
|
168
|
+
else:
|
169
|
+
# todo handle this case?
|
170
|
+
raise NotImplementedError()
|
171
|
+
|
172
|
+
receive_messages_task = asyncio.create_task(receive_messages())
|
173
|
+
|
174
|
+
while True:
|
175
|
+
tasks: list[asyncio.Task[Message | RunApiError | None] | asyncio.Task[RunAutomationSuccessResult]] = [
|
176
|
+
receive_messages_task,
|
177
|
+
]
|
178
|
+
if run_api_task is not None:
|
179
|
+
tasks.append(run_api_task)
|
180
|
+
message_or_result, _ = await asyncio.wait(
|
181
|
+
tasks,
|
182
|
+
return_when=asyncio.FIRST_COMPLETED,
|
183
|
+
)
|
184
|
+
if message_or_result.pop() == receive_messages_task:
|
185
|
+
message = await receive_messages_task
|
186
|
+
if message is None:
|
187
|
+
if run_api_task is not None and not run_api_task.done():
|
188
|
+
run_api_task.cancel()
|
189
|
+
break
|
190
|
+
if isinstance(message, RunApiError):
|
191
|
+
await client.send_message({"type": "done", "success": False, "result": message.json})
|
192
|
+
break
|
193
|
+
if message.type == "ping":
|
194
|
+
await client.send_message({"type": "pong"})
|
195
|
+
break
|
196
|
+
await handle_message(message)
|
197
|
+
receive_messages_task = asyncio.create_task(receive_messages())
|
198
|
+
continue
|
199
|
+
|
200
|
+
if run_api_task is None:
|
201
|
+
continue
|
202
|
+
|
203
|
+
try:
|
204
|
+
result = await run_api_task # type: ignore
|
205
|
+
await client.send_message(
|
206
|
+
{
|
207
|
+
"type": "done",
|
208
|
+
"success": True,
|
209
|
+
"result": {
|
210
|
+
"result": result.result,
|
211
|
+
"session": result.session.model_dump(by_alias=True) if result.session else None,
|
212
|
+
"extendedPayloads": [
|
213
|
+
{
|
214
|
+
"api": payload.api_name,
|
215
|
+
"parameters": payload.parameters,
|
216
|
+
}
|
217
|
+
for payload in result.payload_to_append
|
218
|
+
]
|
219
|
+
if result.payload_to_append is not None
|
220
|
+
else None,
|
221
|
+
},
|
222
|
+
}
|
223
|
+
)
|
224
|
+
except RunApiError as e:
|
225
|
+
print("Error", e)
|
226
|
+
await client.send_message({"type": "done", "success": False, "result": e.json})
|
227
|
+
except asyncio.CancelledError:
|
228
|
+
await client.send_message({"type": "done", "success": False, "result": None})
|
229
|
+
break
|
230
|
+
|
231
|
+
done()
|
232
|
+
|
233
|
+
|
234
|
+
class UDASClient:
|
235
|
+
def __init__(self, socket_path: str):
|
236
|
+
self.socket_path = socket_path
|
237
|
+
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
238
|
+
self.json_socket = None
|
239
|
+
|
240
|
+
async def connect(self):
|
241
|
+
try:
|
242
|
+
self.sock.connect(self.socket_path)
|
243
|
+
self.json_socket = JSONUnixSocket(self.sock)
|
244
|
+
return True
|
245
|
+
except Exception as e:
|
246
|
+
print(f"Failed to connect to UDAS: {e}")
|
247
|
+
return False
|
248
|
+
|
249
|
+
def receive_messages(self) -> AsyncGenerator[dict[str, Any], None]:
|
250
|
+
if self.json_socket is None:
|
251
|
+
raise Exception("Socket not connected")
|
252
|
+
|
253
|
+
return self.json_socket.receive_json()
|
254
|
+
|
255
|
+
async def send_message(self, message: dict[str, Any]):
|
256
|
+
if self.json_socket is None:
|
257
|
+
raise Exception("Socket not connected")
|
258
|
+
|
259
|
+
await self.json_socket.send_json(message)
|
260
|
+
|
261
|
+
def close(self):
|
262
|
+
if not self.sock:
|
263
|
+
return
|
264
|
+
self.sock.shutdown(socket.SHUT_RDWR)
|
265
|
+
self.sock.close()
|
@@ -0,0 +1,86 @@
|
|
1
|
+
import json
|
2
|
+
import os
|
3
|
+
import subprocess
|
4
|
+
import sys
|
5
|
+
|
6
|
+
import arguably
|
7
|
+
|
8
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
9
|
+
PYRIGHT_CONFIG_PATH = os.path.join(current_dir, "..", "..", "pyright_type_check.json")
|
10
|
+
PYRIGHT_CONFIG_PATH = os.path.abspath(PYRIGHT_CONFIG_PATH)
|
11
|
+
|
12
|
+
@arguably.command # type: ignore
|
13
|
+
async def project__type_check():
|
14
|
+
"""
|
15
|
+
Run type checking on the API directory using pyright.
|
16
|
+
|
17
|
+
This function executes pyright type checker on the API directory and processes its output.
|
18
|
+
It parses the JSON output from pyright and formats any type checking issues found.
|
19
|
+
|
20
|
+
Returns:
|
21
|
+
None
|
22
|
+
|
23
|
+
Raises:
|
24
|
+
Exception: In the following cases:
|
25
|
+
- If pyright finds any type checking issues (with detailed error messages)
|
26
|
+
- If pyright output cannot be parsed as JSON
|
27
|
+
- If pyright subprocess fails to run
|
28
|
+
- For any other unexpected errors during type checking
|
29
|
+
|
30
|
+
The function collects type checking issues including:
|
31
|
+
- File path where issue was found
|
32
|
+
- Line number of the issue
|
33
|
+
- Severity level of the issue
|
34
|
+
- Error message
|
35
|
+
- Rule type (always "type-check")
|
36
|
+
"""
|
37
|
+
project_dir = os.getcwd()
|
38
|
+
print("📦 Checking Types...")
|
39
|
+
|
40
|
+
try:
|
41
|
+
pyright_issues = []
|
42
|
+
pyright_result = subprocess.run(
|
43
|
+
["pyright", "--outputjson", project_dir, "--project", PYRIGHT_CONFIG_PATH],
|
44
|
+
capture_output=True,
|
45
|
+
text=True,
|
46
|
+
check=False
|
47
|
+
)
|
48
|
+
|
49
|
+
if pyright_result.stdout:
|
50
|
+
pyright_data = json.loads(pyright_result.stdout)
|
51
|
+
for diagnostic in pyright_data.get("generalDiagnostics", []):
|
52
|
+
severity = diagnostic.get("severity", "").lower()
|
53
|
+
severity_emoji = "ℹ️" if severity == "information" else "⚠️" if severity == "warning" else "🔴"
|
54
|
+
|
55
|
+
pyright_issues.append(
|
56
|
+
{"path": diagnostic.get("file", ""), "line": diagnostic.get("range", {}).get("start", {}).get("line", 0) + 1, "severity": diagnostic.get("severity", ""), "message": diagnostic.get("message", ""), "rule": "type-check"}
|
57
|
+
)
|
58
|
+
|
59
|
+
file_path = diagnostic.get("file", "")
|
60
|
+
if "api/" in file_path:
|
61
|
+
file_path = file_path[file_path.index("api/"):]
|
62
|
+
line_num = diagnostic.get("range", {}).get("start", {}).get("line", 0) + 1
|
63
|
+
message = diagnostic.get("message", "")
|
64
|
+
print(f"{severity_emoji} {file_path}:{line_num} - {message}")
|
65
|
+
|
66
|
+
if severity.lower() == "error":
|
67
|
+
print("\n🔴 Type check failed")
|
68
|
+
sys.exit(1)
|
69
|
+
|
70
|
+
if pyright_issues:
|
71
|
+
has_warnings = any(issue["severity"].lower() == "warning" for issue in pyright_issues)
|
72
|
+
if has_warnings:
|
73
|
+
print("\n⚠️ Type check passed with warnings")
|
74
|
+
sys.exit(0)
|
75
|
+
|
76
|
+
print("✨ Python type checking passed without errors.")
|
77
|
+
sys.exit(0)
|
78
|
+
except json.JSONDecodeError:
|
79
|
+
print("🔴 Failed to parse pyright output as JSON")
|
80
|
+
sys.exit(1)
|
81
|
+
except subprocess.SubprocessError:
|
82
|
+
print("🔴 Failed to run pyright type checker")
|
83
|
+
sys.exit(1)
|
84
|
+
except Exception as e:
|
85
|
+
print(f"🔴 Type checking failed: {str(e)}")
|
86
|
+
sys.exit(1)
|