#!/usr/bin/env python3 """Check a documented subset of model package fields; never load model code.""" import argparse import json import math import sys from pathlib import Path, PurePosixPath def check_package(directory): root = Path(directory).resolve() errors, warnings = [], [] def require(condition, message): if not condition: errors.append(message) def read(name): path = root / name if not path.resolve().is_relative_to(root): raise ValueError(name + ": ссылка выходит за пределы пакета") value = json.loads(path.read_text(encoding="utf-8")) if not isinstance(value, dict): raise ValueError(name + ": ожидается JSON-объект") return value try: info, categories = read("model_info.json"), read("coco_categories.json") except (OSError, ValueError) as error: return {"errors": [str(error)], "warnings": [], "status": "invalid"} for field in ("model_type", "task_type"): value = info.get(field) require(isinstance(value, str) and bool(value.strip()), field + ": требуется строка") if isinstance(value, str) and "<" in value: warnings.append(field + ": учебная заглушка, замените значением runtime") size = info.get("img_size", {}) for axis in ("height", "width"): value = size.get(axis) if isinstance(size, dict) else None require(type(value) is int and value > 0, "img_size." + axis + ": требуется положительный integer") deployment = info.get("deploy_profile", {}) profiles = deployment.get("profiles") if isinstance(deployment, dict) else None valid_profiles = isinstance(profiles, list) and bool(profiles) and all(isinstance(p, str) and p.strip() for p in profiles) require(valid_profiles, "deploy_profile.profiles: требуется непустой string[]") if valid_profiles: require(deployment.get("default_profile") in profiles, "default_profile отсутствует в profiles") if any("<" in p for p in profiles): warnings.append("profiles: учебная заглушка, проверьте профили целевого runtime") weights = info.get("weights_path") require(isinstance(weights, str) and bool(weights), "weights_path: требуется относительный путь") if isinstance(weights, str) and weights: pure = PurePosixPath(weights) if pure.is_absolute() or ".." in pure.parts or "\\" in weights or ":" in weights: errors.append("weights_path: нужен путь внутри пакета через /") elif any(char in weights for char in "*?["): warnings.append("weights_path: шаблоны допустимы в отдельных runtime; этот checker их не раскрывает") else: path = root / weights require(path.resolve().is_relative_to(root), "weights_path: ссылка выходит за пределы пакета") require(path.is_file(), "weights_path: файл не найден") items = categories.get("categories") require(isinstance(items, list), "categories: требуется массив") ids = set() for index, category in enumerate(items if isinstance(items, list) else []): prefix = "categories[" + str(index) + "]" if not isinstance(category, dict): errors.append(prefix + ": требуется объект") continue value = category.get("id") require(type(value) is int, prefix + ".id: требуется integer") if type(value) is int: require(value not in ids, prefix + ".id: повторяется") ids.add(value) for field in ("name", "ru_name", "full_ru_name", "supercategory"): require(isinstance(category.get(field), str), prefix + "." + field + ": требуется строка") threshold = category.get("threshold") require(type(threshold) in (int, float) and math.isfinite(threshold), prefix + ".threshold: требуется конечное число") color = category.get("color") require(isinstance(color, list) and len(color) == 3 and all(type(c) is int and 0 <= c <= 255 for c in color), prefix + ".color: нужны три integer 0…255") for field in ("is_violation", "is_primary", "exclude"): require(type(category.get(field)) is bool, prefix + "." + field + ": требуется boolean") warnings.append("Веса, inference, тип модели, дополнительные файлы и совместимость runtime не проверены") return {"errors": errors, "warnings": warnings, "status": "invalid" if errors else "structure_checked"} def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("directory", help="распакованная директория модели") args = parser.parse_args() result = check_package(args.directory) print(json.dumps(result, ensure_ascii=False, indent=2)) return 1 if result["errors"] else 0 if __name__ == "__main__": sys.exit(main())