instance-repo 1.0.7.dev0__tar.gz → 1.0.8__tar.gz
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.
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/PKG-INFO +1 -1
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/__init__.py +1 -1
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/clients/instances.py +140 -12
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/layout.py +8 -2
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/validate.py +81 -8
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo.egg-info/PKG-INFO +1 -1
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/pyproject.toml +1 -1
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/README.md +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/_routing.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/_site_defaults.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/bootstrap.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/cache.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/cli.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/clients/__init__.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/clients/config.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/clients/datasets.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/clients/images.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/clients/reports.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/clients/scaffold.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/clients/versions.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/concurrency.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/content.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/endpoints.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/errors.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/image.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/loader.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/models.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/paths.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/release.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/repo.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/retry.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/store/__init__.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/store/acr.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/store/base.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/store/oss.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/store/registry.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/tasktoml.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo/transport.py +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo.egg-info/SOURCES.txt +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo.egg-info/dependency_links.txt +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo.egg-info/entry_points.txt +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo.egg-info/requires.txt +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo.egg-info/top_level.txt +0 -0
- {instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/setup.cfg +0 -0
|
@@ -21,7 +21,9 @@ from ..layout import Layout
|
|
|
21
21
|
from ..models import (
|
|
22
22
|
DatasetInstance, ManifestEntry, OwnerReference, PushResult, Feedback,
|
|
23
23
|
)
|
|
24
|
-
from ..validate import check_layout, check_swe_json,
|
|
24
|
+
from ..validate import (check_layout, check_swe_json, check_custom,
|
|
25
|
+
detect_format, validate_instance_id,
|
|
26
|
+
CUSTOM_SCHEMA_VERSION)
|
|
25
27
|
|
|
26
28
|
logger = logging.getLogger(__name__)
|
|
27
29
|
|
|
@@ -96,6 +98,11 @@ class InstancesClient:
|
|
|
96
98
|
return self._push_swe(local_path, dataset, version, split=split,
|
|
97
99
|
overwrite=overwrite, register=register,
|
|
98
100
|
verify=verify)
|
|
101
|
+
if fmt == "custom":
|
|
102
|
+
return self._push_custom(local_path, dataset, version, split=split,
|
|
103
|
+
push_image=push_image,
|
|
104
|
+
overwrite=overwrite, register=register,
|
|
105
|
+
verify=verify)
|
|
99
106
|
# ── harbor 路径 ──
|
|
100
107
|
report = check_layout(local_path) # ① 本地校验(缺文件即抛)
|
|
101
108
|
instance_id = report.instance_id
|
|
@@ -147,23 +154,30 @@ class InstancesClient:
|
|
|
147
154
|
instance_status="draft",
|
|
148
155
|
)
|
|
149
156
|
uid = instance_id
|
|
150
|
-
if register:
|
|
151
|
-
registered = self._reg.register(meta) # 校验 schema(owner_reference 可选)
|
|
152
|
-
uid = registered.id or instance_id
|
|
153
|
-
|
|
154
|
-
image_pushed = False
|
|
155
|
-
if push_image and has_image:
|
|
156
|
-
# ③ 推 ACR(final 改写 Dockerfile)
|
|
157
|
-
self._image.push(image_tar, docker_ref, overwrite=overwrite)
|
|
158
|
-
rewrite_dockerfile(dockerfile, docker_ref, tar_role="final")
|
|
159
|
-
image_pushed = True
|
|
160
157
|
|
|
161
|
-
#
|
|
158
|
+
# 打包 + 构造 manifest(本地)→ register(pending) → 推 ACR → 上传 OSS。
|
|
159
|
+
# register 需要 manifest:apiserver 的 hasFile 校验要求至少一条
|
|
160
|
+
# content_type=file 项,manifest 缺失时 register 被 92005 拒(曾导致
|
|
161
|
+
# custom/harbor 主路径 push(register=True) 失败)。register 仍先于任何
|
|
162
|
+
# 远程写(元数据先行),manifest 在本地打包时即可算出 digest。
|
|
162
163
|
with tempfile.TemporaryDirectory(prefix="irepo_") as td:
|
|
163
164
|
tgz = Path(td) / "content.tgz"
|
|
164
165
|
digest = pack(local_path, tgz, exclude_image_tar=True)
|
|
165
166
|
meta.manifest = _manifest(self._p.oss_bucket, dataset, version,
|
|
166
167
|
instance_id, split, digest, lay)
|
|
168
|
+
|
|
169
|
+
if register:
|
|
170
|
+
registered = self._reg.register(meta) # 校验 schema(owner_reference 可选)
|
|
171
|
+
uid = registered.id or instance_id
|
|
172
|
+
|
|
173
|
+
image_pushed = False
|
|
174
|
+
if push_image and has_image:
|
|
175
|
+
# ③ 推 ACR(final 改写 Dockerfile)
|
|
176
|
+
self._image.push(image_tar, docker_ref, overwrite=overwrite)
|
|
177
|
+
rewrite_dockerfile(dockerfile, docker_ref, tar_role="final")
|
|
178
|
+
image_pushed = True
|
|
179
|
+
|
|
180
|
+
# ④ 上传 OSS(排除 image.tar.gz);{id}.json 供 pull 校验 / ingest 扫描
|
|
167
181
|
put = self._content.put(instance_id, dataset, version, split, tgz,
|
|
168
182
|
meta.to_canonical_json(), overwrite=overwrite,
|
|
169
183
|
layout=lay)
|
|
@@ -272,6 +286,120 @@ class InstancesClient:
|
|
|
272
286
|
docker_image=docker_image or None,
|
|
273
287
|
digests={"instance.json": digest})
|
|
274
288
|
|
|
289
|
+
def _push_custom(self, local_path: Path, dataset: str, version: str, *,
|
|
290
|
+
split: str = "default", push_image: bool = True,
|
|
291
|
+
overwrite: bool = False, register: bool = True,
|
|
292
|
+
verify: bool = True) -> PushResult:
|
|
293
|
+
"""推一个 custom 格式实例:validate → 上传元数据 JSON → (目录输入时) 上传资产。
|
|
294
|
+
|
|
295
|
+
custom 格式的两种输入:
|
|
296
|
+
|
|
297
|
+
- **目录**:SDK 生成最小元数据 JSON(``instance_id`` / ``metadata_model`` /
|
|
298
|
+
``schema_version`` / ``created_at`` / ``updated_at``)上传到 ``{split}/{id}.json``,
|
|
299
|
+
目录内容**原样**逐文件上传到 ``{split}-assets/{id}/`` 前缀下(保持相对路径)。
|
|
300
|
+
- **``.json`` 文件**:校验后**原样**上传到 ``{split}/{id}.json``(无资产)。
|
|
301
|
+
|
|
302
|
+
custom 格式**不参与镜像流程**:``push_image=True`` 时告警并忽略(与 SWE 同口径)。
|
|
303
|
+
"""
|
|
304
|
+
import datetime as _dt
|
|
305
|
+
import warnings as _warnings
|
|
306
|
+
|
|
307
|
+
if push_image:
|
|
308
|
+
_warnings.warn(
|
|
309
|
+
"custom 格式不支持 ACR 镜像推送(push_image=True 已忽略);"
|
|
310
|
+
"如需镜像请用 harbor 格式",
|
|
311
|
+
UserWarning, stacklevel=3)
|
|
312
|
+
|
|
313
|
+
report = check_custom(local_path) # ① 校验(含 instance_id 安全检查)
|
|
314
|
+
instance_id = report.instance_id
|
|
315
|
+
lay = self._layout()
|
|
316
|
+
idx_key = lay.metadata_key(dataset, instance_id, version=version, split=split)
|
|
317
|
+
sts_prefix = lay.sts_prefix(dataset)
|
|
318
|
+
|
|
319
|
+
if report.source == "json":
|
|
320
|
+
# JSON 输入:原样上传
|
|
321
|
+
raw_bytes = local_path.read_bytes()
|
|
322
|
+
meta_digest = "sha256:" + hashlib.sha256(raw_bytes).hexdigest()
|
|
323
|
+
put = self._content.put_object(
|
|
324
|
+
idx_key, raw_bytes, overwrite=overwrite,
|
|
325
|
+
sts_prefix=sts_prefix, dataset=dataset)
|
|
326
|
+
skipped = bool(put.get("skipped"))
|
|
327
|
+
asset_digests: dict[str, str] = {}
|
|
328
|
+
else:
|
|
329
|
+
# 目录输入:生成最小元数据 JSON + 原样上传资产
|
|
330
|
+
now_iso = _dt.datetime.now(_dt.timezone.utc).isoformat(
|
|
331
|
+
timespec="seconds").replace("+00:00", "Z")
|
|
332
|
+
meta_doc = {
|
|
333
|
+
"instance_id": instance_id,
|
|
334
|
+
"metadata_model": "custom",
|
|
335
|
+
"schema_version": CUSTOM_SCHEMA_VERSION,
|
|
336
|
+
"created_at": now_iso,
|
|
337
|
+
"updated_at": now_iso,
|
|
338
|
+
}
|
|
339
|
+
raw_bytes = json.dumps(meta_doc, ensure_ascii=False).encode("utf-8")
|
|
340
|
+
meta_digest = "sha256:" + hashlib.sha256(raw_bytes).hexdigest()
|
|
341
|
+
put = self._content.put_object(
|
|
342
|
+
idx_key, raw_bytes, overwrite=overwrite,
|
|
343
|
+
sts_prefix=sts_prefix, dataset=dataset)
|
|
344
|
+
skipped = bool(put.get("skipped"))
|
|
345
|
+
|
|
346
|
+
# 原样上传目录内所有文件到 {split}-assets/{id}/<relpath>
|
|
347
|
+
asset_prefix = (f"{lay.dataset_root(dataset)}"
|
|
348
|
+
f"{split}-assets/{instance_id}/")
|
|
349
|
+
asset_digests = {}
|
|
350
|
+
for child in sorted(local_path.rglob("*")):
|
|
351
|
+
if not child.is_file():
|
|
352
|
+
continue
|
|
353
|
+
rel = child.relative_to(local_path).as_posix()
|
|
354
|
+
key = asset_prefix + rel
|
|
355
|
+
data = child.read_bytes()
|
|
356
|
+
digest = "sha256:" + hashlib.sha256(data).hexdigest()
|
|
357
|
+
asset_digests[rel] = digest
|
|
358
|
+
aput = self._content.put_object(
|
|
359
|
+
key, data, overwrite=overwrite,
|
|
360
|
+
sts_prefix=sts_prefix, dataset=dataset)
|
|
361
|
+
if not skipped and bool(aput.get("skipped")):
|
|
362
|
+
skipped = True # 任一被 skip 即整体视为 skip
|
|
363
|
+
|
|
364
|
+
# ③ 回读校验(防静默成功)
|
|
365
|
+
if verify and not skipped:
|
|
366
|
+
if not self._content.object_exists(idx_key, sts_prefix=sts_prefix,
|
|
367
|
+
dataset=dataset):
|
|
368
|
+
raise TransportError(
|
|
369
|
+
f"{instance_id}: OSS 回读校验失败——上传后未在 OSS 找到 {idx_key}")
|
|
370
|
+
|
|
371
|
+
# ④ 可选 register/commit(manifest 列出元数据 + 每个资产文件的相对路径)
|
|
372
|
+
manifest = [ManifestEntry(
|
|
373
|
+
file_path=P.oss_uri(self._p.oss_bucket, idx_key),
|
|
374
|
+
content_type="file", content_format="json",
|
|
375
|
+
content_digest=meta_digest)]
|
|
376
|
+
for rel, digest in asset_digests.items():
|
|
377
|
+
manifest.append(ManifestEntry(
|
|
378
|
+
file_path=P.oss_uri(self._p.oss_bucket,
|
|
379
|
+
f"{asset_prefix}{rel}"),
|
|
380
|
+
content_type="file", content_format="raw",
|
|
381
|
+
content_digest=digest))
|
|
382
|
+
|
|
383
|
+
if register:
|
|
384
|
+
meta = DatasetInstance(
|
|
385
|
+
instance_id=instance_id, dataset=dataset,
|
|
386
|
+
dataset_version=version, split=split,
|
|
387
|
+
manifest=manifest,
|
|
388
|
+
extend_meta={"metadata_model": "custom",
|
|
389
|
+
"schema_version": CUSTOM_SCHEMA_VERSION},
|
|
390
|
+
instance_status="draft",
|
|
391
|
+
)
|
|
392
|
+
registered = self._reg.register(meta)
|
|
393
|
+
uid = registered.id or instance_id
|
|
394
|
+
digests_for_commit = {"instance.json": meta_digest}
|
|
395
|
+
digests_for_commit.update(asset_digests)
|
|
396
|
+
self._reg.commit(uid, digests_for_commit, manifest=manifest)
|
|
397
|
+
|
|
398
|
+
return PushResult(instance_id=instance_id, uploaded=not skipped,
|
|
399
|
+
skipped=skipped, image_pushed=False,
|
|
400
|
+
docker_image=None,
|
|
401
|
+
digests={"instance.json": meta_digest})
|
|
402
|
+
|
|
275
403
|
# ── 上架前:把生产副本的 task.toml 指向生产镜像(本地重写,不触网)──────────
|
|
276
404
|
def retarget_prod_content(self, local_dir, dataset: str, version: str,
|
|
277
405
|
split: str, target_ref: str, *,
|
|
@@ -37,9 +37,13 @@ from .errors import SchemaError
|
|
|
37
37
|
|
|
38
38
|
MODEL_VERSION_FIRST = "version_first"
|
|
39
39
|
MODEL_SPLIT_FIRST = "split_first"
|
|
40
|
+
#: ``custom`` 是用户自定义格式的元数据模型:OSS 路径沿用 split_first 约定
|
|
41
|
+
#: ({split}/{id}.json 元数据 + {split}-assets/{id}/ 原样目录资产),SDK 不校验具体
|
|
42
|
+
#: 字段(仅强约束 instance_id),元数据 freeform。
|
|
43
|
+
MODEL_CUSTOM = "custom"
|
|
40
44
|
#: v0.8 起默认 split_first(v0.7.x 行为等价于显式传 version_first)。
|
|
41
45
|
DEFAULT_METADATA_MODEL = MODEL_SPLIT_FIRST
|
|
42
|
-
_MODELS = (MODEL_VERSION_FIRST, MODEL_SPLIT_FIRST)
|
|
46
|
+
_MODELS = (MODEL_VERSION_FIRST, MODEL_SPLIT_FIRST, MODEL_CUSTOM)
|
|
43
47
|
|
|
44
48
|
# ACR tag 合法字符集与长度(OCI/Docker 约定):首字符须为字母数字或下划线,
|
|
45
49
|
# 其余可含字母数字、下划线、点、连字符,总长 **< 105**(ACR tag 硬上限 128,
|
|
@@ -126,7 +130,9 @@ class Layout:
|
|
|
126
130
|
|
|
127
131
|
@property
|
|
128
132
|
def split_first(self) -> bool:
|
|
129
|
-
|
|
133
|
+
# custom 沿用 split_first 的路径约定({split}/{id}.json +
|
|
134
|
+
# {split}-assets/{id}/ 兄弟目录),故 path 构造上一视同仁。
|
|
135
|
+
return self.model in (MODEL_SPLIT_FIRST, MODEL_CUSTOM)
|
|
130
136
|
|
|
131
137
|
# ── 目录基座 ──
|
|
132
138
|
def dataset_root(self, dataset: str) -> str:
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
"""validate.py — Instance 本地校验(Layout / SWE JSON),纯本地无网络。
|
|
1
|
+
"""validate.py — Instance 本地校验(Layout / SWE JSON / Custom),纯本地无网络。
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
- harbor
|
|
3
|
+
三种格式自动检测(detect_format):
|
|
4
|
+
- harbor(目录 + task.toml):instruction.md · task.toml · environment/Dockerfile · tests/test.sh|test.ps1
|
|
5
5
|
- swe(单 JSON 文件):instance_id + docker_image 必备;数据和元数据合一
|
|
6
|
+
- custom(目录无 task.toml / 单 JSON 文件 freeform):仅强约束 instance_id;
|
|
7
|
+
元数据 freeform,push 时目录输入由 SDK 生成最小 JSON、JSON 输入原样上传
|
|
6
8
|
|
|
7
9
|
测试脚本兼容 Linux(tests/test.sh)与 Windows(tests/test.ps1),二者至少其一。
|
|
8
10
|
"""
|
|
@@ -50,21 +52,25 @@ def validate_instance_id(instance_id: str, source: str = "") -> None:
|
|
|
50
52
|
|
|
51
53
|
# ── 格式检测 ─────────────────────────────────────────────────────────────────
|
|
52
54
|
|
|
53
|
-
def detect_format(local_path: str | Path) -> Literal["harbor", "swe"]:
|
|
54
|
-
"""
|
|
55
|
+
def detect_format(local_path: str | Path) -> Literal["harbor", "swe", "custom"]:
|
|
56
|
+
"""自动检测输入格式:
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
- 目录 + 含 task.toml → harbor
|
|
59
|
+
- 目录 + 不含 task.toml → custom(用户自定义格式,SDK 不校验具体字段)
|
|
60
|
+
- .json 文件 → swe
|
|
61
|
+
|
|
62
|
+
不做内容校验(那是 check_layout / check_swe_json / check_custom 的责任)。
|
|
57
63
|
"""
|
|
58
64
|
p = Path(local_path)
|
|
59
65
|
if p.is_dir():
|
|
60
|
-
return "harbor"
|
|
66
|
+
return "harbor" if (p / "task.toml").is_file() else "custom"
|
|
61
67
|
if p.is_file() and p.suffix.lower() == ".json":
|
|
62
68
|
return "swe"
|
|
63
69
|
if not p.exists():
|
|
64
70
|
raise LayoutError(f"path does not exist: {p}")
|
|
65
71
|
raise SchemaError(
|
|
66
72
|
f"unrecognized instance format: {p} "
|
|
67
|
-
f"(expected a directory for harbor or a .json file for swe)")
|
|
73
|
+
f"(expected a directory for harbor/custom or a .json file for swe)")
|
|
68
74
|
|
|
69
75
|
|
|
70
76
|
# ── Harbor 格式校验 ──────────────────────────────────────────────────────────
|
|
@@ -204,3 +210,70 @@ def check_swe_json(json_path: str | Path) -> SWEValidationReport:
|
|
|
204
210
|
return SWEValidationReport(
|
|
205
211
|
instance_id=instance_id, ok=True, warnings=warnings,
|
|
206
212
|
metadata=metadata, raw_data=data)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ── Custom 格式校验(用户自定义,freeform) ─────────────────────────────────
|
|
216
|
+
|
|
217
|
+
CUSTOM_SCHEMA_VERSION = "1.0"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@dataclass
|
|
221
|
+
class CustomValidationReport:
|
|
222
|
+
"""Custom 格式校验结果。"""
|
|
223
|
+
instance_id: str
|
|
224
|
+
format: str = "custom"
|
|
225
|
+
ok: bool = True
|
|
226
|
+
metadata: dict = field(default_factory=dict) # 目录输入时 SDK 生成;JSON 输入时原样解析
|
|
227
|
+
raw_data: dict = field(default_factory=dict) # JSON 输入时的原始数据;目录输入时为空
|
|
228
|
+
source: str = "directory" # "directory" 或 "json"
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def check_custom(local_path: str | Path) -> CustomValidationReport:
|
|
232
|
+
"""校验 custom 格式输入。
|
|
233
|
+
|
|
234
|
+
两种输入形态:
|
|
235
|
+
|
|
236
|
+
- 目录:``instance_id`` = 目录名(强约束 charset),元数据由 SDK push 时生成
|
|
237
|
+
(最小 schema:``instance_id`` / ``metadata_model`` / ``schema_version`` /
|
|
238
|
+
``created_at`` / ``updated_at``)。
|
|
239
|
+
- ``.json`` 文件:必须含 ``instance_id`` 字段且与文件名 stem 一致;其余字段
|
|
240
|
+
freeform,push 时原样上传。
|
|
241
|
+
"""
|
|
242
|
+
p = Path(local_path)
|
|
243
|
+
if p.is_dir():
|
|
244
|
+
instance_id = p.name
|
|
245
|
+
validate_instance_id(instance_id, source=str(p))
|
|
246
|
+
return CustomValidationReport(
|
|
247
|
+
instance_id=instance_id, source="directory")
|
|
248
|
+
if p.is_file() and p.suffix.lower() == ".json":
|
|
249
|
+
try:
|
|
250
|
+
with open(p, "r", encoding="utf-8") as f:
|
|
251
|
+
data = json.load(f)
|
|
252
|
+
except UnicodeDecodeError as e:
|
|
253
|
+
raise SchemaError(
|
|
254
|
+
f"{p.name}: not valid UTF-8: {e}") from e
|
|
255
|
+
except json.JSONDecodeError as e:
|
|
256
|
+
raise SchemaError(f"{p.name}: invalid JSON: {e}") from e
|
|
257
|
+
if not isinstance(data, dict):
|
|
258
|
+
raise SchemaError(
|
|
259
|
+
f"{p.name}: JSON root must be an object, got {type(data).__name__}")
|
|
260
|
+
raw_id = data.get("instance_id")
|
|
261
|
+
if not raw_id or (isinstance(raw_id, str) and not raw_id.strip()):
|
|
262
|
+
raise SchemaError(f"{p.name}: required field 'instance_id' missing or empty")
|
|
263
|
+
if not isinstance(raw_id, str):
|
|
264
|
+
raise SchemaError(
|
|
265
|
+
f"{p.name}: instance_id must be a string, got {type(raw_id).__name__}")
|
|
266
|
+
instance_id = raw_id.strip()
|
|
267
|
+
validate_instance_id(instance_id, source=p.name)
|
|
268
|
+
stem = p.stem
|
|
269
|
+
if stem != instance_id:
|
|
270
|
+
raise SchemaError(
|
|
271
|
+
f"{p.name}: filename stem '{stem}' does not match instance_id "
|
|
272
|
+
f"'{instance_id}' — file must be named '{instance_id}.json'")
|
|
273
|
+
return CustomValidationReport(
|
|
274
|
+
instance_id=instance_id, source="json",
|
|
275
|
+
metadata=dict(data), raw_data=dict(data))
|
|
276
|
+
if not p.exists():
|
|
277
|
+
raise LayoutError(f"path does not exist: {p}")
|
|
278
|
+
raise SchemaError(
|
|
279
|
+
f"custom format expects a directory or .json file, got: {p}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{instance_repo-1.0.7.dev0 → instance_repo-1.0.8}/instance_repo.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|