makers-sdk 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.
- makers_sdk/__init__.py +26 -0
- makers_sdk/_artifacts.py +361 -0
- makers_sdk/client.py +1735 -0
- makers_sdk/errors.py +54 -0
- makers_sdk-0.1.0.dist-info/METADATA +436 -0
- makers_sdk-0.1.0.dist-info/RECORD +8 -0
- makers_sdk-0.1.0.dist-info/WHEEL +5 -0
- makers_sdk-0.1.0.dist-info/top_level.txt +1 -0
makers_sdk/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# sdk-v1.CONTRACT.6 sdk-v1.CONTRACT.19 sdk-v1.CONTRACT.102
|
|
2
|
+
from .client import Makers
|
|
3
|
+
from .errors import (
|
|
4
|
+
AuthError,
|
|
5
|
+
ConflictError,
|
|
6
|
+
DeploymentTimeoutError,
|
|
7
|
+
MakersError,
|
|
8
|
+
NotFoundError,
|
|
9
|
+
RateLimitError,
|
|
10
|
+
TimeoutError,
|
|
11
|
+
UploadError,
|
|
12
|
+
ValidationError,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"AuthError",
|
|
17
|
+
"ConflictError",
|
|
18
|
+
"DeploymentTimeoutError",
|
|
19
|
+
"Makers",
|
|
20
|
+
"MakersError",
|
|
21
|
+
"NotFoundError",
|
|
22
|
+
"RateLimitError",
|
|
23
|
+
"TimeoutError",
|
|
24
|
+
"UploadError",
|
|
25
|
+
"ValidationError",
|
|
26
|
+
]
|
makers_sdk/_artifacts.py
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
import os
|
|
5
|
+
import stat
|
|
6
|
+
import tempfile
|
|
7
|
+
import zipfile
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from functools import lru_cache
|
|
11
|
+
from pathlib import Path, PurePosixPath
|
|
12
|
+
from typing import Any, Iterator, Mapping
|
|
13
|
+
|
|
14
|
+
from .errors import ValidationError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# sdk-v1.CONTRACT.36 sdk-v1.CONTRACT.117 sdk-v1.CONTRACT.128
|
|
18
|
+
# sdk-v1.CONTRACT.130
|
|
19
|
+
_DEFAULT_EXCLUDED_DIRECTORIES = {".git", "node_modules"}
|
|
20
|
+
_DEFAULT_ROOT_EXCLUDED_DIRECTORIES = {".edgeone"}
|
|
21
|
+
_DEFAULT_EXCLUDED_FILES = {".env", ".DS_Store", "Thumbs.db", "desktop.ini"}
|
|
22
|
+
_DEFAULT_EXCLUDED_SUFFIXES = (".log", ".tmp", ".temp", ".swp", ".swo")
|
|
23
|
+
_EDGEONE_DIR = ".edgeone"
|
|
24
|
+
_EDGEONE_CONFIG = "edgeone.json"
|
|
25
|
+
_KEPT_DOT_SEGMENTS = {".well-known", _EDGEONE_DIR}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class PreparedArtifact:
|
|
30
|
+
archive_path: Path
|
|
31
|
+
total_bytes: int
|
|
32
|
+
total_files: int
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _zip_entry_name(info: zipfile.ZipInfo) -> str:
|
|
36
|
+
return (info.orig_filename or info.filename).split("\x00", 1)[0]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _normalized_path(path: str, *, label: str) -> str:
|
|
40
|
+
if (
|
|
41
|
+
not path
|
|
42
|
+
or "\\" in path
|
|
43
|
+
or path.startswith("/")
|
|
44
|
+
or (
|
|
45
|
+
len(path) >= 2
|
|
46
|
+
and path[0] in "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
47
|
+
and path[1] == ":"
|
|
48
|
+
)
|
|
49
|
+
):
|
|
50
|
+
raise ValidationError(
|
|
51
|
+
f"{label} paths must be relative POSIX paths: {path}."
|
|
52
|
+
)
|
|
53
|
+
parts = PurePosixPath(path).parts
|
|
54
|
+
if ".." in parts:
|
|
55
|
+
raise ValidationError(
|
|
56
|
+
f"{label} paths must not contain parent traversal: {path}."
|
|
57
|
+
)
|
|
58
|
+
normalized = "/".join(part for part in parts if part != ".")
|
|
59
|
+
if not normalized:
|
|
60
|
+
raise ValidationError(
|
|
61
|
+
f"{label} paths must be relative POSIX paths: {path}."
|
|
62
|
+
)
|
|
63
|
+
return normalized
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _excluded_directory_names(is_edgeone_directory: bool) -> set[str]:
|
|
67
|
+
if is_edgeone_directory:
|
|
68
|
+
return {".git"}
|
|
69
|
+
return set(_DEFAULT_EXCLUDED_DIRECTORIES)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _has_ignored_dot_segment(path: str) -> bool:
|
|
73
|
+
return any(
|
|
74
|
+
part.startswith(".") and part not in _KEPT_DOT_SEGMENTS
|
|
75
|
+
for part in PurePosixPath(path).parts
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _is_excluded(
|
|
80
|
+
path: str, patterns: list[str], *, is_edgeone_directory: bool
|
|
81
|
+
) -> bool:
|
|
82
|
+
parts = PurePosixPath(path).parts
|
|
83
|
+
if (
|
|
84
|
+
not is_edgeone_directory
|
|
85
|
+
and _has_ignored_dot_segment(path)
|
|
86
|
+
):
|
|
87
|
+
return True
|
|
88
|
+
if (
|
|
89
|
+
len(parts) > 1
|
|
90
|
+
and parts[0] in _DEFAULT_ROOT_EXCLUDED_DIRECTORIES
|
|
91
|
+
):
|
|
92
|
+
return True
|
|
93
|
+
excluded_directories = _excluded_directory_names(is_edgeone_directory)
|
|
94
|
+
if any(part in excluded_directories for part in parts[:-1]):
|
|
95
|
+
return True
|
|
96
|
+
name = parts[-1]
|
|
97
|
+
if name in _DEFAULT_EXCLUDED_FILES or name.endswith(
|
|
98
|
+
_DEFAULT_EXCLUDED_SUFFIXES
|
|
99
|
+
):
|
|
100
|
+
return True
|
|
101
|
+
return any(_matches_posix_glob(path, pattern) for pattern in patterns)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _matches_posix_glob(path: str, pattern: str) -> bool:
|
|
105
|
+
path_parts = path.split("/")
|
|
106
|
+
pattern_parts = pattern.split("/")
|
|
107
|
+
|
|
108
|
+
@lru_cache(maxsize=None)
|
|
109
|
+
def match(path_index: int, pattern_index: int) -> bool:
|
|
110
|
+
if pattern_index == len(pattern_parts):
|
|
111
|
+
return path_index == len(path_parts)
|
|
112
|
+
current = pattern_parts[pattern_index]
|
|
113
|
+
if current == "**":
|
|
114
|
+
return match(path_index, pattern_index + 1) or (
|
|
115
|
+
path_index < len(path_parts)
|
|
116
|
+
and match(path_index + 1, pattern_index)
|
|
117
|
+
)
|
|
118
|
+
return (
|
|
119
|
+
path_index < len(path_parts)
|
|
120
|
+
and fnmatch.fnmatchcase(path_parts[path_index], current)
|
|
121
|
+
and match(path_index + 1, pattern_index + 1)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return match(0, 0)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _validate_patterns(patterns: Any) -> list[str]:
|
|
128
|
+
if patterns is None:
|
|
129
|
+
return []
|
|
130
|
+
if not isinstance(patterns, list) or not all(
|
|
131
|
+
isinstance(pattern, str) for pattern in patterns
|
|
132
|
+
):
|
|
133
|
+
raise ValidationError("exclude_patterns must be a list of POSIX globs.")
|
|
134
|
+
for pattern in patterns:
|
|
135
|
+
if (
|
|
136
|
+
not pattern
|
|
137
|
+
or "\\" in pattern
|
|
138
|
+
or pattern.startswith("/")
|
|
139
|
+
or ".." in PurePosixPath(pattern).parts
|
|
140
|
+
):
|
|
141
|
+
raise ValidationError(
|
|
142
|
+
"excludePatterns must contain relative POSIX globs."
|
|
143
|
+
)
|
|
144
|
+
if pattern.startswith("!"):
|
|
145
|
+
raise ValidationError(
|
|
146
|
+
f"excludePatterns does not support negation: {pattern}."
|
|
147
|
+
)
|
|
148
|
+
return patterns
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _assert_not_symlink(path: Path, relative: str) -> None:
|
|
152
|
+
if path.is_symlink():
|
|
153
|
+
raise ValidationError(
|
|
154
|
+
"Directory artifacts must not contain symbolic links: "
|
|
155
|
+
f"{relative}."
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _should_descend_directory(
|
|
160
|
+
path: Path,
|
|
161
|
+
relative: str,
|
|
162
|
+
patterns: list[str],
|
|
163
|
+
*,
|
|
164
|
+
is_edgeone_directory: bool,
|
|
165
|
+
) -> bool:
|
|
166
|
+
if _is_excluded(
|
|
167
|
+
relative, patterns, is_edgeone_directory=is_edgeone_directory
|
|
168
|
+
) or _is_excluded(
|
|
169
|
+
f"{relative}/placeholder",
|
|
170
|
+
patterns,
|
|
171
|
+
is_edgeone_directory=is_edgeone_directory,
|
|
172
|
+
):
|
|
173
|
+
return False
|
|
174
|
+
_assert_not_symlink(path, relative)
|
|
175
|
+
return True
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _directory_entries(
|
|
179
|
+
directory: Path, patterns: list[str]
|
|
180
|
+
) -> list[tuple[str, Path]]:
|
|
181
|
+
is_edgeone_directory = directory.name == _EDGEONE_DIR
|
|
182
|
+
entries: list[tuple[str, Path]] = []
|
|
183
|
+
for root, directory_names, file_names in os.walk(
|
|
184
|
+
directory, followlinks=False
|
|
185
|
+
):
|
|
186
|
+
root_path = Path(root)
|
|
187
|
+
kept_directories: list[str] = []
|
|
188
|
+
for name in sorted(directory_names):
|
|
189
|
+
path = root_path / name
|
|
190
|
+
relative = path.relative_to(directory).as_posix()
|
|
191
|
+
if _should_descend_directory(
|
|
192
|
+
path,
|
|
193
|
+
relative,
|
|
194
|
+
patterns,
|
|
195
|
+
is_edgeone_directory=is_edgeone_directory,
|
|
196
|
+
):
|
|
197
|
+
kept_directories.append(name)
|
|
198
|
+
directory_names[:] = kept_directories
|
|
199
|
+
for name in sorted(file_names):
|
|
200
|
+
path = root_path / name
|
|
201
|
+
relative = path.relative_to(directory).as_posix()
|
|
202
|
+
if _is_excluded(
|
|
203
|
+
relative,
|
|
204
|
+
patterns,
|
|
205
|
+
is_edgeone_directory=is_edgeone_directory,
|
|
206
|
+
):
|
|
207
|
+
continue
|
|
208
|
+
_assert_not_symlink(path, relative)
|
|
209
|
+
entries.append((relative, path))
|
|
210
|
+
return sorted(entries)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _write_zip(
|
|
214
|
+
archive_path: Path, entries: list[tuple[str, bytes]]
|
|
215
|
+
) -> PreparedArtifact:
|
|
216
|
+
with zipfile.ZipFile(
|
|
217
|
+
archive_path, "w", compression=zipfile.ZIP_DEFLATED
|
|
218
|
+
) as archive:
|
|
219
|
+
for path, content in entries:
|
|
220
|
+
archive.writestr(path, content)
|
|
221
|
+
return PreparedArtifact(
|
|
222
|
+
archive_path=archive_path,
|
|
223
|
+
total_bytes=sum(len(content) for _, content in entries),
|
|
224
|
+
total_files=len(entries),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _validate_archive(archive_path: Path) -> PreparedArtifact:
|
|
229
|
+
try:
|
|
230
|
+
with zipfile.ZipFile(archive_path) as archive:
|
|
231
|
+
seen: set[str] = set()
|
|
232
|
+
total_bytes = 0
|
|
233
|
+
total_files = 0
|
|
234
|
+
for info in archive.infolist():
|
|
235
|
+
path = _zip_entry_name(info)
|
|
236
|
+
normalized = _normalized_path(
|
|
237
|
+
path, label="Archive entry"
|
|
238
|
+
).rstrip("/")
|
|
239
|
+
if normalized in seen:
|
|
240
|
+
raise ValidationError(
|
|
241
|
+
"Archive contains a duplicate normalized path: "
|
|
242
|
+
f"{normalized}."
|
|
243
|
+
)
|
|
244
|
+
seen.add(normalized)
|
|
245
|
+
mode = info.external_attr >> 16
|
|
246
|
+
if stat.S_ISLNK(mode):
|
|
247
|
+
raise ValidationError(
|
|
248
|
+
"Archive artifacts must not contain symbolic links: "
|
|
249
|
+
f"{normalized}."
|
|
250
|
+
)
|
|
251
|
+
if not info.is_dir():
|
|
252
|
+
total_bytes += info.file_size
|
|
253
|
+
total_files += 1
|
|
254
|
+
return PreparedArtifact(
|
|
255
|
+
archive_path=archive_path,
|
|
256
|
+
total_bytes=total_bytes,
|
|
257
|
+
total_files=total_files,
|
|
258
|
+
)
|
|
259
|
+
except (FileNotFoundError, IsADirectoryError, zipfile.BadZipFile) as error:
|
|
260
|
+
raise ValidationError(f"Invalid Zip archive: {archive_path}.") from error
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# sdk-v1.CONTRACT.127
|
|
264
|
+
def _apply_edgeone_cli_layout(
|
|
265
|
+
directory: Path, entries: list[tuple[str, Path]]
|
|
266
|
+
) -> list[tuple[str, bytes]]:
|
|
267
|
+
files = [(path, source.read_bytes()) for path, source in entries]
|
|
268
|
+
if directory.name != _EDGEONE_DIR:
|
|
269
|
+
return files
|
|
270
|
+
parent_name = Path(os.path.abspath(os.fspath(directory))).parent.name
|
|
271
|
+
if not parent_name or parent_name in {".", "/"}:
|
|
272
|
+
raise ValidationError(
|
|
273
|
+
"Directory artifact .edgeone must have a named parent directory."
|
|
274
|
+
)
|
|
275
|
+
prefix = f"{parent_name}/{_EDGEONE_DIR}"
|
|
276
|
+
laid_out = [(f"{prefix}/{path}", content) for path, content in files]
|
|
277
|
+
config = directory.parent / _EDGEONE_CONFIG
|
|
278
|
+
if config.is_symlink():
|
|
279
|
+
raise ValidationError(
|
|
280
|
+
"Directory artifacts must not contain symbolic links: "
|
|
281
|
+
f"{parent_name}/{_EDGEONE_CONFIG}."
|
|
282
|
+
)
|
|
283
|
+
if config.is_file():
|
|
284
|
+
laid_out.append(
|
|
285
|
+
(f"{parent_name}/{_EDGEONE_CONFIG}", config.read_bytes())
|
|
286
|
+
)
|
|
287
|
+
laid_out.sort(key=lambda item: item[0])
|
|
288
|
+
return laid_out
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _directory_zip_entries(artifact: Mapping[str, Any]) -> list[tuple[str, bytes]]:
|
|
292
|
+
directory = Path(str(artifact["directory"]))
|
|
293
|
+
if directory.is_symlink():
|
|
294
|
+
raise ValidationError(
|
|
295
|
+
"Directory artifact path must not be a symbolic link."
|
|
296
|
+
)
|
|
297
|
+
if not directory.is_dir():
|
|
298
|
+
raise ValidationError("Directory artifact path must be a directory.")
|
|
299
|
+
patterns = _validate_patterns(
|
|
300
|
+
artifact.get("exclude_patterns", artifact.get("excludePatterns"))
|
|
301
|
+
)
|
|
302
|
+
return _apply_edgeone_cli_layout(
|
|
303
|
+
directory, _directory_entries(directory, patterns)
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _inline_file_content(value: Any) -> bytes:
|
|
308
|
+
if isinstance(value, str):
|
|
309
|
+
return value.encode()
|
|
310
|
+
if isinstance(value, bytes):
|
|
311
|
+
return value
|
|
312
|
+
raise ValidationError("Inline file contents must be str or bytes.")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _inline_zip_entries(files: Any) -> list[tuple[str, bytes]]:
|
|
316
|
+
if not isinstance(files, Mapping):
|
|
317
|
+
raise ValidationError("Artifact files must be a mapping.")
|
|
318
|
+
seen: set[str] = set()
|
|
319
|
+
entries: list[tuple[str, bytes]] = []
|
|
320
|
+
for path, value in files.items():
|
|
321
|
+
normalized = _normalized_path(str(path), label="Inline file")
|
|
322
|
+
if normalized in seen:
|
|
323
|
+
raise ValidationError(
|
|
324
|
+
"Inline files contain a duplicate normalized path: "
|
|
325
|
+
f"{normalized}."
|
|
326
|
+
)
|
|
327
|
+
seen.add(normalized)
|
|
328
|
+
entries.append((normalized, _inline_file_content(value)))
|
|
329
|
+
entries.sort(key=lambda entry: entry[0])
|
|
330
|
+
return entries
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# sdk-v1.CONTRACT.8 sdk-v1.CONTRACT.17 sdk-v1.CONTRACT.26
|
|
334
|
+
# sdk-v1.CONTRACT.36 sdk-v1.CONTRACT.37 sdk-v1.CONTRACT.52
|
|
335
|
+
# sdk-v1.CONTRACT.59 sdk-v1.CONTRACT.60 sdk-v1.CONTRACT.61
|
|
336
|
+
# sdk-v1.CONTRACT.90 sdk-v1.CONTRACT.108 sdk-v1.CONTRACT.111
|
|
337
|
+
# sdk-v1.CONTRACT.114 sdk-v1.CONTRACT.117 sdk-v1.CONTRACT.127
|
|
338
|
+
# sdk-v1.CONTRACT.128 sdk-v1.CONTRACT.130
|
|
339
|
+
@contextmanager
|
|
340
|
+
def prepare_artifact(
|
|
341
|
+
artifact: Mapping[str, Any],
|
|
342
|
+
) -> Iterator[PreparedArtifact]:
|
|
343
|
+
variants = [name for name in ("directory", "archive", "files") if name in artifact]
|
|
344
|
+
if len(variants) != 1:
|
|
345
|
+
raise ValidationError(
|
|
346
|
+
"Artifact must contain exactly one of directory, archive, or files."
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
variant = variants[0]
|
|
350
|
+
if variant == "archive":
|
|
351
|
+
yield _validate_archive(Path(str(artifact["archive"])))
|
|
352
|
+
return
|
|
353
|
+
|
|
354
|
+
with tempfile.TemporaryDirectory(prefix="edgeone-makers-sdk-") as temporary:
|
|
355
|
+
archive_path = Path(temporary) / "artifact.zip"
|
|
356
|
+
entries = (
|
|
357
|
+
_directory_zip_entries(artifact)
|
|
358
|
+
if variant == "directory"
|
|
359
|
+
else _inline_zip_entries(artifact["files"])
|
|
360
|
+
)
|
|
361
|
+
yield _write_zip(archive_path, entries)
|