loopix-sdk 2.30.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.
- loopix/__init__.py +260 -0
- loopix/api/__init__.py +287 -0
- loopix/api/client/__init__.py +8 -0
- loopix/api/client/api/__init__.py +1 -0
- loopix/api/client/api/sandboxes/__init__.py +1 -0
- loopix/api/client/api/sandboxes/delete_sandboxes_sandbox_id.py +161 -0
- loopix/api/client/api/sandboxes/get_sandboxes.py +176 -0
- loopix/api/client/api/sandboxes/get_sandboxes_metrics.py +173 -0
- loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id.py +163 -0
- loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id_logs.py +199 -0
- loopix/api/client/api/sandboxes/get_sandboxes_sandbox_id_metrics.py +212 -0
- loopix/api/client/api/sandboxes/get_v2_sandboxes.py +230 -0
- loopix/api/client/api/sandboxes/get_v_2_sandboxes_sandbox_id_logs.py +254 -0
- loopix/api/client/api/sandboxes/post_sandboxes.py +172 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_connect.py +193 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_pause.py +187 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_refreshes.py +181 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_resume.py +189 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_snapshots.py +195 -0
- loopix/api/client/api/sandboxes/post_sandboxes_sandbox_id_timeout.py +193 -0
- loopix/api/client/api/sandboxes/put_sandboxes_sandbox_id_network.py +199 -0
- loopix/api/client/api/snapshots/__init__.py +1 -0
- loopix/api/client/api/snapshots/get_snapshots.py +202 -0
- loopix/api/client/api/tags/__init__.py +1 -0
- loopix/api/client/api/tags/delete_templates_tags.py +174 -0
- loopix/api/client/api/tags/get_templates_template_id_tags.py +172 -0
- loopix/api/client/api/tags/post_templates_tags.py +176 -0
- loopix/api/client/api/templates/__init__.py +1 -0
- loopix/api/client/api/templates/delete_templates_template_id.py +157 -0
- loopix/api/client/api/templates/get_templates.py +172 -0
- loopix/api/client/api/templates/get_templates_aliases_alias.py +167 -0
- loopix/api/client/api/templates/get_templates_template_id.py +195 -0
- loopix/api/client/api/templates/get_templates_template_id_builds_build_id_logs.py +272 -0
- loopix/api/client/api/templates/get_templates_template_id_builds_build_id_status.py +232 -0
- loopix/api/client/api/templates/get_templates_template_id_files_hash.py +180 -0
- loopix/api/client/api/templates/patch_templates_template_id.py +183 -0
- loopix/api/client/api/templates/patch_v_2_templates_template_id.py +185 -0
- loopix/api/client/api/templates/post_templates.py +172 -0
- loopix/api/client/api/templates/post_templates_template_id.py +181 -0
- loopix/api/client/api/templates/post_templates_template_id_builds_build_id.py +170 -0
- loopix/api/client/api/templates/post_v2_templates.py +172 -0
- loopix/api/client/api/templates/post_v3_templates.py +176 -0
- loopix/api/client/api/templates/post_v_2_templates_template_id_builds_build_id.py +192 -0
- loopix/api/client/api/volumes/__init__.py +1 -0
- loopix/api/client/api/volumes/delete_volumes_volume_id.py +161 -0
- loopix/api/client/api/volumes/get_volumes.py +140 -0
- loopix/api/client/api/volumes/get_volumes_volume_id.py +163 -0
- loopix/api/client/api/volumes/post_volumes.py +172 -0
- loopix/api/client/client.py +286 -0
- loopix/api/client/errors.py +16 -0
- loopix/api/client/models/__init__.py +185 -0
- loopix/api/client/models/admin_build_cancel_result.py +67 -0
- loopix/api/client/models/admin_sandbox_kill_result.py +67 -0
- loopix/api/client/models/assign_template_tags_request.py +67 -0
- loopix/api/client/models/assigned_template_tags.py +68 -0
- loopix/api/client/models/aws_registry.py +85 -0
- loopix/api/client/models/aws_registry_type.py +8 -0
- loopix/api/client/models/build_log_entry.py +89 -0
- loopix/api/client/models/build_status_reason.py +95 -0
- loopix/api/client/models/connect_sandbox.py +59 -0
- loopix/api/client/models/created_access_token.py +100 -0
- loopix/api/client/models/created_team_api_key.py +166 -0
- loopix/api/client/models/delete_template_tags_request.py +67 -0
- loopix/api/client/models/disk_metrics.py +91 -0
- loopix/api/client/models/error.py +67 -0
- loopix/api/client/models/gcp_registry.py +69 -0
- loopix/api/client/models/gcp_registry_type.py +8 -0
- loopix/api/client/models/general_registry.py +77 -0
- loopix/api/client/models/general_registry_type.py +8 -0
- loopix/api/client/models/identifier_masking_details.py +83 -0
- loopix/api/client/models/listed_sandbox.py +179 -0
- loopix/api/client/models/log_level.py +11 -0
- loopix/api/client/models/logs_direction.py +9 -0
- loopix/api/client/models/logs_source.py +9 -0
- loopix/api/client/models/machine_info.py +83 -0
- loopix/api/client/models/max_team_metric.py +78 -0
- loopix/api/client/models/mcp_type_0.py +44 -0
- loopix/api/client/models/new_access_token.py +59 -0
- loopix/api/client/models/new_sandbox.py +224 -0
- loopix/api/client/models/new_team_api_key.py +59 -0
- loopix/api/client/models/new_volume.py +59 -0
- loopix/api/client/models/node.py +160 -0
- loopix/api/client/models/node_detail.py +160 -0
- loopix/api/client/models/node_metrics.py +122 -0
- loopix/api/client/models/node_status.py +12 -0
- loopix/api/client/models/node_status_change.py +82 -0
- loopix/api/client/models/post_sandboxes_sandbox_id_refreshes_body.py +59 -0
- loopix/api/client/models/post_sandboxes_sandbox_id_snapshots_body.py +60 -0
- loopix/api/client/models/post_sandboxes_sandbox_id_timeout_body.py +59 -0
- loopix/api/client/models/resumed_sandbox.py +68 -0
- loopix/api/client/models/sandbox.py +145 -0
- loopix/api/client/models/sandbox_auto_resume_config.py +60 -0
- loopix/api/client/models/sandbox_detail.py +267 -0
- loopix/api/client/models/sandbox_lifecycle.py +70 -0
- loopix/api/client/models/sandbox_log.py +70 -0
- loopix/api/client/models/sandbox_log_entry.py +93 -0
- loopix/api/client/models/sandbox_log_entry_fields.py +44 -0
- loopix/api/client/models/sandbox_logs.py +91 -0
- loopix/api/client/models/sandbox_logs_v2_response.py +73 -0
- loopix/api/client/models/sandbox_metric.py +126 -0
- loopix/api/client/models/sandbox_network_config.py +118 -0
- loopix/api/client/models/sandbox_network_config_rules.py +72 -0
- loopix/api/client/models/sandbox_network_rule.py +74 -0
- loopix/api/client/models/sandbox_network_transform.py +79 -0
- loopix/api/client/models/sandbox_network_transform_headers.py +47 -0
- loopix/api/client/models/sandbox_network_update_config.py +114 -0
- loopix/api/client/models/sandbox_network_update_config_rules.py +71 -0
- loopix/api/client/models/sandbox_on_timeout.py +9 -0
- loopix/api/client/models/sandbox_pause_request.py +62 -0
- loopix/api/client/models/sandbox_state.py +9 -0
- loopix/api/client/models/sandbox_volume_mount.py +67 -0
- loopix/api/client/models/sandboxes_with_metrics.py +59 -0
- loopix/api/client/models/snapshot_info.py +70 -0
- loopix/api/client/models/team.py +83 -0
- loopix/api/client/models/team_api_key.py +158 -0
- loopix/api/client/models/team_metric.py +86 -0
- loopix/api/client/models/team_user.py +75 -0
- loopix/api/client/models/template.py +225 -0
- loopix/api/client/models/template_alias_response.py +67 -0
- loopix/api/client/models/template_build.py +139 -0
- loopix/api/client/models/template_build_file_upload.py +70 -0
- loopix/api/client/models/template_build_info.py +126 -0
- loopix/api/client/models/template_build_logs_response.py +73 -0
- loopix/api/client/models/template_build_request.py +115 -0
- loopix/api/client/models/template_build_request_v2.py +88 -0
- loopix/api/client/models/template_build_request_v3.py +107 -0
- loopix/api/client/models/template_build_start_v2.py +184 -0
- loopix/api/client/models/template_build_status.py +11 -0
- loopix/api/client/models/template_legacy.py +207 -0
- loopix/api/client/models/template_request_response_v3.py +99 -0
- loopix/api/client/models/template_step.py +91 -0
- loopix/api/client/models/template_tag.py +78 -0
- loopix/api/client/models/template_update_request.py +59 -0
- loopix/api/client/models/template_update_response.py +59 -0
- loopix/api/client/models/template_with_builds.py +156 -0
- loopix/api/client/models/update_team_api_key.py +59 -0
- loopix/api/client/models/volume.py +67 -0
- loopix/api/client/models/volume_and_token.py +75 -0
- loopix/api/client/models/volume_token.py +59 -0
- loopix/api/client/py.typed +1 -0
- loopix/api/client/types.py +54 -0
- loopix/api/client_async/__init__.py +74 -0
- loopix/api/client_sync/__init__.py +73 -0
- loopix/api/metadata.py +14 -0
- loopix/connection_config.py +309 -0
- loopix/envd/api.py +170 -0
- loopix/envd/filesystem/filesystem_connect.py +193 -0
- loopix/envd/filesystem/filesystem_pb2.py +80 -0
- loopix/envd/filesystem/filesystem_pb2.pyi +272 -0
- loopix/envd/process/process_connect.py +174 -0
- loopix/envd/process/process_pb2.py +96 -0
- loopix/envd/process/process_pb2.pyi +316 -0
- loopix/envd/rpc.py +139 -0
- loopix/envd/versions.py +11 -0
- loopix/exceptions.py +133 -0
- loopix/io_utils.py +57 -0
- loopix/paginator.py +52 -0
- loopix/py.typed +0 -0
- loopix/sandbox/_git/__init__.py +85 -0
- loopix/sandbox/_git/args.py +363 -0
- loopix/sandbox/_git/auth.py +132 -0
- loopix/sandbox/_git/config.py +32 -0
- loopix/sandbox/_git/parse.py +222 -0
- loopix/sandbox/_git/types.py +149 -0
- loopix/sandbox/commands/command_handle.py +69 -0
- loopix/sandbox/commands/main.py +39 -0
- loopix/sandbox/filesystem/filesystem.py +337 -0
- loopix/sandbox/filesystem/watch_handle.py +70 -0
- loopix/sandbox/main.py +227 -0
- loopix/sandbox/mcp.py +1949 -0
- loopix/sandbox/network.py +8 -0
- loopix/sandbox/sandbox_api.py +624 -0
- loopix/sandbox/signature.py +47 -0
- loopix/sandbox/utils.py +34 -0
- loopix/sandbox_async/commands/command.py +396 -0
- loopix/sandbox_async/commands/command_handle.py +298 -0
- loopix/sandbox_async/commands/pty.py +257 -0
- loopix/sandbox_async/filesystem/filesystem.py +720 -0
- loopix/sandbox_async/filesystem/watch_handle.py +97 -0
- loopix/sandbox_async/git.py +1100 -0
- loopix/sandbox_async/main.py +987 -0
- loopix/sandbox_async/paginator.py +140 -0
- loopix/sandbox_async/sandbox_api.py +504 -0
- loopix/sandbox_async/utils.py +7 -0
- loopix/sandbox_domains.py +5 -0
- loopix/sandbox_sync/commands/command.py +420 -0
- loopix/sandbox_sync/commands/command_handle.py +239 -0
- loopix/sandbox_sync/commands/pty.py +279 -0
- loopix/sandbox_sync/filesystem/filesystem.py +710 -0
- loopix/sandbox_sync/filesystem/watch_handle.py +102 -0
- loopix/sandbox_sync/git.py +1077 -0
- loopix/sandbox_sync/main.py +975 -0
- loopix/sandbox_sync/paginator.py +140 -0
- loopix/sandbox_sync/sandbox_api.py +491 -0
- loopix/template/consts.py +45 -0
- loopix/template/dockerfile_parser.py +286 -0
- loopix/template/logger.py +232 -0
- loopix/template/main.py +1368 -0
- loopix/template/readycmd.py +144 -0
- loopix/template/types.py +194 -0
- loopix/template/utils.py +426 -0
- loopix/template_async/build_api.py +419 -0
- loopix/template_async/main.py +528 -0
- loopix/template_sync/build_api.py +409 -0
- loopix/template_sync/main.py +529 -0
- loopix/volume/client/__init__.py +8 -0
- loopix/volume/client/api/__init__.py +1 -0
- loopix/volume/client/api/volumes/__init__.py +1 -0
- loopix/volume/client/api/volumes/delete_volumecontent_volume_id_path.py +174 -0
- loopix/volume/client/api/volumes/get_volumecontent_volume_id_dir.py +204 -0
- loopix/volume/client/api/volumes/get_volumecontent_volume_id_file.py +179 -0
- loopix/volume/client/api/volumes/get_volumecontent_volume_id_path.py +176 -0
- loopix/volume/client/api/volumes/patch_volumecontent_volume_id_path.py +203 -0
- loopix/volume/client/api/volumes/post_volumecontent_volume_id_dir.py +239 -0
- loopix/volume/client/api/volumes/put_volumecontent_volume_id_file.py +259 -0
- loopix/volume/client/client.py +286 -0
- loopix/volume/client/errors.py +16 -0
- loopix/volume/client/models/__init__.py +13 -0
- loopix/volume/client/models/error.py +67 -0
- loopix/volume/client/models/patch_volumecontent_volume_id_path_body.py +77 -0
- loopix/volume/client/models/volume_entry_stat.py +145 -0
- loopix/volume/client/models/volume_entry_stat_type.py +11 -0
- loopix/volume/client/py.typed +1 -0
- loopix/volume/client/types.py +54 -0
- loopix/volume/client_async/__init__.py +88 -0
- loopix/volume/client_sync/__init__.py +80 -0
- loopix/volume/connection_config.py +145 -0
- loopix/volume/types.py +62 -0
- loopix/volume/utils.py +52 -0
- loopix/volume/volume_async.py +639 -0
- loopix/volume/volume_sync.py +639 -0
- loopix_connect/__init__.py +1 -0
- loopix_connect/client.py +534 -0
- loopix_connect/py.typed +0 -0
- loopix_sdk-2.30.0.dist-info/METADATA +98 -0
- loopix_sdk-2.30.0.dist-info/RECORD +238 -0
- loopix_sdk-2.30.0.dist-info/WHEEL +4 -0
- loopix_sdk-2.30.0.dist-info/licenses/LICENSE +9 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
from urllib.parse import urlparse
|
|
3
|
+
|
|
4
|
+
from loopix.exceptions import InvalidArgumentException
|
|
5
|
+
from loopix.sandbox._git.types import GitBranches, GitFileStatus, GitStatus
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def derive_repo_dir_from_url(url: str) -> Optional[str]:
|
|
9
|
+
"""
|
|
10
|
+
Derive the default repository directory name from a Git URL.
|
|
11
|
+
|
|
12
|
+
:param url: Git repository URL
|
|
13
|
+
:return: Repository directory name, if it can be determined
|
|
14
|
+
"""
|
|
15
|
+
parsed = urlparse(url)
|
|
16
|
+
if parsed.scheme not in ("http", "https"):
|
|
17
|
+
return None
|
|
18
|
+
trimmed_path = parsed.path.rstrip("/")
|
|
19
|
+
if not trimmed_path:
|
|
20
|
+
return None
|
|
21
|
+
last_segment = trimmed_path.split("/")[-1]
|
|
22
|
+
if not last_segment:
|
|
23
|
+
return None
|
|
24
|
+
return last_segment[:-4] if last_segment.endswith(".git") else last_segment
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _parse_ahead_behind(segment: Optional[str]) -> tuple[int, int]:
|
|
28
|
+
"""
|
|
29
|
+
Parse the ahead/behind segment from porcelain branch info.
|
|
30
|
+
|
|
31
|
+
:param segment: Segment text like "ahead 2, behind 1"
|
|
32
|
+
:return: Tuple of (ahead, behind)
|
|
33
|
+
"""
|
|
34
|
+
if not segment:
|
|
35
|
+
return 0, 0
|
|
36
|
+
ahead = 0
|
|
37
|
+
behind = 0
|
|
38
|
+
if "ahead" in segment:
|
|
39
|
+
try:
|
|
40
|
+
ahead = int(segment.split("ahead")[1].split(",")[0].strip())
|
|
41
|
+
except Exception:
|
|
42
|
+
ahead = 0
|
|
43
|
+
if "behind" in segment:
|
|
44
|
+
try:
|
|
45
|
+
behind = int(segment.split("behind")[1].split(",")[0].strip())
|
|
46
|
+
except Exception:
|
|
47
|
+
behind = 0
|
|
48
|
+
return ahead, behind
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _normalize_branch_name(name: str) -> str:
|
|
52
|
+
"""
|
|
53
|
+
Normalize branch names from porcelain branch output.
|
|
54
|
+
|
|
55
|
+
:param name: Raw branch name section
|
|
56
|
+
:return: Normalized branch name
|
|
57
|
+
"""
|
|
58
|
+
if name.startswith("HEAD (detached at "):
|
|
59
|
+
return name.replace("HEAD (detached at ", "").rstrip(")")
|
|
60
|
+
return (
|
|
61
|
+
name.replace("HEAD (no branch)", "HEAD")
|
|
62
|
+
.replace("No commits yet on ", "")
|
|
63
|
+
.replace("Initial commit on ", "")
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _derive_status(index_status: str, working_status: str) -> str:
|
|
68
|
+
"""
|
|
69
|
+
Derive a normalized status label from porcelain status characters.
|
|
70
|
+
|
|
71
|
+
:param index_status: Index status character
|
|
72
|
+
:param working_status: Working tree status character
|
|
73
|
+
:return: Normalized status label
|
|
74
|
+
"""
|
|
75
|
+
statuses = {index_status, working_status}
|
|
76
|
+
if "U" in statuses:
|
|
77
|
+
return "conflict"
|
|
78
|
+
if "R" in statuses:
|
|
79
|
+
return "renamed"
|
|
80
|
+
if "C" in statuses:
|
|
81
|
+
return "copied"
|
|
82
|
+
if "D" in statuses:
|
|
83
|
+
return "deleted"
|
|
84
|
+
if "A" in statuses:
|
|
85
|
+
return "added"
|
|
86
|
+
if "M" in statuses:
|
|
87
|
+
return "modified"
|
|
88
|
+
if "T" in statuses:
|
|
89
|
+
return "typechange"
|
|
90
|
+
if "?" in statuses:
|
|
91
|
+
return "untracked"
|
|
92
|
+
return "unknown"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def parse_git_status(output: str) -> GitStatus:
|
|
96
|
+
"""
|
|
97
|
+
Parse `git status --porcelain=1 -b` output into a structured object.
|
|
98
|
+
|
|
99
|
+
:param output: Git status output
|
|
100
|
+
:return: Parsed `GitStatus`
|
|
101
|
+
"""
|
|
102
|
+
lines = [line.rstrip() for line in output.split("\n") if line.strip()]
|
|
103
|
+
current_branch: Optional[str] = None
|
|
104
|
+
upstream: Optional[str] = None
|
|
105
|
+
ahead = 0
|
|
106
|
+
behind = 0
|
|
107
|
+
detached = False
|
|
108
|
+
file_status: List[GitFileStatus] = []
|
|
109
|
+
|
|
110
|
+
if not lines:
|
|
111
|
+
return GitStatus(
|
|
112
|
+
current_branch=current_branch,
|
|
113
|
+
upstream=upstream,
|
|
114
|
+
ahead=ahead,
|
|
115
|
+
behind=behind,
|
|
116
|
+
detached=detached,
|
|
117
|
+
file_status=file_status,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
branch_line = lines[0]
|
|
121
|
+
if branch_line.startswith("## "):
|
|
122
|
+
branch_info = branch_line[3:]
|
|
123
|
+
ahead_start = branch_info.find(" [")
|
|
124
|
+
branch_part = branch_info if ahead_start == -1 else branch_info[:ahead_start]
|
|
125
|
+
ahead_part = None if ahead_start == -1 else branch_info[ahead_start + 2 : -1]
|
|
126
|
+
normalized_branch = _normalize_branch_name(branch_part)
|
|
127
|
+
raw_branch = branch_part
|
|
128
|
+
is_detached = raw_branch.startswith("HEAD (detached at ") or (
|
|
129
|
+
"detached" in raw_branch
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if is_detached or normalized_branch.startswith("HEAD"):
|
|
133
|
+
detached = True
|
|
134
|
+
elif "..." in normalized_branch:
|
|
135
|
+
branch, upstream_branch = normalized_branch.split("...")
|
|
136
|
+
current_branch = branch or None
|
|
137
|
+
upstream = upstream_branch or None
|
|
138
|
+
else:
|
|
139
|
+
current_branch = normalized_branch or None
|
|
140
|
+
|
|
141
|
+
ahead, behind = _parse_ahead_behind(ahead_part)
|
|
142
|
+
|
|
143
|
+
for line in lines[1:]:
|
|
144
|
+
if line.startswith("?? "):
|
|
145
|
+
name = line[3:]
|
|
146
|
+
file_status.append(
|
|
147
|
+
GitFileStatus(
|
|
148
|
+
name=name,
|
|
149
|
+
status="untracked",
|
|
150
|
+
index_status="?",
|
|
151
|
+
working_tree_status="?",
|
|
152
|
+
staged=False,
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
if len(line) < 3:
|
|
158
|
+
continue
|
|
159
|
+
index_status = line[0]
|
|
160
|
+
working_status = line[1]
|
|
161
|
+
path = line[3:]
|
|
162
|
+
renamed_from: Optional[str] = None
|
|
163
|
+
name = path
|
|
164
|
+
if " -> " in path:
|
|
165
|
+
renamed_from, name = path.split(" -> ", 1)
|
|
166
|
+
|
|
167
|
+
file_status.append(
|
|
168
|
+
GitFileStatus(
|
|
169
|
+
name=name,
|
|
170
|
+
status=_derive_status(index_status, working_status),
|
|
171
|
+
index_status=index_status,
|
|
172
|
+
working_tree_status=working_status,
|
|
173
|
+
staged=index_status not in (" ", "?"),
|
|
174
|
+
renamed_from=renamed_from,
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return GitStatus(
|
|
179
|
+
current_branch=current_branch,
|
|
180
|
+
upstream=upstream,
|
|
181
|
+
ahead=ahead,
|
|
182
|
+
behind=behind,
|
|
183
|
+
detached=detached,
|
|
184
|
+
file_status=file_status,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def parse_git_branches(output: str) -> GitBranches:
|
|
189
|
+
"""
|
|
190
|
+
Parse `git branch --format=%(refname:short)\t%(HEAD)` output.
|
|
191
|
+
|
|
192
|
+
:param output: Git branch output
|
|
193
|
+
:return: Parsed `GitBranches`
|
|
194
|
+
"""
|
|
195
|
+
branches: List[str] = []
|
|
196
|
+
current_branch: Optional[str] = None
|
|
197
|
+
|
|
198
|
+
lines = [line.strip() for line in output.split("\n") if line.strip()]
|
|
199
|
+
for line in lines:
|
|
200
|
+
parts = line.split("\t")
|
|
201
|
+
name = parts[0]
|
|
202
|
+
branches.append(name)
|
|
203
|
+
if len(parts) > 1 and parts[1] == "*":
|
|
204
|
+
current_branch = name
|
|
205
|
+
|
|
206
|
+
return GitBranches(branches=branches, current_branch=current_branch)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def parse_remote_url(output: str, remote: str) -> str:
|
|
210
|
+
"""
|
|
211
|
+
Parse a git remote URL output and validate it's present.
|
|
212
|
+
|
|
213
|
+
:param output: Git remote get-url output
|
|
214
|
+
:param remote: Remote name for the error message
|
|
215
|
+
:return: Remote URL
|
|
216
|
+
"""
|
|
217
|
+
url = output.strip()
|
|
218
|
+
if not url:
|
|
219
|
+
raise InvalidArgumentException(
|
|
220
|
+
f'Remote "{remote}" URL not found in repository.'
|
|
221
|
+
)
|
|
222
|
+
return url
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from typing_extensions import Literal
|
|
5
|
+
|
|
6
|
+
GitResetMode = Literal["soft", "mixed", "hard", "merge", "keep"]
|
|
7
|
+
"""Mode for a git reset operation."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class GitFileStatus:
|
|
12
|
+
"""
|
|
13
|
+
Parsed git status entry for a file.
|
|
14
|
+
|
|
15
|
+
:param name: Path relative to the repository root
|
|
16
|
+
:param status: Normalized status string (e.g. "modified", "added")
|
|
17
|
+
:param index_status: Index status character from porcelain output
|
|
18
|
+
:param working_tree_status: Working tree status character from porcelain output
|
|
19
|
+
:param staged: Whether the change is staged
|
|
20
|
+
:param renamed_from: Original path when the file was renamed
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
name: str
|
|
24
|
+
status: str
|
|
25
|
+
index_status: str
|
|
26
|
+
working_tree_status: str
|
|
27
|
+
staged: bool
|
|
28
|
+
renamed_from: Optional[str] = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class GitStatus:
|
|
33
|
+
"""
|
|
34
|
+
Parsed git repository status.
|
|
35
|
+
|
|
36
|
+
:param current_branch: Current branch name, if available
|
|
37
|
+
:param upstream: Upstream branch name, if available
|
|
38
|
+
:param ahead: Number of commits the branch is ahead of upstream
|
|
39
|
+
:param behind: Number of commits the branch is behind upstream
|
|
40
|
+
:param detached: Whether HEAD is detached
|
|
41
|
+
:param file_status: List of file status entries
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
current_branch: Optional[str]
|
|
45
|
+
upstream: Optional[str]
|
|
46
|
+
ahead: int
|
|
47
|
+
behind: int
|
|
48
|
+
detached: bool
|
|
49
|
+
file_status: List[GitFileStatus]
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def is_clean(self) -> bool:
|
|
53
|
+
"""
|
|
54
|
+
Return True when there are no tracked or untracked file changes.
|
|
55
|
+
"""
|
|
56
|
+
return len(self.file_status) == 0
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def has_changes(self) -> bool:
|
|
60
|
+
"""
|
|
61
|
+
Return True when there are any tracked or untracked file changes.
|
|
62
|
+
"""
|
|
63
|
+
return len(self.file_status) > 0
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def has_staged(self) -> bool:
|
|
67
|
+
"""
|
|
68
|
+
Return True when at least one file has staged changes.
|
|
69
|
+
"""
|
|
70
|
+
return any(item.staged for item in self.file_status)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def has_untracked(self) -> bool:
|
|
74
|
+
"""
|
|
75
|
+
Return True when at least one file is untracked.
|
|
76
|
+
"""
|
|
77
|
+
return any(item.status == "untracked" for item in self.file_status)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def has_conflicts(self) -> bool:
|
|
81
|
+
"""
|
|
82
|
+
Return True when at least one file is in conflict.
|
|
83
|
+
"""
|
|
84
|
+
return any(item.status == "conflict" for item in self.file_status)
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def total_count(self) -> int:
|
|
88
|
+
"""
|
|
89
|
+
Return the total number of changed files.
|
|
90
|
+
"""
|
|
91
|
+
return len(self.file_status)
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def staged_count(self) -> int:
|
|
95
|
+
"""
|
|
96
|
+
Return the number of files with staged changes.
|
|
97
|
+
"""
|
|
98
|
+
return sum(1 for item in self.file_status if item.staged)
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def unstaged_count(self) -> int:
|
|
102
|
+
"""
|
|
103
|
+
Return the number of files with unstaged changes.
|
|
104
|
+
"""
|
|
105
|
+
return sum(1 for item in self.file_status if not item.staged)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def untracked_count(self) -> int:
|
|
109
|
+
"""
|
|
110
|
+
Return the number of untracked files.
|
|
111
|
+
"""
|
|
112
|
+
return sum(1 for item in self.file_status if item.status == "untracked")
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def conflict_count(self) -> int:
|
|
116
|
+
"""
|
|
117
|
+
Return the number of files with merge conflicts.
|
|
118
|
+
"""
|
|
119
|
+
return sum(1 for item in self.file_status if item.status == "conflict")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class GitBranches:
|
|
124
|
+
"""
|
|
125
|
+
Parsed git branch list.
|
|
126
|
+
|
|
127
|
+
:param branches: List of branch names
|
|
128
|
+
:param current_branch: Current branch name, if available
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
branches: List[str]
|
|
132
|
+
current_branch: Optional[str]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass
|
|
136
|
+
class ClonePlan:
|
|
137
|
+
"""
|
|
138
|
+
Prepared arguments and metadata for git clone.
|
|
139
|
+
|
|
140
|
+
:param args: Command arguments for git clone
|
|
141
|
+
:param repo_path: Repository path to use for post-clone adjustments
|
|
142
|
+
:param sanitized_url: Credential-stripped URL to restore
|
|
143
|
+
:param should_strip: Whether to reset the remote URL after clone
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
args: List[str]
|
|
147
|
+
repo_path: Optional[str]
|
|
148
|
+
sanitized_url: Optional[str]
|
|
149
|
+
should_strip: bool
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from loopix.exceptions import SandboxException
|
|
5
|
+
|
|
6
|
+
Stdout = str
|
|
7
|
+
"""
|
|
8
|
+
Command stdout output.
|
|
9
|
+
"""
|
|
10
|
+
Stderr = str
|
|
11
|
+
"""
|
|
12
|
+
Command stderr output.
|
|
13
|
+
"""
|
|
14
|
+
PtyOutput = bytes
|
|
15
|
+
"""
|
|
16
|
+
Pty output.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class PtySize:
|
|
22
|
+
"""
|
|
23
|
+
Pseudo-terminal size.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
rows: int
|
|
27
|
+
"""
|
|
28
|
+
Number of rows.
|
|
29
|
+
"""
|
|
30
|
+
cols: int
|
|
31
|
+
"""
|
|
32
|
+
Number of columns.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class CommandResult:
|
|
38
|
+
"""
|
|
39
|
+
Command execution result.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
stderr: str
|
|
43
|
+
"""
|
|
44
|
+
Command stderr output.
|
|
45
|
+
"""
|
|
46
|
+
stdout: str
|
|
47
|
+
"""
|
|
48
|
+
Command stdout output.
|
|
49
|
+
"""
|
|
50
|
+
exit_code: int
|
|
51
|
+
"""
|
|
52
|
+
Command exit code.
|
|
53
|
+
|
|
54
|
+
`0` if the command finished successfully.
|
|
55
|
+
"""
|
|
56
|
+
error: Optional[str]
|
|
57
|
+
"""
|
|
58
|
+
Error message from command execution if it failed.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class CommandExitException(SandboxException, CommandResult):
|
|
64
|
+
"""
|
|
65
|
+
Exception raised when a command exits with a non-zero exit code.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __str__(self):
|
|
69
|
+
return f"Command exited with code {self.exit_code} and error:\n{self.stderr}"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class ProcessInfo:
|
|
7
|
+
"""
|
|
8
|
+
Information about a command, PTY session or start command running in the sandbox as process.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
pid: int
|
|
12
|
+
"""
|
|
13
|
+
Process ID.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
tag: Optional[str]
|
|
17
|
+
"""
|
|
18
|
+
Custom tag used for identifying special commands like start command in the custom template.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
cmd: str
|
|
22
|
+
"""
|
|
23
|
+
Command that was executed.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
args: List[str]
|
|
27
|
+
"""
|
|
28
|
+
Command arguments.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
envs: Dict[str, str]
|
|
32
|
+
"""
|
|
33
|
+
Environment variables used for the command.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
cwd: Optional[str]
|
|
37
|
+
"""
|
|
38
|
+
Executed command working directory.
|
|
39
|
+
"""
|