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.
Files changed (58) hide show
  1. cli/__init__.py +45 -0
  2. cli/commands/__init__.py +25 -0
  3. cli/commands/ai_source/__init__.py +4 -0
  4. cli/commands/ai_source/ai_source.py +10 -0
  5. cli/commands/ai_source/deploy.py +64 -0
  6. cli/commands/browser/__init__.py +3 -0
  7. cli/commands/browser/save_state.py +32 -0
  8. cli/commands/init.py +127 -0
  9. cli/commands/project/__init__.py +20 -0
  10. cli/commands/project/auth_session/__init__.py +5 -0
  11. cli/commands/project/auth_session/check.py +118 -0
  12. cli/commands/project/auth_session/create.py +96 -0
  13. cli/commands/project/auth_session/load.py +39 -0
  14. cli/commands/project/project.py +10 -0
  15. cli/commands/project/run.py +340 -0
  16. cli/commands/project/run_interface.py +265 -0
  17. cli/commands/project/type_check.py +86 -0
  18. cli/commands/project/upgrade.py +92 -0
  19. cli/commands/publish_packages.py +264 -0
  20. cli/logger.py +19 -0
  21. cli/utils/ai_source_project.py +31 -0
  22. cli/utils/code_tree.py +83 -0
  23. cli/utils/run_apis.py +147 -0
  24. cli/utils/unix_socket.py +55 -0
  25. intuned_runtime-1.0.0.dist-info/LICENSE +42 -0
  26. intuned_runtime-1.0.0.dist-info/METADATA +113 -0
  27. intuned_runtime-1.0.0.dist-info/RECORD +58 -0
  28. intuned_runtime-1.0.0.dist-info/WHEEL +4 -0
  29. intuned_runtime-1.0.0.dist-info/entry_points.txt +3 -0
  30. runtime/__init__.py +3 -0
  31. runtime/backend_functions/__init__.py +5 -0
  32. runtime/backend_functions/_call_backend_function.py +86 -0
  33. runtime/backend_functions/get_auth_session_parameters.py +30 -0
  34. runtime/browser/__init__.py +3 -0
  35. runtime/browser/launch_chromium.py +212 -0
  36. runtime/browser/storage_state.py +106 -0
  37. runtime/context/__init__.py +5 -0
  38. runtime/context/context.py +51 -0
  39. runtime/env.py +13 -0
  40. runtime/errors/__init__.py +21 -0
  41. runtime/errors/auth_session_errors.py +9 -0
  42. runtime/errors/run_api_errors.py +120 -0
  43. runtime/errors/trace_errors.py +3 -0
  44. runtime/helpers/__init__.py +5 -0
  45. runtime/helpers/extend_payload.py +9 -0
  46. runtime/helpers/extend_timeout.py +13 -0
  47. runtime/helpers/get_auth_session_parameters.py +14 -0
  48. runtime/py.typed +0 -0
  49. runtime/run/__init__.py +3 -0
  50. runtime/run/intuned_settings.py +38 -0
  51. runtime/run/playwright_constructs.py +19 -0
  52. runtime/run/run_api.py +233 -0
  53. runtime/run/traces.py +36 -0
  54. runtime/types/__init__.py +15 -0
  55. runtime/types/payload.py +7 -0
  56. runtime/types/run_types.py +177 -0
  57. runtime_helpers/__init__.py +5 -0
  58. 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)