holocubic-cli-python 0.1.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.
@@ -0,0 +1,179 @@
1
+ """Typed public models for the HoloCubic DevTools API."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+
7
+ LEGACY_V1_CAPABILITIES = (
8
+ "fs.list",
9
+ "fs.stat",
10
+ "fs.read",
11
+ "fs.write",
12
+ "fs.mkdir",
13
+ "fs.rename",
14
+ "fs.remove",
15
+ "fs.rmdir",
16
+ "apps.list",
17
+ "devrun.read",
18
+ "devrun.save",
19
+ "devrun.run",
20
+ )
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class DeviceInfo:
25
+ api_version: int
26
+ version: str | None
27
+ route_base: str
28
+ root_path: str
29
+ chunk_size: int
30
+ max_file_size: int
31
+ max_code_bytes: int
32
+ run_app_id: str
33
+ run_app_main: str
34
+ capabilities: tuple[str, ...]
35
+ raw: dict[str, Any] = field(repr=False)
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class RemoteEntry:
40
+ name: str
41
+ path: str
42
+ size: int
43
+ is_dir: bool
44
+ ext: str
45
+ mime: str
46
+ category: str
47
+
48
+ def public(self) -> dict[str, Any]:
49
+ return {
50
+ "name": self.name,
51
+ "path": self.path,
52
+ "size": self.size,
53
+ "isDir": self.is_dir,
54
+ "ext": self.ext,
55
+ "mime": self.mime,
56
+ "category": self.category,
57
+ }
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class ListResult:
62
+ path: str
63
+ parent: str
64
+ dir_count: int
65
+ file_count: int
66
+ total_bytes: int
67
+ items: tuple[RemoteEntry, ...]
68
+
69
+ def public(self) -> dict[str, Any]:
70
+ return {
71
+ "path": self.path,
72
+ "parent": self.parent,
73
+ "dirCount": self.dir_count,
74
+ "fileCount": self.file_count,
75
+ "totalBytes": self.total_bytes,
76
+ "items": [item.public() for item in self.items],
77
+ }
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class RemoteStat:
82
+ path: str
83
+ name: str
84
+ parent: str
85
+ size: int
86
+ is_dir: bool
87
+ ext: str
88
+ mime: str
89
+ category: str
90
+
91
+ def public(self) -> dict[str, Any]:
92
+ return {
93
+ "path": self.path,
94
+ "name": self.name,
95
+ "parent": self.parent,
96
+ "size": self.size,
97
+ "isDir": self.is_dir,
98
+ "ext": self.ext,
99
+ "mime": self.mime,
100
+ "category": self.category,
101
+ }
102
+
103
+
104
+ @dataclass(frozen=True)
105
+ class ReadChunk:
106
+ data: bytes
107
+ size: int
108
+ next_offset: int
109
+ eof: bool
110
+ name: str
111
+ mime: str
112
+
113
+
114
+ @dataclass(frozen=True)
115
+ class UploadResult:
116
+ path: str
117
+ next_offset: int
118
+ total: int
119
+ done: bool
120
+ size: int
121
+
122
+
123
+ @dataclass(frozen=True)
124
+ class AppsResult:
125
+ apps: tuple[dict[str, Any], ...]
126
+ current_app_id: str | None
127
+ run_app_id: str
128
+ run_app_main: str
129
+
130
+ def public(self) -> dict[str, Any]:
131
+ return {
132
+ "apps": list(self.apps),
133
+ "currentAppId": self.current_app_id,
134
+ "runAppId": self.run_app_id,
135
+ "runAppMain": self.run_app_main,
136
+ }
137
+
138
+
139
+ @dataclass(frozen=True)
140
+ class DevRunResult:
141
+ id: str
142
+ entry: str
143
+ bytes: int
144
+ launched: bool
145
+ rescan_requested: bool
146
+
147
+ def public(self) -> dict[str, Any]:
148
+ return {
149
+ "id": self.id,
150
+ "entry": self.entry,
151
+ "bytes": self.bytes,
152
+ "launched": self.launched,
153
+ "rescanRequested": self.rescan_requested,
154
+ }
155
+
156
+
157
+ @dataclass(frozen=True)
158
+ class TransferLimits:
159
+ max_depth: int = 32
160
+ max_entries: int = 4096
161
+ max_download_bytes: int = 128 * 1024 * 1024
162
+
163
+
164
+ @dataclass(frozen=True)
165
+ class TransferSummary:
166
+ source: str
167
+ destination: str
168
+ files: int
169
+ directories: int
170
+ bytes: int
171
+
172
+ def public(self) -> dict[str, Any]:
173
+ return {
174
+ "source": self.source,
175
+ "destination": self.destination,
176
+ "files": self.files,
177
+ "directories": self.directories,
178
+ "bytes": self.bytes,
179
+ }
@@ -0,0 +1,62 @@
1
+ """Remote and local path confinement helpers."""
2
+
3
+ import os
4
+ import posixpath
5
+ from pathlib import Path, PurePosixPath
6
+
7
+ from .errors import UsageError
8
+
9
+
10
+ REMOTE_ROOT = "/sd"
11
+
12
+
13
+ def _reject_nul(value: str) -> None:
14
+ if "\0" in value:
15
+ raise UsageError("Remote path contains an invalid NUL character.")
16
+
17
+
18
+ def normalize_remote_path(value: str | None, root: str = REMOTE_ROOT) -> str:
19
+ candidate_value = (value or "").strip()
20
+ _reject_nul(candidate_value)
21
+ unix = candidate_value.replace("\\", "/")
22
+ candidate = unix if unix.startswith("/") else posixpath.join(root, unix or ".")
23
+ normalized = posixpath.normpath(candidate)
24
+ if normalized != root and not normalized.startswith(f"{root}/"):
25
+ raise UsageError(f"Remote path must stay below {root}: {value or ''}")
26
+ return normalized
27
+
28
+
29
+ def remote_join(parent: str, name: str) -> str:
30
+ _reject_nul(name)
31
+ if not name or name in {".", ".."} or "/" in name or "\\" in name:
32
+ raise UsageError(f"Invalid remote entry name: {name}")
33
+ return normalize_remote_path(posixpath.join(normalize_remote_path(parent), name))
34
+
35
+
36
+ def remote_basename(remote_path: str) -> str:
37
+ normalized = normalize_remote_path(remote_path)
38
+ return "sd" if normalized == REMOTE_ROOT else posixpath.basename(normalized)
39
+
40
+
41
+ def assert_can_delete_remote(remote_path: str) -> None:
42
+ if normalize_remote_path(remote_path) == REMOTE_ROOT:
43
+ raise UsageError(f"Refusing to delete {REMOTE_ROOT}.")
44
+
45
+
46
+ def safe_local_destination(root: str | Path, relative_path: str) -> Path:
47
+ _reject_nul(relative_path)
48
+ normalized = relative_path.replace("\\", "/")
49
+ pure = PurePosixPath(normalized)
50
+ if pure.is_absolute() or ".." in pure.parts:
51
+ raise UsageError(f"Download entry escapes destination: {relative_path}")
52
+ resolved_root = Path(root).resolve()
53
+ resolved = (resolved_root / Path(*pure.parts)).resolve()
54
+ try:
55
+ common = os.path.commonpath((resolved_root, resolved))
56
+ except ValueError as error:
57
+ raise UsageError(
58
+ f"Download entry escapes destination: {relative_path}"
59
+ ) from error
60
+ if Path(common) != resolved_root:
61
+ raise UsageError(f"Download entry escapes destination: {relative_path}")
62
+ return resolved