adrlint 0.1.0__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.
adrlint-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: adrlint
3
+ Version: 0.1.0
4
+ Summary: A fast, opinionated linter and formatter for MADR architecture decision records
5
+ Author-email: Na'aman Hirschfeld <nhirschfeld@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Goldziher/adrlint
8
+ Project-URL: Repository, https://github.com/Goldziher/adrlint.git
9
+ Project-URL: Issues, https://github.com/Goldziher/adrlint/issues
10
+ Keywords: adr,architecture-decision-record,madr,linter,markdown,cli,rust
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Documentation
20
+ Classifier: Topic :: Software Development :: Quality Assurance
21
+ Classifier: Topic :: Text Processing
22
+ Classifier: Environment :: Console
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ Requires-Dist: certifi>=2026.7.22
26
+
27
+ # adrlint
28
+
29
+ [![PyPI](https://img.shields.io/pypi/v/adrlint?style=flat-square&color=f59e0b)](https://pypi.org/project/adrlint/)
30
+ [![License: MIT](https://img.shields.io/badge/License-MIT-f59e0b?style=flat-square)](https://github.com/Goldziher/adrlint/blob/main/LICENSE)
31
+
32
+ A fast Rust CLI that lints and formats [MADR](https://adr.github.io/madr/) architecture decision
33
+ records: structure, numbering, Markdown quality, and immutability of accepted decisions.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install adrlint
39
+ ```
40
+
41
+ The first run downloads the pre-compiled binary for your platform and verifies it against the release
42
+ checksum manifest. Prebuilt archives exist for Linux (glibc) `x86_64`/`aarch64`, macOS
43
+ `x86_64`/`aarch64`, and Windows `x86_64`.
44
+
45
+ Set `ADRLINT_BINARY` to use an existing executable instead of downloading one.
46
+
47
+ ## Usage
48
+
49
+ ```bash
50
+ adrlint init # write .adrlint.toml and docs/adr
51
+ adrlint lint # check every ADR
52
+ adrlint new "Use PostgreSQL" # create the next sequential ADR
53
+ adrlint lint --format github # GitHub annotations for CI
54
+ ```
55
+
56
+ ## Documentation
57
+
58
+ Rules, configuration, locking and the full CLI reference:
59
+ [https://github.com/Goldziher/adrlint](https://github.com/Goldziher/adrlint)
60
+
61
+ ## License
62
+
63
+ MIT — see [LICENSE](https://github.com/Goldziher/adrlint/blob/main/LICENSE).
@@ -0,0 +1,37 @@
1
+ # adrlint
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/adrlint?style=flat-square&color=f59e0b)](https://pypi.org/project/adrlint/)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-f59e0b?style=flat-square)](https://github.com/Goldziher/adrlint/blob/main/LICENSE)
5
+
6
+ A fast Rust CLI that lints and formats [MADR](https://adr.github.io/madr/) architecture decision
7
+ records: structure, numbering, Markdown quality, and immutability of accepted decisions.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install adrlint
13
+ ```
14
+
15
+ The first run downloads the pre-compiled binary for your platform and verifies it against the release
16
+ checksum manifest. Prebuilt archives exist for Linux (glibc) `x86_64`/`aarch64`, macOS
17
+ `x86_64`/`aarch64`, and Windows `x86_64`.
18
+
19
+ Set `ADRLINT_BINARY` to use an existing executable instead of downloading one.
20
+
21
+ ## Usage
22
+
23
+ ```bash
24
+ adrlint init # write .adrlint.toml and docs/adr
25
+ adrlint lint # check every ADR
26
+ adrlint new "Use PostgreSQL" # create the next sequential ADR
27
+ adrlint lint --format github # GitHub annotations for CI
28
+ ```
29
+
30
+ ## Documentation
31
+
32
+ Rules, configuration, locking and the full CLI reference:
33
+ [https://github.com/Goldziher/adrlint](https://github.com/Goldziher/adrlint)
34
+
35
+ ## License
36
+
37
+ MIT — see [LICENSE](https://github.com/Goldziher/adrlint/blob/main/LICENSE).
@@ -0,0 +1,5 @@
1
+ """
2
+ adrlint: a fast, opinionated linter and formatter for MADR architecture decision records
3
+ """
4
+
5
+ __version__ = "0.1.0"
@@ -0,0 +1,16 @@
1
+ """
2
+ CLI entry point for adrlint
3
+ """
4
+
5
+ import sys
6
+
7
+ from .downloader import run_adrlint
8
+
9
+
10
+ def main():
11
+ """Main entry point for the CLI."""
12
+ run_adrlint(sys.argv[1:])
13
+
14
+
15
+ if __name__ == "__main__":
16
+ main()
@@ -0,0 +1,252 @@
1
+ """Locate the adrlint executable, downloading and verifying a release asset when needed.
2
+
3
+ Environment
4
+ -----------
5
+ ``ADRLINT_BINARY``
6
+ Absolute path to an already-installed adrlint executable. When set it is used as-is: nothing is
7
+ downloaded and no checksum is checked, so the caller owns the integrity of that file.
8
+
9
+ The downloaded binary is cached under ``~/.cache/adrlint/<version>/``. The directory is private
10
+ (mode 0o700) and sits next to ``<binary>.sha256``, which records the digest of the extracted binary
11
+ and of the archive it came from. A cache hit is only served after both digests are re-checked
12
+ against the file on disk and against the release checksum manifest; otherwise the binary is
13
+ discarded and downloaded again. Archive checksums are always verified before extraction.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import hashlib
19
+ import os
20
+ import platform
21
+ import shutil
22
+ import ssl
23
+ import subprocess
24
+ import sys
25
+ import tarfile
26
+ import tempfile
27
+ import zipfile
28
+ from pathlib import Path
29
+ from urllib.error import URLError
30
+ from urllib.request import Request, urlopen
31
+
32
+ import certifi
33
+
34
+ BASE_URL = "https://github.com/Goldziher/adrlint/releases/download"
35
+
36
+ # A release archive is a few megabytes. Refusing anything larger keeps a hostile or broken
37
+ # response from being buffered into memory.
38
+ MAX_DOWNLOAD_BYTES = 100 * 1024 * 1024
39
+
40
+
41
+ def _platform_triple() -> str:
42
+ system = platform.system().lower()
43
+ machine = platform.machine().lower()
44
+
45
+ if system == "windows":
46
+ if machine in {"amd64", "x86_64"}:
47
+ return "x86_64-pc-windows-gnu"
48
+ if machine in {"x86", "i386", "i686"}:
49
+ raise RuntimeError("32-bit Windows is not supported")
50
+ elif system == "linux":
51
+ if machine in {"amd64", "x86_64"}:
52
+ return "x86_64-unknown-linux-gnu"
53
+ if machine in {"aarch64", "arm64"}:
54
+ return "aarch64-unknown-linux-gnu"
55
+ elif system == "darwin":
56
+ if machine in {"amd64", "x86_64"}:
57
+ return "x86_64-apple-darwin"
58
+ if machine in {"aarch64", "arm64"}:
59
+ return "aarch64-apple-darwin"
60
+
61
+ raise RuntimeError(f"Unsupported platform: {system} {machine}")
62
+
63
+
64
+ def _python_version_to_tag(version: str) -> str:
65
+ if "rc" in version:
66
+ core, suffix = version.split("rc")
67
+ return f"{core}-rc.{suffix}"
68
+ return version
69
+
70
+
71
+ def _asset(version: str) -> tuple[str, str, str]:
72
+ tag = _python_version_to_tag(version)
73
+ triple = _platform_triple()
74
+ ext = "zip" if "windows" in triple else "tar.gz"
75
+ name = f"adrlint-{triple}.{ext}"
76
+ return f"{BASE_URL}/v{tag}/{name}", name, ext
77
+
78
+
79
+ def _get(url: str) -> bytes:
80
+ request = Request(url, headers={"User-Agent": "adrlint-python-wrapper"})
81
+ context = ssl.create_default_context(cafile=certifi.where())
82
+ try:
83
+ with urlopen(request, timeout=30, context=context) as response:
84
+ if response.status != 200:
85
+ raise RuntimeError(f"HTTP {response.status}: {response.reason}")
86
+ declared = response.headers.get("Content-Length")
87
+ if declared is not None and int(declared) > MAX_DOWNLOAD_BYTES:
88
+ raise RuntimeError(f"Refusing to download {url}: {declared} bytes is too large")
89
+
90
+ # Read in bounded chunks so a chunked response cannot buffer past the cap either.
91
+ body = bytearray()
92
+ while chunk := response.read(1024 * 1024):
93
+ body.extend(chunk)
94
+ if len(body) > MAX_DOWNLOAD_BYTES:
95
+ raise RuntimeError(f"Refusing to download {url}: response exceeds {MAX_DOWNLOAD_BYTES} bytes")
96
+ return bytes(body)
97
+ except URLError as exc:
98
+ raise RuntimeError(f"Failed to download {url}: {exc}") from exc
99
+
100
+
101
+ def _digest_for(manifest: str, name: str) -> str | None:
102
+ for line in manifest.splitlines():
103
+ parts = line.split()
104
+ if len(parts) == 2 and parts[1].lstrip("*") == name:
105
+ return parts[0]
106
+ return None
107
+
108
+
109
+ def _expected_digest(version: str, asset_name: str) -> str:
110
+ tag = _python_version_to_tag(version)
111
+ manifest_url = f"{BASE_URL}/v{tag}/adrlint_{tag}_checksums.txt"
112
+ digest = _digest_for(_get(manifest_url).decode("utf-8"), asset_name)
113
+ if digest is None:
114
+ raise RuntimeError(f"No checksum published for {asset_name}")
115
+ return digest
116
+
117
+
118
+ def _download(version: str) -> tuple[bytes, str, str, str]:
119
+ url, asset_name, ext = _asset(version)
120
+ archive = _get(url)
121
+ expected = _expected_digest(version, asset_name)
122
+ actual = hashlib.sha256(archive).hexdigest()
123
+ if actual != expected:
124
+ raise RuntimeError(f"Checksum mismatch for {asset_name}: expected {expected}, got {actual}")
125
+ return archive, asset_name, ext, expected
126
+
127
+
128
+ def _extract(archive: Path, ext: str, destination: Path) -> None:
129
+ if ext == "zip":
130
+ with zipfile.ZipFile(archive) as zf:
131
+ for name in zf.namelist():
132
+ if name.endswith("adrlint") or name.endswith("adrlint.exe"):
133
+ with zf.open(name) as src, destination.open("wb") as dst:
134
+ dst.write(src.read())
135
+ return
136
+ else:
137
+ with tarfile.open(archive, "r:gz") as tar:
138
+ for member in tar.getmembers():
139
+ if member.name.endswith("adrlint") or member.name.endswith("adrlint.exe"):
140
+ with tar.extractfile(member) as src, destination.open("wb") as dst:
141
+ dst.write(src.read())
142
+ return
143
+ raise RuntimeError("Binary not found in downloaded archive")
144
+
145
+
146
+ def _cache_dir(version: str) -> Path:
147
+ cache_dir = Path.home() / ".cache" / "adrlint" / version
148
+ cache_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
149
+ # mkdir's mode is filtered by the umask and is ignored when the directory already exists.
150
+ try:
151
+ os.chmod(cache_dir, 0o700)
152
+ except OSError:
153
+ pass
154
+ return cache_dir
155
+
156
+
157
+ def _sha256_file(path: Path) -> str:
158
+ digest = hashlib.sha256()
159
+ with path.open("rb") as handle:
160
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
161
+ digest.update(chunk)
162
+ return digest.hexdigest()
163
+
164
+
165
+ def _write_cache_file(destination: Path, source: Path, mode: int) -> None:
166
+ """Copy ``source`` into ``destination`` without ever writing through an existing path.
167
+
168
+ The content is written to a fresh ``O_EXCL`` file in the destination directory and then
169
+ renamed into place, so a symlink left at ``destination`` is replaced rather than followed.
170
+ """
171
+ fd, staging = tempfile.mkstemp(dir=str(destination.parent), prefix=f".{destination.name}.")
172
+ try:
173
+ with os.fdopen(fd, "wb") as target, source.open("rb") as origin:
174
+ shutil.copyfileobj(origin, target)
175
+ target.flush()
176
+ os.fsync(target.fileno())
177
+ os.chmod(staging, mode)
178
+ os.replace(staging, destination)
179
+ except BaseException:
180
+ Path(staging).unlink(missing_ok=True)
181
+ raise
182
+
183
+
184
+ def _cache_is_valid(binary_path: Path, record_path: Path, version: str, asset_name: str) -> bool:
185
+ """Check a cached binary against the record written at install time and the live manifest."""
186
+ if not binary_path.is_file() or not record_path.is_file():
187
+ return False
188
+
189
+ published_archive = _expected_digest(version, asset_name)
190
+ record = record_path.read_text(encoding="utf-8")
191
+ if _digest_for(record, asset_name) != published_archive:
192
+ return False
193
+
194
+ recorded_binary = _digest_for(record, binary_path.name)
195
+ if recorded_binary is None:
196
+ return False
197
+
198
+ return _sha256_file(binary_path) == recorded_binary
199
+
200
+
201
+ def ensure_binary() -> str:
202
+ """Ensure the binary is available, downloading if necessary."""
203
+ from . import __version__
204
+
205
+ override = os.getenv("ADRLINT_BINARY")
206
+ if override:
207
+ return override
208
+
209
+ suffix = ".exe" if platform.system().lower() == "windows" else ""
210
+ cache_dir = _cache_dir(__version__)
211
+ binary_path = cache_dir / f"adrlint{suffix}"
212
+ record_path = cache_dir / f"adrlint{suffix}.sha256"
213
+ _, asset_name, _ = _asset(__version__)
214
+
215
+ if binary_path.is_file() and os.access(binary_path, os.X_OK):
216
+ if _cache_is_valid(binary_path, record_path, __version__, asset_name):
217
+ return str(binary_path)
218
+ print("Cached adrlint binary failed verification; reinstalling.", file=sys.stderr)
219
+
220
+ binary_path.unlink(missing_ok=True)
221
+ record_path.unlink(missing_ok=True)
222
+
223
+ print(f"Downloading adrlint binary v{__version__}...", file=sys.stderr)
224
+ archive, asset_name, ext, archive_digest = _download(__version__)
225
+
226
+ with tempfile.TemporaryDirectory() as tmpdir:
227
+ archive_path = Path(tmpdir) / asset_name
228
+ archive_path.write_bytes(archive)
229
+ extracted = Path(tmpdir) / binary_path.name
230
+ _extract(archive_path, ext, extracted)
231
+
232
+ _write_cache_file(binary_path, extracted, 0o755)
233
+ record = f"{_sha256_file(binary_path)} {binary_path.name}\n{archive_digest} {asset_name}\n"
234
+ record_file = Path(tmpdir) / record_path.name
235
+ record_file.write_text(record, encoding="utf-8")
236
+ _write_cache_file(record_path, record_file, 0o600)
237
+
238
+ print("Binary downloaded successfully!", file=sys.stderr)
239
+ return str(binary_path)
240
+
241
+
242
+ def run_adrlint(args: list[str]) -> None:
243
+ """Run the adrlint binary with the given arguments."""
244
+ binary_path = ensure_binary()
245
+
246
+ try:
247
+ result = subprocess.run([binary_path] + args, check=False)
248
+ sys.exit(result.returncode)
249
+ except FileNotFoundError:
250
+ raise RuntimeError(f"Binary not found at {binary_path}")
251
+ except Exception as exc:
252
+ raise RuntimeError(f"Failed to run adrlint: {exc}")
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: adrlint
3
+ Version: 0.1.0
4
+ Summary: A fast, opinionated linter and formatter for MADR architecture decision records
5
+ Author-email: Na'aman Hirschfeld <nhirschfeld@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Goldziher/adrlint
8
+ Project-URL: Repository, https://github.com/Goldziher/adrlint.git
9
+ Project-URL: Issues, https://github.com/Goldziher/adrlint/issues
10
+ Keywords: adr,architecture-decision-record,madr,linter,markdown,cli,rust
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Documentation
20
+ Classifier: Topic :: Software Development :: Quality Assurance
21
+ Classifier: Topic :: Text Processing
22
+ Classifier: Environment :: Console
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ Requires-Dist: certifi>=2026.7.22
26
+
27
+ # adrlint
28
+
29
+ [![PyPI](https://img.shields.io/pypi/v/adrlint?style=flat-square&color=f59e0b)](https://pypi.org/project/adrlint/)
30
+ [![License: MIT](https://img.shields.io/badge/License-MIT-f59e0b?style=flat-square)](https://github.com/Goldziher/adrlint/blob/main/LICENSE)
31
+
32
+ A fast Rust CLI that lints and formats [MADR](https://adr.github.io/madr/) architecture decision
33
+ records: structure, numbering, Markdown quality, and immutability of accepted decisions.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install adrlint
39
+ ```
40
+
41
+ The first run downloads the pre-compiled binary for your platform and verifies it against the release
42
+ checksum manifest. Prebuilt archives exist for Linux (glibc) `x86_64`/`aarch64`, macOS
43
+ `x86_64`/`aarch64`, and Windows `x86_64`.
44
+
45
+ Set `ADRLINT_BINARY` to use an existing executable instead of downloading one.
46
+
47
+ ## Usage
48
+
49
+ ```bash
50
+ adrlint init # write .adrlint.toml and docs/adr
51
+ adrlint lint # check every ADR
52
+ adrlint new "Use PostgreSQL" # create the next sequential ADR
53
+ adrlint lint --format github # GitHub annotations for CI
54
+ ```
55
+
56
+ ## Documentation
57
+
58
+ Rules, configuration, locking and the full CLI reference:
59
+ [https://github.com/Goldziher/adrlint](https://github.com/Goldziher/adrlint)
60
+
61
+ ## License
62
+
63
+ MIT — see [LICENSE](https://github.com/Goldziher/adrlint/blob/main/LICENSE).
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ adrlint/__init__.py
4
+ adrlint/cli.py
5
+ adrlint/downloader.py
6
+ adrlint.egg-info/PKG-INFO
7
+ adrlint.egg-info/SOURCES.txt
8
+ adrlint.egg-info/dependency_links.txt
9
+ adrlint.egg-info/entry_points.txt
10
+ adrlint.egg-info/requires.txt
11
+ adrlint.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ adrlint = adrlint.cli:main
@@ -0,0 +1 @@
1
+ certifi>=2026.7.22
@@ -0,0 +1 @@
1
+ adrlint
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=84.0.0", "wheel>=0.48.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "adrlint"
7
+ version = "0.1.0"
8
+ description = "A fast, opinionated linter and formatter for MADR architecture decision records"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ authors = [
12
+ {name = "Na'aman Hirschfeld", email = "nhirschfeld@gmail.com"}
13
+ ]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
23
+ "Topic :: Documentation",
24
+ "Topic :: Software Development :: Quality Assurance",
25
+ "Topic :: Text Processing",
26
+ "Environment :: Console"
27
+ ]
28
+ keywords = ["adr", "architecture-decision-record", "madr", "linter", "markdown", "cli", "rust"]
29
+ requires-python = ">=3.10"
30
+ dependencies = [
31
+ "certifi>=2026.7.22"
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/Goldziher/adrlint"
36
+ Repository = "https://github.com/Goldziher/adrlint.git"
37
+ Issues = "https://github.com/Goldziher/adrlint/issues"
38
+
39
+ [project.scripts]
40
+ adrlint = "adrlint.cli:main"
41
+
42
+ [tool.setuptools]
43
+ packages = ["adrlint"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+