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,92 @@
1
+ import os
2
+ import subprocess
3
+ from typing import List
4
+ from typing import Tuple
5
+
6
+ import arguably
7
+ import git
8
+ import git.cmd
9
+ import semver
10
+
11
+
12
+ @arguably.command # type: ignore
13
+ def project__upgrade():
14
+ """
15
+ Upgrade the project's Intuned runtime and Intuned SDK to the latest version.
16
+ """
17
+ runtime_version, sdk_version = resolve_packages_versions()
18
+ print(f"Upgrading to runtime version {runtime_version} and SDK version {sdk_version}")
19
+
20
+ # Upgrade runtime and SDK using poetry
21
+ repo_url = "git+ssh://git@github.com/intuned/python-packages.git"
22
+ cwd = os.getcwd()
23
+
24
+ try:
25
+ # Upgrade runtime
26
+ subprocess.run(
27
+ ["poetry", "add", f"{repo_url}@runtime-{runtime_version}#subdirectory=runtime"], check=True, cwd=cwd
28
+ )
29
+
30
+ # Upgrade SDK
31
+ subprocess.run(["poetry", "add", f"{repo_url}@sdk-{sdk_version}#subdirectory=sdk"], check=True, cwd=cwd)
32
+
33
+ except subprocess.CalledProcessError as e:
34
+ raise RuntimeError(f"Failed to upgrade packages: {e.stderr}") from e
35
+
36
+
37
+ def get_latest_version(versions: List[str], prefix: str) -> str:
38
+ """
39
+ Get the latest version from a list of versioned tags.
40
+ Filters out dev versions and returns the highest semver version.
41
+ """
42
+ valid_versions: list[str] = []
43
+ for version in versions:
44
+ version_str = version.replace(f"{prefix}-", "")
45
+ try:
46
+ # Skip dev versions
47
+ if "dev" not in version_str:
48
+ semver.VersionInfo.parse(version_str)
49
+ valid_versions.append(version_str)
50
+ except ValueError:
51
+ continue
52
+
53
+ if not valid_versions:
54
+ raise ValueError(f"No valid versions found for {prefix}")
55
+
56
+ # Sort versions and get the latest
57
+ latest = str(max(map(semver.VersionInfo.parse, valid_versions)))
58
+ return latest
59
+
60
+
61
+ def resolve_packages_versions() -> Tuple[str, str]:
62
+ """
63
+ Resolves the latest versions of runtime and SDK packages using git ls-remote.
64
+ Returns a tuple of (runtime_version, sdk_version)
65
+ """
66
+ try:
67
+ # Use GitPython to get remote tags
68
+ repo = git.cmd.Git()
69
+ refs = repo.ls_remote("--tags", "git@github.com:intuned/python-packages.git").split("\n")
70
+
71
+ # Parse the output which looks like:
72
+ # hash refs/tags/runtime-1.0.1
73
+ # hash refs/tags/sdk-0.1.1
74
+ tags: list[str] = []
75
+ for line in refs:
76
+ if not line.strip():
77
+ continue
78
+ _, ref = line.split(None, 1)
79
+ tag = ref.replace("refs/tags/", "")
80
+ tags.append(tag)
81
+
82
+ # Split into runtime and sdk versions
83
+ runtime_versions = [tag for tag in tags if tag.startswith("runtime-")]
84
+ sdk_versions = [tag for tag in tags if tag.startswith("sdk-")]
85
+
86
+ latest_runtime = get_latest_version(runtime_versions, "runtime")
87
+ latest_sdk = get_latest_version(sdk_versions, "sdk")
88
+
89
+ return latest_runtime, latest_sdk
90
+
91
+ except git.GitCommandError as e:
92
+ raise RuntimeError(f"Failed to fetch tags from git repository: {e.stderr}") from e
@@ -0,0 +1,264 @@
1
+ import functools
2
+ import subprocess
3
+ from os import environ
4
+ from os import path
5
+
6
+ import arguably
7
+ import toml
8
+ from more_termcolor import bold # type: ignore
9
+ from more_termcolor import cyan # type: ignore
10
+ from more_termcolor import green # type: ignore
11
+ from more_termcolor import italic # type: ignore
12
+ from more_termcolor import underline # type: ignore
13
+ from more_termcolor import yellow # type: ignore
14
+
15
+
16
+ @arguably.command # type: ignore
17
+ def publish_packages(
18
+ *,
19
+ sdk: bool = False,
20
+ runtime: bool = False,
21
+ show_diff: bool = False,
22
+ update_template: bool = False,
23
+ ):
24
+ """
25
+ Publishes the SDK and Runtime packages to `python-packages` repository. Uses the version defined in the pyproject.toml file.
26
+ Args:
27
+ sdk (bool): Publish the SDK package.
28
+ runtime (bool): Publish the Runtime package.
29
+ show_diff (bool): Show the diff of the files that will be copied to the package repo. Configure your git difftool to use your preferred diff tool (VSCode recommended).
30
+ update_template (bool): [-u/--update-template] Update the template versions in the WebApp repo.
31
+ """
32
+
33
+ sdk_source_path = None
34
+ sdk_runtime_path = None
35
+
36
+ webapp_path = environ.get("WEBAPP_PATH")
37
+ if not webapp_path:
38
+ webapp_resolved_path = path.join(path.dirname(__file__), "..", "..", "..", "..")
39
+ if path.exists(path.join(webapp_resolved_path, ".git")):
40
+ webapp_path = webapp_resolved_path
41
+ else:
42
+ raise ValueError(
43
+ "WebApp repo could not be found. Maybe you are not running the globally-installed CLI. Set WEBAPP_PATH environment variable."
44
+ )
45
+ print(bold("WebApp path"), underline(cyan(path.abspath(webapp_path))))
46
+
47
+ sdk_source_path = path.join(webapp_path, "apps", "python-sdk")
48
+ sdk_runtime_path = path.join(webapp_path, "apps", "python-runtime")
49
+
50
+ if True not in [sdk, runtime]:
51
+ raise ValueError("You should select at least one package to release")
52
+
53
+ _release_package = functools.partial(release_package, webapp_path=webapp_path, show_diff=show_diff)
54
+
55
+ if sdk:
56
+ _release_package(
57
+ package_human_name="SDK",
58
+ package_name="intuned-sdk",
59
+ packages_repo_dirname="sdk",
60
+ package_source_path=sdk_source_path,
61
+ )
62
+
63
+ if runtime:
64
+ _release_package(
65
+ package_human_name="Runtime",
66
+ package_name="intuned-runtime",
67
+ packages_repo_dirname="runtime",
68
+ package_source_path=sdk_runtime_path,
69
+ )
70
+
71
+ runtime_version = check_package_version(sdk_runtime_path, "intuned-runtime")
72
+ sdk_version = check_package_version(sdk_source_path, "intuned-sdk")
73
+
74
+ # Only run template generation in production environment
75
+ if update_template:
76
+ update_template_version(webapp_path, runtime_version, sdk_version)
77
+ generate_authoring_template_files(webapp_path)
78
+ else:
79
+ print(bold(f"\nšŸ” {yellow('Skipping authoring template generation - only runs in production environment')}"))
80
+
81
+
82
+ def generate_authoring_template_files(webapp_path: str):
83
+ """
84
+ Generates the authoring template files by running the `yarn generate:build-authoring-template-files` command.
85
+ """
86
+ web_app_dir = path.join(webapp_path, "apps", "web")
87
+ if not path.exists(web_app_dir):
88
+ raise ValueError(f"Web app directory not found at {web_app_dir}")
89
+
90
+ print(bold("\nšŸ“ Generating authoring template files..."))
91
+
92
+ # Run the command to generate the authoring template files
93
+ subprocess.run(
94
+ ["yarn", "generate:build-authoring-template-files"],
95
+ cwd=web_app_dir,
96
+ check=True,
97
+ )
98
+ print(bold(f"\n✨ {green('Authoring template files generated successfully!')}"))
99
+
100
+
101
+ def update_template_version(webapp_path: str, runtime_version: str, sdk_version: str):
102
+ template_path = path.join(
103
+ webapp_path, "apps", "web", "packagerWorkerAssets", "packagerTemplates", "playwright_v1_python", "default"
104
+ )
105
+ pyproject_path = path.join(template_path, "pyproject.toml")
106
+ if not path.exists(pyproject_path):
107
+ raise ValueError(f"pyproject.toml not found at {pyproject_path}")
108
+
109
+ with open(pyproject_path) as f:
110
+ pyproject = toml.load(f)
111
+
112
+ # Update the runtime and sdk versions
113
+ pyproject["tool"]["poetry"]["dependencies"]["intuned-runtime"]["tag"] = f"runtime-{runtime_version}"
114
+ pyproject["tool"]["poetry"]["dependencies"]["intuned-sdk"]["tag"] = f"sdk-{sdk_version}"
115
+
116
+ with open(pyproject_path, "w") as f:
117
+ toml.dump(pyproject, f)
118
+
119
+ print(bold(f"\n✨ {green('Updated template versions successfully!')}"))
120
+
121
+
122
+ def release_package(
123
+ *,
124
+ package_human_name: str,
125
+ package_name: str,
126
+ package_source_path: str | None,
127
+ webapp_path: str,
128
+ packages_repo_dirname: str,
129
+ show_diff: bool = False,
130
+ ):
131
+ print(bold(f"šŸš€ Releasing {green(package_human_name)}"))
132
+ if not package_source_path:
133
+ package_source_path_input = input(bold(f" Enter {green(package_human_name)} path:"))
134
+ package_source_path = package_source_path_input
135
+ else:
136
+ print(bold(f" {green(package_human_name)} Source Path"), underline(cyan(path.abspath(package_source_path))))
137
+ package_version = check_package_version(package_source_path, package_name)
138
+
139
+ print(bold(f" Using package version {green(package_version)} to release:"))
140
+
141
+ package_repo_path = path.join(webapp_path, "..", "python-packages", packages_repo_dirname)
142
+ if not path.exists(package_repo_path):
143
+ raise ValueError(
144
+ f"Package path does not exist. The {underline("python-packages")} repo is expected to be next to the {underline("WebApp")} (expected package at {underline(path.abspath(package_repo_path))})"
145
+ )
146
+
147
+ package_tag = f"{packages_repo_dirname}-{package_version}"
148
+
149
+ # Check if a tag with the sdk_version exists
150
+ result = subprocess.run(
151
+ ["git", "tag", "--list", package_tag], capture_output=True, text=True, cwd=package_repo_path
152
+ )
153
+ should_delete_tag = False
154
+ if package_tag in result.stdout.split():
155
+ raise ValueError(f"Tag {package_version} already exists. Please update the version in the pyproject.toml file.")
156
+
157
+ # fetch and pull main branch
158
+ print(bold("\nšŸ“” Fetching main branch..."))
159
+ subprocess.run(["git", "fetch", "origin", "main"], cwd=package_repo_path, check=True)
160
+ print(bold("šŸ”ƒ Pulling main branch..."))
161
+ subprocess.run(["git", "reset", "--hard", "origin/main"], cwd=package_repo_path, check=True)
162
+
163
+ # switch to main branch
164
+ print(bold("šŸ”„ Switching to main branch..."))
165
+ subprocess.run(["git", "checkout", "main"], cwd=package_repo_path, check=True)
166
+
167
+ print(bold("šŸ“‘ Writing source to repo path..."))
168
+ print(
169
+ yellow(
170
+ f"This will delete all files in {underline(path.realpath(package_repo_path))} and copy the source from {underline(path.realpath(package_source_path))}"
171
+ )
172
+ )
173
+ if input("Continue? (y, N) ").lower().strip() != "y":
174
+ raise ValueError("Publish cancelled")
175
+
176
+ # delete all files in the repo path except .git
177
+ subprocess.run(
178
+ [
179
+ "find",
180
+ package_repo_path,
181
+ "-mindepth",
182
+ "1",
183
+ "-maxdepth",
184
+ "1",
185
+ "-not",
186
+ "-name",
187
+ ".git",
188
+ "-exec",
189
+ "rm",
190
+ "-rf",
191
+ "{}",
192
+ "+",
193
+ ]
194
+ )
195
+ yes_process = subprocess.Popen(["yes"], stdout=subprocess.PIPE)
196
+ try:
197
+ subprocess.run(
198
+ ["cp", "-rf", f"{path.realpath(package_source_path)}/", path.realpath(package_repo_path)],
199
+ stdin=yes_process.stdout,
200
+ check=True,
201
+ )
202
+ finally:
203
+ yes_process.kill()
204
+
205
+ # Commit and push copied changes to main, force with lease
206
+ print(bold("\nšŸ“„ Committing changes..."))
207
+ subprocess.run(["git", "add", "."], cwd=package_repo_path, check=True)
208
+ subprocess.run(
209
+ ["git", "commit", "--allow-empty", "-m", f"Release {package_human_name} version {package_version}"],
210
+ cwd=package_repo_path,
211
+ check=True,
212
+ )
213
+
214
+ try:
215
+ if show_diff:
216
+ print(bold("\nšŸ” Showing diff..."))
217
+ print(italic("Close the diff tool to continue. You will be prompted to confirm the release."))
218
+ subprocess.run(["git", "difftool", "HEAD^", "-t", "vscode", "-y"], cwd=package_repo_path, check=True)
219
+
220
+ if input("Push? (y, N) ").lower().strip() != "y":
221
+ raise ValueError("Publish cancelled")
222
+
223
+ print(bold("\nšŸ“ Pushing changes..."))
224
+ subprocess.run(["git", "push", "origin", "main", "--force-with-lease"], cwd=package_repo_path, check=True)
225
+
226
+ except (KeyboardInterrupt, ValueError):
227
+ print(bold("Resetting commit..."))
228
+ # drop the current commit and go back to the previous state
229
+ subprocess.run(["git", "reset", "--hard", "HEAD^"], cwd=package_repo_path, check=True)
230
+ raise
231
+
232
+ # Create a tag with the version to the current HEAD and push it
233
+ print(bold(f"\nšŸ“ Creating tag {green(package_tag)}..."))
234
+ if should_delete_tag:
235
+ subprocess.run(["git", "tag", "-d", package_tag], cwd=package_repo_path, check=True)
236
+ subprocess.run(["git", "tag", package_tag], cwd=package_repo_path, check=True)
237
+ subprocess.run(
238
+ ["git", "push", "origin", package_tag, *(["--force"] if should_delete_tag else [])],
239
+ cwd=package_repo_path,
240
+ check=True,
241
+ )
242
+
243
+ print(bold(f"\n✨ {green(package_human_name)} released successfully!\n\n"))
244
+
245
+
246
+ def check_package_version(package_path: str, package_expected_name: str):
247
+ if not path.exists(package_path):
248
+ raise ValueError("Invalid package path")
249
+ pyproject_path = path.join(package_path, "pyproject.toml")
250
+ if not path.exists(pyproject_path):
251
+ raise ValueError("pyproject.toml not found in package path")
252
+ with open(pyproject_path) as f:
253
+ pyproject = toml.load(f)
254
+ if not pyproject:
255
+ raise ValueError("Invalid pyproject.toml")
256
+ if not pyproject.get("tool"):
257
+ raise ValueError("Invalid pyproject.toml")
258
+ if not pyproject["tool"].get("poetry"):
259
+ raise ValueError("Invalid pyproject.toml")
260
+ if pyproject["tool"]["poetry"].get("name") != package_expected_name:
261
+ raise ValueError(f"Invalid package name - expected {package_expected_name}")
262
+ if not pyproject["tool"]["poetry"].get("version"):
263
+ raise ValueError("Invalid package version")
264
+ return pyproject["tool"]["poetry"]["version"]
cli/logger.py ADDED
@@ -0,0 +1,19 @@
1
+ import logging
2
+
3
+
4
+ class SimpleLogger:
5
+ def __init__(self, enabled: bool = True) -> None:
6
+ self.enabled: bool = enabled
7
+ self.logger: logging.Logger = logging.getLogger("simple_logger")
8
+ self.logger.setLevel(logging.DEBUG)
9
+ handler = logging.StreamHandler()
10
+ handler.setFormatter(logging.Formatter("%(message)s"))
11
+ self.logger.addHandler(handler)
12
+ self.logger.propagate = False
13
+
14
+ def log(self, message: str) -> None:
15
+ if self.enabled:
16
+ self.logger.debug(message)
17
+
18
+
19
+ default_logger: SimpleLogger = SimpleLogger(enabled=True)
@@ -0,0 +1,31 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any
3
+
4
+ from requests import post
5
+
6
+ from .code_tree import FileSystemTree
7
+
8
+
9
+ @dataclass
10
+ class AiSourceInfo:
11
+ workspace_id: str
12
+ api_key: str
13
+ id: str
14
+ version_id: str
15
+ environment_url: str | None
16
+
17
+ @classmethod
18
+ def from_json(cls, json_data: dict[str, Any]):
19
+ return cls(**json_data)
20
+
21
+
22
+ def deploy_ai_source(code_tree: FileSystemTree, ai_source_info: AiSourceInfo):
23
+ result = post(
24
+ f"{ai_source_info.environment_url}/api/v1/workspace/{ai_source_info.workspace_id}/ai-source/{ai_source_info.id}/version/{ai_source_info.version_id}/deploy",
25
+ headers={"Content-Type": "application/json", "x-api-key": ai_source_info.api_key},
26
+ json={"codeTree": code_tree},
27
+ )
28
+
29
+ if not result.ok:
30
+ print(result.text)
31
+ return result.ok
cli/utils/code_tree.py ADDED
@@ -0,0 +1,83 @@
1
+ import os
2
+ from datetime import datetime
3
+ from typing import Dict
4
+ from typing import Union
5
+
6
+ from more_termcolor import bold # type: ignore
7
+ from more_termcolor import cyan # type: ignore
8
+ from more_termcolor import yellow # type: ignore
9
+ from pathspec import PathSpec
10
+ from pathspec.patterns.gitwildmatch import GitWildMatchPattern
11
+ from typing_extensions import TypedDict
12
+
13
+
14
+ class FileContents(TypedDict):
15
+ contents: str
16
+
17
+
18
+ class FileNode(TypedDict):
19
+ file: FileContents
20
+
21
+
22
+ class DirectoryNode(TypedDict):
23
+ directory: "FileSystemTree"
24
+
25
+
26
+ FileSystemTree = Dict[str, Union[DirectoryNode, FileNode]]
27
+
28
+
29
+ def convert_project_to_code_tree(project_path: str, wait_for_confirm: bool = True) -> FileSystemTree:
30
+ ignore: list[str] | None = None
31
+ cwd = os.path.normpath(project_path)
32
+ while ignore is None and cwd != "/":
33
+ gitignore_path = os.path.join(cwd, ".gitignore")
34
+ if os.path.exists(gitignore_path):
35
+ with open(gitignore_path) as gitignore_file:
36
+ ignore = gitignore_file.read().splitlines()
37
+ print(f"Found .gitignore file: {bold(cyan(gitignore_path))}")
38
+ else:
39
+ cwd = os.path.abspath(os.path.dirname(cwd))
40
+ if ignore is None:
41
+ print(yellow(".gitignore file not found. Deploying all files."))
42
+ ignore = []
43
+
44
+ ignore_spec = PathSpec.from_lines(GitWildMatchPattern, ignore)
45
+
46
+ files_to_deploy_text = "\n ".join(["", *ignore_spec.match_tree(project_path, negate=True), ""])
47
+ print("The following files will be deployed:", files_to_deploy_text)
48
+ if wait_for_confirm and input("Continue? (y/N): ").lower().strip() != "y":
49
+ raise ValueError("Deployment cancelled")
50
+
51
+ def read_directory(path: str) -> FileSystemTree:
52
+ tree: FileSystemTree = {}
53
+ files_or_dirs = os.listdir(path)
54
+
55
+ for item in files_or_dirs:
56
+ item_path = os.path.join(path, item)
57
+ if ignore_spec.match_file(item_path):
58
+ continue
59
+ if os.path.isfile(item_path):
60
+ try:
61
+ with open(item_path) as file:
62
+ content = file.read()
63
+ tree[item] = {"file": {"contents": content}}
64
+ except:
65
+ pass
66
+ elif os.path.isdir(item_path):
67
+ tree[item] = {"directory": read_directory(item_path)}
68
+ return tree
69
+
70
+ tree: FileSystemTree = read_directory(project_path)
71
+ return tree
72
+
73
+
74
+ def get_project_name(path: str):
75
+ path = os.path.abspath(path)
76
+ while path != "/":
77
+ dirname = os.path.basename(path)
78
+ try:
79
+ datetime.strptime(dirname, "%Y-%m-%d_%H:%M")
80
+ path = os.path.dirname(path)
81
+ except ValueError:
82
+ return dirname
83
+ raise ValueError("Could not find project name")
cli/utils/run_apis.py ADDED
@@ -0,0 +1,147 @@
1
+ import builtins
2
+ import json
3
+ import os
4
+ import sys
5
+ import traceback
6
+ from collections import Counter
7
+ from functools import wraps
8
+ from typing import Any
9
+ from typing import Literal
10
+ from typing import NotRequired
11
+ from typing import TypedDict
12
+
13
+ from more_termcolor import bold # type: ignore
14
+ from more_termcolor import cyan # type: ignore
15
+ from more_termcolor import green # type: ignore
16
+ from more_termcolor import italic # type: ignore
17
+ from more_termcolor import yellow # type: ignore
18
+
19
+ from runtime.context.context import IntunedContext
20
+ from runtime.errors.run_api_errors import RunApiError
21
+ from runtime.run import run_api
22
+ from runtime.types import Payload
23
+ from runtime.types.run_types import Auth
24
+ from runtime.types.run_types import AutomationFunction
25
+ from runtime.types.run_types import CDPRunOptions
26
+ from runtime.types.run_types import RunApiParameters
27
+ from runtime.types.run_types import StandaloneRunOptions
28
+
29
+
30
+ class _RunInput(TypedDict):
31
+ api: str
32
+ parameters: dict[str, Any]
33
+ headless: bool
34
+
35
+
36
+ class _RunSuccessOutput(TypedDict):
37
+ success: Literal[True]
38
+ result: Any
39
+ extended_payloads: NotRequired[list[Payload]]
40
+
41
+
42
+ class _RunErrorOutput(TypedDict):
43
+ success: Literal[False]
44
+ error: list[str]
45
+
46
+
47
+ _RunOutput = _RunSuccessOutput | _RunErrorOutput
48
+
49
+
50
+ class RunResultData(TypedDict):
51
+ input: _RunInput
52
+ output: _RunOutput
53
+
54
+
55
+ async def run_api_for_cli(
56
+ initial_api_to_run: Payload,
57
+ headless: bool = True,
58
+ cdp_address: str | None = None,
59
+ *,
60
+ auth: Auth | None = None,
61
+ print_output: bool = True,
62
+ auth_session_parameters: dict[str, Any] | None = None,
63
+ ) -> RunResultData:
64
+ sys.path.append(os.path.join(os.getcwd()))
65
+
66
+ @wraps(builtins.print)
67
+ def print(*args: ..., **kwargs: ...):
68
+ if print_output:
69
+ return builtins.print(*args, **kwargs)
70
+
71
+ to_run: Payload | None = initial_api_to_run
72
+ extended_payloads: list[Payload] = []
73
+ try:
74
+ IntunedContext.current().extend_timeout = extend_timeout
75
+ if auth_session_parameters:
76
+
77
+ async def get_auth_session_parameters() -> dict[str, Any]:
78
+ return auth_session_parameters
79
+
80
+ IntunedContext.current().get_auth_session_parameters = get_auth_session_parameters
81
+ print(bold(f"\nšŸƒ Running {green(to_run["api"])}"))
82
+ run_input: _RunInput = {
83
+ "api": to_run["api"],
84
+ "parameters": to_run["parameters"],
85
+ "headless": headless,
86
+ }
87
+ try:
88
+ result = await run_api(
89
+ RunApiParameters(
90
+ automation_function=AutomationFunction(
91
+ name=f"api/{to_run["api"]}",
92
+ params=to_run["parameters"] or {},
93
+ ),
94
+ run_options=CDPRunOptions(
95
+ cdp_address=cdp_address,
96
+ )
97
+ if cdp_address is not None
98
+ else StandaloneRunOptions(
99
+ headless=headless,
100
+ ),
101
+ auth=auth,
102
+ ),
103
+ )
104
+
105
+ result_to_print = result.result
106
+ if result_to_print is None:
107
+ result_to_print = italic("None")
108
+ elif result_to_print == "":
109
+ result_to_print = italic("Empty string")
110
+ else:
111
+ result_to_print = bold(str(json.dumps(result_to_print, indent=2)))
112
+ print(f"{bold("šŸ“¦ Result: ")}{green(result_to_print)}")
113
+ extended_payloads = [
114
+ {"api": p.api_name, "parameters": p.parameters} for p in (result.payload_to_append or [])
115
+ ]
116
+ extended_payload_name_counter = Counter([p["api"] for p in extended_payloads])
117
+ if sum(extended_payload_name_counter.values()) > 0:
118
+ print(
119
+ bold("āž• Extended payloads:"),
120
+ ", ".join([f"{cyan(k)}: {v}" for k, v in extended_payload_name_counter.items()]),
121
+ )
122
+ _run_output: _RunOutput = {
123
+ "success": True,
124
+ "result": result.result,
125
+ "extended_payloads": extended_payloads,
126
+ }
127
+
128
+ except RunApiError as e:
129
+ print("ā—ļø", yellow(e))
130
+ _run_output: _RunOutput = {
131
+ "success": False,
132
+ "error": traceback.format_exc().split("\n"),
133
+ }
134
+ return {
135
+ "input": run_input,
136
+ "output": _run_output,
137
+ }
138
+
139
+ except Exception:
140
+ traceback.print_exc()
141
+ raise
142
+ finally:
143
+ sys.path.remove(os.path.join(os.getcwd()))
144
+
145
+
146
+ async def extend_timeout():
147
+ print(green(italic("ā±ļø Extending timeout")))
@@ -0,0 +1,55 @@
1
+ import asyncio
2
+ import json
3
+ import socket
4
+ import struct
5
+ from typing import Any
6
+
7
+
8
+ class JSONUnixSocket:
9
+ LENGTH_HEADER_LENGTH = 4
10
+
11
+ def __init__(self, sock: socket.socket):
12
+ self.socket = sock
13
+
14
+ async def send_json(self, data: Any):
15
+ # Convert data to JSON string and encode to bytes
16
+ data_to_send = json.dumps(data).encode()
17
+ # Calculate length
18
+ length = len(data_to_send)
19
+ # Pack length as 4-byte big-endian unsigned int
20
+ length_header = struct.pack(">I", length)
21
+ # Send length header followed by data
22
+ return await asyncio.to_thread(self.socket.sendall, length_header + data_to_send)
23
+
24
+ async def receive_json(self):
25
+ buffer = bytearray()
26
+
27
+ while True:
28
+ try:
29
+ # First, ensure we have the length header
30
+ while len(buffer) < self.LENGTH_HEADER_LENGTH:
31
+ chunk = await asyncio.to_thread(self.socket.recv, 4096)
32
+ if not chunk:
33
+ return
34
+ buffer.extend(chunk)
35
+
36
+ # Read the message length
37
+ length = struct.unpack(">I", buffer[: self.LENGTH_HEADER_LENGTH])[0]
38
+ total_length = length + self.LENGTH_HEADER_LENGTH
39
+
40
+ # Read the full message
41
+ while len(buffer) < total_length:
42
+ chunk = await asyncio.to_thread(self.socket.recv, 4096)
43
+ if not chunk:
44
+ return
45
+ buffer.extend(chunk)
46
+
47
+ # Extract the JSON data
48
+ data = buffer[self.LENGTH_HEADER_LENGTH : total_length]
49
+ # Remove processed data from buffer
50
+ buffer = buffer[total_length:]
51
+ # Parse and yield the JSON data
52
+ yield json.loads(data.decode())
53
+
54
+ except OSError:
55
+ break