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 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
+ ]
@@ -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)