1panel-toolkit 0.3.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.
- 1panel_toolkit-0.3.0.dist-info/METADATA +407 -0
- 1panel_toolkit-0.3.0.dist-info/RECORD +47 -0
- 1panel_toolkit-0.3.0.dist-info/WHEEL +4 -0
- 1panel_toolkit-0.3.0.dist-info/entry_points.txt +2 -0
- 1panel_toolkit-0.3.0.dist-info/licenses/LICENSE +21 -0
- p1toolkit/__init__.py +3 -0
- p1toolkit/checks/__init__.py +21 -0
- p1toolkit/checks/compose.py +188 -0
- p1toolkit/checks/forms.py +129 -0
- p1toolkit/checks/hygiene.py +73 -0
- p1toolkit/checks/i18n.py +140 -0
- p1toolkit/checks/lessons.py +111 -0
- p1toolkit/checks/structure.py +140 -0
- p1toolkit/checks/substance.py +55 -0
- p1toolkit/cli.py +890 -0
- p1toolkit/config.py +114 -0
- p1toolkit/dependencies.py +218 -0
- p1toolkit/deploy.py +302 -0
- p1toolkit/depth.py +414 -0
- p1toolkit/errors.py +18 -0
- p1toolkit/evidence.py +523 -0
- p1toolkit/fix.py +394 -0
- p1toolkit/gen.py +728 -0
- p1toolkit/lifecycle.py +143 -0
- p1toolkit/loader.py +110 -0
- p1toolkit/logo.py +174 -0
- p1toolkit/model.py +71 -0
- p1toolkit/net.py +81 -0
- p1toolkit/panel.py +154 -0
- p1toolkit/patterns/__init__.py +250 -0
- p1toolkit/patterns/catalog.py +271 -0
- p1toolkit/patterns/data/patterns.json +724 -0
- p1toolkit/patterns/extract.py +402 -0
- p1toolkit/remote.py +554 -0
- p1toolkit/report.py +96 -0
- p1toolkit/resolve/__init__.py +554 -0
- p1toolkit/resolve/analyze.py +329 -0
- p1toolkit/resolve/sources.py +306 -0
- p1toolkit/rules/__init__.py +99 -0
- p1toolkit/rules/data/decisions.yaml +132 -0
- p1toolkit/rules/data/dependencies.yaml +113 -0
- p1toolkit/rules/data/envkeys.yaml +19 -0
- p1toolkit/rules/data/form-design.yaml +59 -0
- p1toolkit/rules/data/i18n.yaml +354 -0
- p1toolkit/rules/data/lessons.yaml +84 -0
- p1toolkit/rules/data/store.yaml +171 -0
- p1toolkit/selftest.py +211 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Package structure: data.yml layout, required metadata, version dirs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .. import rules
|
|
8
|
+
from ..model import Package
|
|
9
|
+
from ..report import Report
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run(package: Package, report: Report, strict: bool = False) -> None:
|
|
13
|
+
assets(package, report)
|
|
14
|
+
store = rules.store()
|
|
15
|
+
allowed = set(store.get("root_allowed_keys") or [])
|
|
16
|
+
warn_keys = set(store.get("root_warn_keys") or [])
|
|
17
|
+
required = list(store.get("root_required_additional") or [])
|
|
18
|
+
recommended = list(store.get("root_recommended_additional") or [])
|
|
19
|
+
|
|
20
|
+
root_keys = set(package.app)
|
|
21
|
+
for key in sorted(root_keys - allowed):
|
|
22
|
+
if key in warn_keys:
|
|
23
|
+
report.warn(
|
|
24
|
+
"P001",
|
|
25
|
+
f"root data.yml has non-standard top-level key {key!r}",
|
|
26
|
+
"data.yml",
|
|
27
|
+
)
|
|
28
|
+
else:
|
|
29
|
+
report.fail(
|
|
30
|
+
"P001",
|
|
31
|
+
f"root data.yml has unsupported top-level key {key!r}",
|
|
32
|
+
"data.yml",
|
|
33
|
+
)
|
|
34
|
+
for key in sorted(allowed - root_keys):
|
|
35
|
+
if key != "tags":
|
|
36
|
+
report.warn("P002", f"root data.yml is missing top-level {key!r}", "data.yml")
|
|
37
|
+
|
|
38
|
+
additional = package.additional
|
|
39
|
+
for key in required:
|
|
40
|
+
if key not in additional:
|
|
41
|
+
report.fail("P003", f"root additionalProperties.{key} is required", "data.yml")
|
|
42
|
+
for key in recommended:
|
|
43
|
+
if key not in additional:
|
|
44
|
+
report.warn(
|
|
45
|
+
"P004", f"root additionalProperties.{key} is recommended", "data.yml"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
architectures = additional.get("architectures")
|
|
49
|
+
if architectures is not None and not (
|
|
50
|
+
isinstance(architectures, list) and architectures
|
|
51
|
+
):
|
|
52
|
+
report.fail(
|
|
53
|
+
"P005",
|
|
54
|
+
"additionalProperties.architectures must be a non-empty list",
|
|
55
|
+
"data.yml",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
tags = additional.get("tags")
|
|
59
|
+
if tags:
|
|
60
|
+
allowed_tags = rules.allowed_tags()
|
|
61
|
+
unknown = [t for t in tags if t not in allowed_tags]
|
|
62
|
+
if unknown:
|
|
63
|
+
report.warn(
|
|
64
|
+
"P006",
|
|
65
|
+
f"additionalProperties.tags not seen in the store survey: {unknown} "
|
|
66
|
+
"(allowed if intentional, but check for a typo or a localized value)",
|
|
67
|
+
"data.yml",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
app_type = additional.get("type")
|
|
71
|
+
if app_type and str(app_type) not in set(store.get("app_types") or []):
|
|
72
|
+
report.warn(
|
|
73
|
+
"P007",
|
|
74
|
+
f"additionalProperties.type {app_type!r} is outside the documented set "
|
|
75
|
+
f"{store.get('app_types')}",
|
|
76
|
+
"data.yml",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if not package.versions:
|
|
80
|
+
report.fail(
|
|
81
|
+
"P010",
|
|
82
|
+
"no version directory found (expected e.g. 'latest/' with docker-compose.yml)",
|
|
83
|
+
)
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
version_allowed = set(store.get("version_allowed_keys") or [])
|
|
87
|
+
for version in package.versions:
|
|
88
|
+
label = f"{version.name}/data.yml"
|
|
89
|
+
if not version.dir.joinpath("data.yml").exists():
|
|
90
|
+
report.fail("P011", f"{version.name}/ is missing data.yml", label)
|
|
91
|
+
if not version.compose_text:
|
|
92
|
+
report.fail(
|
|
93
|
+
"P012", f"{version.name}/ is missing docker-compose.yml", label
|
|
94
|
+
)
|
|
95
|
+
extra = set(version.data) - version_allowed
|
|
96
|
+
if extra:
|
|
97
|
+
report.fail(
|
|
98
|
+
"P013",
|
|
99
|
+
f"{label} has unsupported top-level keys: {sorted(extra)}",
|
|
100
|
+
label,
|
|
101
|
+
)
|
|
102
|
+
if isinstance(version.data.get("formFields"), list):
|
|
103
|
+
report.fail(
|
|
104
|
+
"P014",
|
|
105
|
+
f"{label} declares formFields at the top level; it belongs under additionalProperties",
|
|
106
|
+
label,
|
|
107
|
+
)
|
|
108
|
+
if "architectures" in version.data or "architectures" in version.additional:
|
|
109
|
+
report.fail(
|
|
110
|
+
"P015",
|
|
111
|
+
f"{label} must not declare architectures (root data.yml owns it)",
|
|
112
|
+
label,
|
|
113
|
+
)
|
|
114
|
+
if not version.form_fields:
|
|
115
|
+
report.warn("P016", f"{label} declares no formFields", label)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def assets(package: Package, report: Report) -> None:
|
|
119
|
+
"""Logo check lives separately so tests can call it in isolation."""
|
|
120
|
+
logo = package.root / "logo.png"
|
|
121
|
+
if not logo.exists():
|
|
122
|
+
report.fail("P020", "logo.png is missing", "logo.png")
|
|
123
|
+
return
|
|
124
|
+
size = _png_size(logo)
|
|
125
|
+
if size is None:
|
|
126
|
+
report.warn("P021", "logo.png is not a readable PNG", "logo.png")
|
|
127
|
+
return
|
|
128
|
+
if size != (180, 180):
|
|
129
|
+
report.warn(
|
|
130
|
+
"P022",
|
|
131
|
+
f"logo.png is {size[0]}x{size[1]}; the store default is 180x180",
|
|
132
|
+
"logo.png",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _png_size(path: Path) -> tuple[int, int] | None:
|
|
137
|
+
raw = path.read_bytes()[:24]
|
|
138
|
+
if len(raw) < 24 or raw[:8] != b"\x89PNG\r\n\x1a\n":
|
|
139
|
+
return None
|
|
140
|
+
return int.from_bytes(raw[16:20], "big"), int.from_bytes(raw[20:24], "big")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Substance checks: does this package look adapted, or just reskinned?
|
|
2
|
+
|
|
3
|
+
These are the cheap, corpus-free half of ``p1 depth``. They catch the package
|
|
4
|
+
that was generated by filling a template and never thinking about the app.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ..depth import profile_app
|
|
10
|
+
from ..model import Package
|
|
11
|
+
from ..report import Report
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run(package: Package, report: Report, strict: bool = False) -> None:
|
|
15
|
+
profile = profile_app(package.root)
|
|
16
|
+
if profile is None:
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
if not profile.app_env_keys:
|
|
20
|
+
report.warn(
|
|
21
|
+
"S001",
|
|
22
|
+
"compose 里没有任何应用自有环境变量;官方语料里平均每个应用有 6 个,"
|
|
23
|
+
"确认这个应用是真的不需要配置,还是照模板填的",
|
|
24
|
+
"docker-compose.yml",
|
|
25
|
+
)
|
|
26
|
+
if not profile.mount_targets:
|
|
27
|
+
report.warn(
|
|
28
|
+
"S002",
|
|
29
|
+
"compose 里没有任何持久化挂载;除非应用确实无状态,否则卸载/升级会丢数据",
|
|
30
|
+
"docker-compose.yml",
|
|
31
|
+
)
|
|
32
|
+
# A package that ships source evidence had its ports traced back to an
|
|
33
|
+
# upstream file, so "looks like a template default" would be noise.
|
|
34
|
+
traced = (package.root / "source-evidence.json").exists()
|
|
35
|
+
if (
|
|
36
|
+
profile.port_pairs
|
|
37
|
+
and not traced
|
|
38
|
+
and not any(
|
|
39
|
+
port not in {"80", "8080", "3000", "8000", "5000"}
|
|
40
|
+
for port in profile.port_pairs
|
|
41
|
+
)
|
|
42
|
+
):
|
|
43
|
+
report.warn(
|
|
44
|
+
"S003",
|
|
45
|
+
f"容器内端口是常见模板默认值 {profile.port_pairs};"
|
|
46
|
+
"官方语料里这个维度有 359 种取值,请回到上游 EXPOSE / 官方 compose 核对",
|
|
47
|
+
"docker-compose.yml",
|
|
48
|
+
)
|
|
49
|
+
if profile.services == 1 and not profile.high_signal_keys:
|
|
50
|
+
report.info(
|
|
51
|
+
"S004",
|
|
52
|
+
"没有使用任何上游特有的运行时字段(security_opt / user / cap_add 等);"
|
|
53
|
+
"87% 的单服务应用也确实如此,仅供参考",
|
|
54
|
+
"docker-compose.yml",
|
|
55
|
+
)
|