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,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")))
|
cli/utils/unix_socket.py
ADDED
@@ -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
|