archi-cli 0.2.0__tar.gz → 0.2.1__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.
- {archi_cli-0.2.0 → archi_cli-0.2.1}/PKG-INFO +1 -1
- {archi_cli-0.2.0 → archi_cli-0.2.1}/pyproject.toml +1 -1
- {archi_cli-0.2.0 → archi_cli-0.2.1}/tools/archi_tool/engine.py +170 -55
- {archi_cli-0.2.0 → archi_cli-0.2.1}/.gitignore +0 -0
- {archi_cli-0.2.0 → archi_cli-0.2.1}/LICENSE +0 -0
- {archi_cli-0.2.0 → archi_cli-0.2.1}/NOTICE +0 -0
- {archi_cli-0.2.0 → archi_cli-0.2.1}/README.md +0 -0
- {archi_cli-0.2.0 → archi_cli-0.2.1}/tools/archi_tool/__init__.py +0 -0
- {archi_cli-0.2.0 → archi_cli-0.2.1}/tools/archi_tool/cli.py +0 -0
- {archi_cli-0.2.0 → archi_cli-0.2.1}/tools/archi_tool/discovery.py +0 -0
- {archi_cli-0.2.0 → archi_cli-0.2.1}/tools/archi_tool/links.py +0 -0
- {archi_cli-0.2.0 → archi_cli-0.2.1}/tools/archi_tool/model.py +0 -0
- {archi_cli-0.2.0 → archi_cli-0.2.1}/tools/archi_tool/normalize.py +0 -0
- {archi_cli-0.2.0 → archi_cli-0.2.1}/tools/archi_tool/render.py +0 -0
- {archi_cli-0.2.0 → archi_cli-0.2.1}/tools/archi_tool/render_html.py +0 -0
- {archi_cli-0.2.0 → archi_cli-0.2.1}/tools/archi_tool/render_slides.py +0 -0
- {archi_cli-0.2.0 → archi_cli-0.2.1}/tools/archi_tool/validate.py +0 -0
- {archi_cli-0.2.0 → archi_cli-0.2.1}/tools/archi_tool/views.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: archi-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: ar·cli·mate — de CLI in je ArchiMate: deterministisch native .archimate-modellen inspecteren, muteren, valideren en renderen
|
|
5
5
|
Project-URL: Homepage, https://github.com/BureauArchitectuurDigitaleOverheid/ai-assisted-architecting
|
|
6
6
|
Project-URL: Repository, https://github.com/BureauArchitectuurDigitaleOverheid/ai-assisted-architecting
|
|
@@ -8,44 +8,43 @@ There is no smaller headless-only artifact; we download the full distribution
|
|
|
8
8
|
once, cache it per user, and point ARCHI_APP at the extracted binary.
|
|
9
9
|
|
|
10
10
|
The version is pinned so the download is deterministic and reproducible. Bump
|
|
11
|
-
ARCHI_VERSION deliberately.
|
|
11
|
+
ARCHI_VERSION deliberately. archi.io only keeps the latest release online,
|
|
12
|
+
though, so once Archi publishes a newer version the pinned one disappears. In
|
|
13
|
+
that case we fall back to the latest release, with a warning, rather than
|
|
14
|
+
leaving `normalize` broken until someone bumps the pin (ADR 0011).
|
|
12
15
|
"""
|
|
13
16
|
from __future__ import annotations
|
|
14
17
|
|
|
15
18
|
import hashlib
|
|
19
|
+
import json
|
|
16
20
|
import os
|
|
17
21
|
import platform
|
|
18
22
|
import shutil
|
|
19
23
|
import subprocess
|
|
24
|
+
import sys
|
|
20
25
|
import tarfile
|
|
26
|
+
import urllib.error
|
|
21
27
|
import urllib.request
|
|
22
28
|
import zipfile
|
|
23
29
|
from pathlib import Path
|
|
24
30
|
|
|
25
31
|
from .model import ModelError
|
|
26
32
|
|
|
27
|
-
ARCHI_VERSION = "5.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
f"/{ARCHI_VERSION}")
|
|
33
|
+
ARCHI_VERSION = "5.10.0"
|
|
34
|
+
RELEASES = "https://github.com/archimatetool/archi.io/releases"
|
|
35
|
+
RELEASES_API = "https://api.github.com/repos/archimatetool/archi.io/releases"
|
|
31
36
|
|
|
32
|
-
# per platform: (asset filename, relative path to the binary inside
|
|
33
|
-
# extracted tree). Archi bundles its own JRE, so no system Java is needed.
|
|
37
|
+
# per platform: (asset filename pattern, relative path to the binary inside
|
|
38
|
+
# the extracted tree). Archi bundles its own JRE, so no system Java is needed.
|
|
34
39
|
_PLATFORMS = {
|
|
35
|
-
"linux": (
|
|
36
|
-
|
|
37
|
-
"Archi/Archi"),
|
|
38
|
-
"windows": (
|
|
39
|
-
f"Archi-Win64-{ARCHI_VERSION}.zip",
|
|
40
|
-
"Archi/Archi.exe"),
|
|
40
|
+
"linux": ("Archi-Linux64-{version}.tgz", "Archi/Archi"),
|
|
41
|
+
"windows": ("Archi-Win64-{version}.zip", "Archi/Archi.exe"),
|
|
41
42
|
# macOS ships a .dmg; both Intel and Apple Silicon expose the same binary
|
|
42
43
|
# path once the app bundle is copied out of the mounted image
|
|
43
|
-
"darwin-arm64": (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
f"Archi-Mac-{ARCHI_VERSION}.dmg",
|
|
48
|
-
"Archi.app/Contents/MacOS/Archi"),
|
|
44
|
+
"darwin-arm64": ("Archi-Mac-Silicon-{version}.dmg",
|
|
45
|
+
"Archi.app/Contents/MacOS/Archi"),
|
|
46
|
+
"darwin-x86_64": ("Archi-Mac-{version}.dmg",
|
|
47
|
+
"Archi.app/Contents/MacOS/Archi"),
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
|
|
@@ -77,12 +76,27 @@ def _platform_key() -> str:
|
|
|
77
76
|
"Archi handmatig en zet ARCHI_APP.")
|
|
78
77
|
|
|
79
78
|
|
|
79
|
+
def _version_key(version: str) -> tuple:
|
|
80
|
+
return tuple(int(p) if p.isdigit() else 0 for p in version.split("."))
|
|
81
|
+
|
|
82
|
+
|
|
80
83
|
def cached_binary() -> Path | None:
|
|
81
|
-
"""
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
"""An extracted engine binary in the cache, if present: the pinned
|
|
85
|
+
version when available, otherwise the newest cached one (left behind by
|
|
86
|
+
a fallback download), so a fallback does not refetch on every run."""
|
|
87
|
+
_, rel = _PLATFORMS[_platform_key()]
|
|
88
|
+
root = cache_dir()
|
|
89
|
+
pinned = root / ARCHI_VERSION / rel
|
|
90
|
+
if pinned.exists():
|
|
91
|
+
return pinned
|
|
92
|
+
if not root.is_dir():
|
|
93
|
+
return None
|
|
94
|
+
versions = [d.name for d in root.iterdir()
|
|
95
|
+
if d.is_dir() and (d / rel).exists()
|
|
96
|
+
and not d.name.endswith(".staging")]
|
|
97
|
+
if not versions:
|
|
98
|
+
return None
|
|
99
|
+
return root / max(versions, key=_version_key) / rel
|
|
86
100
|
|
|
87
101
|
|
|
88
102
|
def _download(url, dest):
|
|
@@ -92,22 +106,76 @@ def _download(url, dest):
|
|
|
92
106
|
shutil.copyfileobj(response, out)
|
|
93
107
|
|
|
94
108
|
|
|
95
|
-
def
|
|
96
|
-
"""
|
|
97
|
-
|
|
98
|
-
Returns None only when the checksum file can't be fetched (a transient
|
|
99
|
-
network failure shouldn't block install; the download itself is over HTTPS
|
|
100
|
-
from GitHub). If the file *is* fetched but lists no hash for ``asset``,
|
|
101
|
-
that is suspicious (renamed/tampered manifest) and raises rather than
|
|
102
|
-
silently skipping verification.
|
|
103
|
-
"""
|
|
104
|
-
url = f"{RELEASE_BASE}/Archi-{ARCHI_VERSION}-SUMSSHA1"
|
|
109
|
+
def _fetch_text(url) -> str | None:
|
|
110
|
+
"""GET a small text file; None when it does not exist (HTTP 404)."""
|
|
105
111
|
try:
|
|
106
112
|
with urllib.request.urlopen(url) as response:
|
|
107
|
-
|
|
108
|
-
except
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
return response.read().decode("utf-8", "replace")
|
|
114
|
+
except urllib.error.HTTPError as exc:
|
|
115
|
+
if exc.code == 404:
|
|
116
|
+
return None
|
|
117
|
+
raise ModelError(f"Kon {url} niet ophalen: {exc}") from exc
|
|
118
|
+
except OSError as exc:
|
|
119
|
+
raise ModelError(f"Kon {url} niet ophalen: {exc}") from exc
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _tag_candidates(version: str) -> list[str]:
|
|
123
|
+
"""Release tags archi.io has used for a version: "5.9.0" for older
|
|
124
|
+
releases, "5.10_0" (last dot as underscore) since 5.10."""
|
|
125
|
+
head, _, last = version.rpartition(".")
|
|
126
|
+
return [version, f"{head}_{last}"] if head else [version]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _latest_tag() -> str:
|
|
130
|
+
"""Tag of the latest release, read from the /releases/latest redirect
|
|
131
|
+
(no API call, so no rate limit)."""
|
|
132
|
+
try:
|
|
133
|
+
with urllib.request.urlopen(f"{RELEASES}/latest") as response:
|
|
134
|
+
final_url = response.geturl()
|
|
135
|
+
except OSError as exc:
|
|
136
|
+
raise ModelError(
|
|
137
|
+
f"Kon de nieuwste Archi-release niet bepalen: {exc}") from exc
|
|
138
|
+
tag = final_url.rstrip("/").rsplit("/", 1)[-1]
|
|
139
|
+
if "/releases/tag/" not in final_url or not tag:
|
|
140
|
+
raise ModelError(
|
|
141
|
+
f"Kon de nieuwste Archi-release niet bepalen uit {final_url}")
|
|
142
|
+
return tag
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _checksums_url(tag: str, version: str) -> str:
|
|
146
|
+
return f"{RELEASES}/download/{tag}/Archi-{version}-SUMSSHA1"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def resolve_release(*, quiet=False) -> tuple[str, str, str]:
|
|
150
|
+
"""(version, tag, SUMSSHA1 text) of the release to download.
|
|
151
|
+
|
|
152
|
+
The pinned version when it is still online, else the latest release. The
|
|
153
|
+
checksum file doubles as the existence probe, and fetching it first means
|
|
154
|
+
every download is verified.
|
|
155
|
+
"""
|
|
156
|
+
for tag in _tag_candidates(ARCHI_VERSION):
|
|
157
|
+
sums = _fetch_text(_checksums_url(tag, ARCHI_VERSION))
|
|
158
|
+
if sums is not None:
|
|
159
|
+
return ARCHI_VERSION, tag, sums
|
|
160
|
+
tag = _latest_tag()
|
|
161
|
+
version = tag.replace("_", ".")
|
|
162
|
+
sums = _fetch_text(_checksums_url(tag, version))
|
|
163
|
+
if sums is None:
|
|
164
|
+
raise ModelError(
|
|
165
|
+
f"Archi {ARCHI_VERSION} staat niet meer online en de nieuwste "
|
|
166
|
+
f"release ({tag}) heeft geen checksumbestand. Installeer Archi "
|
|
167
|
+
"handmatig en zet ARCHI_APP.")
|
|
168
|
+
if not quiet:
|
|
169
|
+
print(f"WAARSCHUWING: Archi {ARCHI_VERSION} staat niet meer online; "
|
|
170
|
+
f"de nieuwste versie {version} wordt gebruikt.", file=sys.stderr)
|
|
171
|
+
return version, tag, sums
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _expected_sha1(asset: str, sums: str) -> str:
|
|
175
|
+
"""The hash for ``asset`` from a SUMSSHA1 text. A checksum file that does
|
|
176
|
+
not list the asset is suspicious (renamed or tampered manifest), so that
|
|
177
|
+
raises rather than skipping verification."""
|
|
178
|
+
for line in sums.splitlines():
|
|
111
179
|
parts = line.split()
|
|
112
180
|
if len(parts) == 2 and parts[1].lstrip("*") == asset:
|
|
113
181
|
return parts[0].lower()
|
|
@@ -116,14 +184,63 @@ def _expected_sha1(asset) -> str | None:
|
|
|
116
184
|
"afgebroken.")
|
|
117
185
|
|
|
118
186
|
|
|
119
|
-
def
|
|
120
|
-
digest = hashlib.
|
|
187
|
+
def _hash(path, algorithm: str) -> str:
|
|
188
|
+
digest = hashlib.new(algorithm)
|
|
121
189
|
with open(path, "rb") as f:
|
|
122
190
|
for chunk in iter(lambda: f.read(1 << 20), b""):
|
|
123
191
|
digest.update(chunk)
|
|
124
192
|
return digest.hexdigest()
|
|
125
193
|
|
|
126
194
|
|
|
195
|
+
def _sha1(path) -> str:
|
|
196
|
+
return _hash(path, "sha1")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _github_sha256(tag: str, asset: str) -> str | None:
|
|
200
|
+
"""The SHA-256 that GitHub computed for ``asset`` when it was uploaded,
|
|
201
|
+
from the releases API; None when the API is unreachable or rate limited.
|
|
202
|
+
A token in GITHUB_TOKEN or GH_TOKEN (set in CI) raises the rate limit."""
|
|
203
|
+
request = urllib.request.Request(
|
|
204
|
+
f"{RELEASES_API}/tags/{tag}",
|
|
205
|
+
headers={"Accept": "application/vnd.github+json"})
|
|
206
|
+
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
|
|
207
|
+
if token:
|
|
208
|
+
request.add_header("Authorization", f"Bearer {token}")
|
|
209
|
+
try:
|
|
210
|
+
with urllib.request.urlopen(request) as response:
|
|
211
|
+
release = json.load(response)
|
|
212
|
+
except (OSError, ValueError):
|
|
213
|
+
return None
|
|
214
|
+
for item in release.get("assets", []):
|
|
215
|
+
digest = item.get("digest") or ""
|
|
216
|
+
if item.get("name") == asset and digest.startswith("sha256:"):
|
|
217
|
+
return digest.removeprefix("sha256:").lower()
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _verify(archive, asset: str, expected_sha1: str, tag: str,
|
|
222
|
+
*, quiet=False):
|
|
223
|
+
"""Check the download against archi.io's SUMSSHA1 list. That list has
|
|
224
|
+
been wrong before (5.10.0 lists a stale hash for the Windows zip), so a
|
|
225
|
+
mismatch is checked against the digest GitHub itself recorded at upload
|
|
226
|
+
before it fails: a file that matches that is byte-identical to the
|
|
227
|
+
published asset."""
|
|
228
|
+
actual = _sha1(archive)
|
|
229
|
+
if actual == expected_sha1:
|
|
230
|
+
return
|
|
231
|
+
github = _github_sha256(tag, asset)
|
|
232
|
+
if github is not None and _hash(archive, "sha256") == github:
|
|
233
|
+
if not quiet:
|
|
234
|
+
print(f"WAARSCHUWING: de checksumlijst van Archi noemt voor "
|
|
235
|
+
f"{asset} een verouderde hash; de download is geverifieerd "
|
|
236
|
+
"tegen de SHA-256 die GitHub bij de upload vastlegde.",
|
|
237
|
+
file=sys.stderr)
|
|
238
|
+
return
|
|
239
|
+
raise ModelError(
|
|
240
|
+
f"Checksum van {asset} klopt niet (verwacht {expected_sha1}, "
|
|
241
|
+
f"kreeg {actual}); download afgebroken.")
|
|
242
|
+
|
|
243
|
+
|
|
127
244
|
def _extract(archive, key, target):
|
|
128
245
|
"""Unpack the downloaded archive into ``target``; format follows platform."""
|
|
129
246
|
if key == "linux":
|
|
@@ -215,36 +332,34 @@ def find_or_none():
|
|
|
215
332
|
|
|
216
333
|
|
|
217
334
|
def download_engine(*, quiet=False) -> Path:
|
|
218
|
-
"""Download and extract the
|
|
219
|
-
|
|
335
|
+
"""Download and extract the Archi engine into the cache. Returns the
|
|
336
|
+
binary path. Raises ModelError on any failure.
|
|
220
337
|
|
|
221
338
|
Extraction happens in a staging directory and is only moved into place
|
|
222
339
|
atomically once the binary is present, so a crash mid-extract never leaves
|
|
223
340
|
a half-populated cache that later looks complete.
|
|
224
341
|
"""
|
|
342
|
+
cached = cached_binary()
|
|
343
|
+
if cached is not None:
|
|
344
|
+
return cached
|
|
225
345
|
key = _platform_key()
|
|
226
|
-
|
|
227
|
-
|
|
346
|
+
pattern, rel = _PLATFORMS[key]
|
|
347
|
+
version, tag, sums = resolve_release(quiet=quiet)
|
|
348
|
+
asset = pattern.format(version=version)
|
|
349
|
+
expected = _expected_sha1(asset, sums)
|
|
350
|
+
version_dir = cache_dir() / version
|
|
228
351
|
binary = version_dir / rel
|
|
229
|
-
if binary.exists():
|
|
230
|
-
return binary
|
|
231
352
|
|
|
232
|
-
url = f"{
|
|
353
|
+
url = f"{RELEASES}/download/{tag}/{asset}"
|
|
233
354
|
if not quiet:
|
|
234
355
|
print(f"Archi-engine ophalen ({asset}, ~165 MB, eenmalig) …")
|
|
235
356
|
cache_dir().mkdir(parents=True, exist_ok=True)
|
|
236
|
-
staging = version_dir.with_name(f"{
|
|
357
|
+
staging = version_dir.with_name(f"{version}.staging")
|
|
237
358
|
shutil.rmtree(staging, ignore_errors=True)
|
|
238
359
|
archive = staging / "_download" / asset
|
|
239
360
|
try:
|
|
240
361
|
_download(url, archive)
|
|
241
|
-
expected =
|
|
242
|
-
if expected is not None:
|
|
243
|
-
actual = _sha1(archive)
|
|
244
|
-
if actual != expected:
|
|
245
|
-
raise ModelError(
|
|
246
|
-
f"Checksum van {asset} klopt niet (verwacht {expected}, "
|
|
247
|
-
f"kreeg {actual}); download afgebroken.")
|
|
362
|
+
_verify(archive, asset, expected, tag, quiet=quiet)
|
|
248
363
|
_extract(archive, key, staging)
|
|
249
364
|
shutil.rmtree(staging / "_download", ignore_errors=True)
|
|
250
365
|
staged_binary = staging / rel
|
|
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
|