loremfile 0.3.0__tar.gz → 0.3.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.
- {loremfile-0.3.0 → loremfile-0.3.1}/PKG-INFO +4 -2
- {loremfile-0.3.0 → loremfile-0.3.1}/README.md +3 -1
- {loremfile-0.3.0 → loremfile-0.3.1}/pyproject.toml +1 -1
- {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile.egg-info/PKG-INFO +4 -2
- {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile_client/__init__.py +1 -1
- {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile_client/api.py +20 -2
- {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile_client/cli.py +18 -2
- {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile_client/mcp.py +5 -2
- {loremfile-0.3.0 → loremfile-0.3.1}/tests/test_commands.py +75 -1
- {loremfile-0.3.0 → loremfile-0.3.1}/tests/test_mcp.py +10 -0
- {loremfile-0.3.0 → loremfile-0.3.1}/setup.cfg +0 -0
- {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile.egg-info/SOURCES.txt +0 -0
- {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile.egg-info/dependency_links.txt +0 -0
- {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile.egg-info/entry_points.txt +0 -0
- {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile.egg-info/top_level.txt +0 -0
- {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile_client/__main__.py +0 -0
- {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile_client/pytest_plugin.py +0 -0
- {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile_client/tools.json +0 -0
- {loremfile-0.3.0 → loremfile-0.3.1}/tests/test_only_loremfile_dev.py +0 -0
- {loremfile-0.3.0 → loremfile-0.3.1}/tests/test_packaging.py +0 -0
- {loremfile-0.3.0 → loremfile-0.3.1}/tests/test_pytest_plugin.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: loremfile
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Download CC0 sample files from loremfile.dev and verify every byte
|
|
5
5
|
Author: the loremfile.dev contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -46,7 +46,9 @@ correctly by default: every file is verified, and a file that fails is not writt
|
|
|
46
46
|
|
|
47
47
|
Every command takes `--json` for scripts, `--quiet`, and `--catalog-version X.Y.Z` to
|
|
48
48
|
refuse to run if loremfile.dev has moved on. `get` also takes `--force` (overwrite) and
|
|
49
|
-
`--dry-run` (what it would fetch, and how many bytes).
|
|
49
|
+
`--dry-run` (what it would fetch, and how many bytes). `get`, `list` and `verify` leave out the
|
|
50
|
+
`sample.{format}` aliases, which repeat another file's bytes, unless you pass `--include-aliases`;
|
|
51
|
+
a path you name is always fetched.
|
|
50
52
|
|
|
51
53
|
Exit codes: `0` fine, `1` a file failed verification or is missing, `2` the request was
|
|
52
54
|
wrong, `3` loremfile.dev could not be read.
|
|
@@ -26,7 +26,9 @@ correctly by default: every file is verified, and a file that fails is not writt
|
|
|
26
26
|
|
|
27
27
|
Every command takes `--json` for scripts, `--quiet`, and `--catalog-version X.Y.Z` to
|
|
28
28
|
refuse to run if loremfile.dev has moved on. `get` also takes `--force` (overwrite) and
|
|
29
|
-
`--dry-run` (what it would fetch, and how many bytes).
|
|
29
|
+
`--dry-run` (what it would fetch, and how many bytes). `get`, `list` and `verify` leave out the
|
|
30
|
+
`sample.{format}` aliases, which repeat another file's bytes, unless you pass `--include-aliases`;
|
|
31
|
+
a path you name is always fetched.
|
|
30
32
|
|
|
31
33
|
Exit codes: `0` fine, `1` a file failed verification or is missing, `2` the request was
|
|
32
34
|
wrong, `3` loremfile.dev could not be read.
|
|
@@ -11,7 +11,7 @@ build-backend = "setuptools.build_meta"
|
|
|
11
11
|
|
|
12
12
|
[project]
|
|
13
13
|
name = "loremfile"
|
|
14
|
-
version = "0.3.
|
|
14
|
+
version = "0.3.1"
|
|
15
15
|
description = "Download CC0 sample files from loremfile.dev and verify every byte"
|
|
16
16
|
readme = "README.md"
|
|
17
17
|
# A range, and a policy: the floor is the oldest CPython still receiving security fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: loremfile
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Download CC0 sample files from loremfile.dev and verify every byte
|
|
5
5
|
Author: the loremfile.dev contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -46,7 +46,9 @@ correctly by default: every file is verified, and a file that fails is not writt
|
|
|
46
46
|
|
|
47
47
|
Every command takes `--json` for scripts, `--quiet`, and `--catalog-version X.Y.Z` to
|
|
48
48
|
refuse to run if loremfile.dev has moved on. `get` also takes `--force` (overwrite) and
|
|
49
|
-
`--dry-run` (what it would fetch, and how many bytes).
|
|
49
|
+
`--dry-run` (what it would fetch, and how many bytes). `get`, `list` and `verify` leave out the
|
|
50
|
+
`sample.{format}` aliases, which repeat another file's bytes, unless you pass `--include-aliases`;
|
|
51
|
+
a path you name is always fetched.
|
|
50
52
|
|
|
51
53
|
Exit codes: `0` fine, `1` a file failed verification or is missing, `2` the request was
|
|
52
54
|
wrong, `3` loremfile.dev could not be read.
|
|
@@ -153,13 +153,28 @@ def active(document: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
153
153
|
return [e for e in document.get("fixtures", []) if e.get("status", "active") == "active"]
|
|
154
154
|
|
|
155
155
|
|
|
156
|
+
#: A `sample.{format}` alias is a second URL for bytes that already have one. Its manifest entry is
|
|
157
|
+
#: the only place that says so, and the field that does is `generator`; its original is
|
|
158
|
+
#: `generator_params.of`. (`aliasOf`, which 1.15.0 carried until 1.15.1, matches nothing.)
|
|
159
|
+
ALIAS_GENERATOR = "alias.copy"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def is_alias(entry: dict[str, Any]) -> bool:
|
|
163
|
+
return entry.get("generator") == ALIAS_GENERATOR
|
|
164
|
+
|
|
165
|
+
|
|
156
166
|
def select(
|
|
157
167
|
entries: list[dict[str, Any]],
|
|
158
168
|
*,
|
|
159
169
|
paths: list[str] | None = None,
|
|
160
170
|
formats: list[str] | None = None,
|
|
171
|
+
include_aliases: bool = False,
|
|
161
172
|
) -> list[dict[str, Any]]:
|
|
162
|
-
"""The entries a request names, or `Refused` naming what does not exist.
|
|
173
|
+
"""The entries a request names, or `Refused` naming what does not exist.
|
|
174
|
+
|
|
175
|
+
A format selects its files, not the aliases that repeat them, unless `include_aliases`; a
|
|
176
|
+
path that is named is always honoured, alias or not.
|
|
177
|
+
"""
|
|
163
178
|
wanted_paths, wanted_formats = set(paths or []), set(formats or [])
|
|
164
179
|
if not wanted_paths and not wanted_formats:
|
|
165
180
|
raise Refused("name at least one path or format")
|
|
@@ -172,7 +187,10 @@ def select(
|
|
|
172
187
|
if unknown_formats:
|
|
173
188
|
raise Refused(f"no such format: {', '.join(unknown_formats)}")
|
|
174
189
|
chosen = {
|
|
175
|
-
e["path"]: e
|
|
190
|
+
e["path"]: e
|
|
191
|
+
for e in entries
|
|
192
|
+
if e["path"] in wanted_paths
|
|
193
|
+
or (e["format"] in wanted_formats and (include_aliases or not is_alias(e)))
|
|
176
194
|
}
|
|
177
195
|
return [chosen[path] for path in sorted(chosen)]
|
|
178
196
|
|
|
@@ -29,6 +29,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
29
29
|
parser.add_argument("--version", action="version", version=f"loremfile {__version__}")
|
|
30
30
|
subcommands = parser.add_subparsers(dest="command", required=True)
|
|
31
31
|
|
|
32
|
+
def aliases(sub: argparse.ArgumentParser) -> None:
|
|
33
|
+
sub.add_argument(
|
|
34
|
+
"--include-aliases",
|
|
35
|
+
action="store_true",
|
|
36
|
+
help="Also the sample.{format} copies, which repeat another file's bytes.",
|
|
37
|
+
)
|
|
38
|
+
|
|
32
39
|
def shared(sub: argparse.ArgumentParser) -> None:
|
|
33
40
|
sub.add_argument("--json", action="store_true", help="Print one JSON object.")
|
|
34
41
|
sub.add_argument("--quiet", action="store_true", help="Print nothing but failures.")
|
|
@@ -50,18 +57,21 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
50
57
|
get.add_argument("--dest", default=DEFAULT_DEST, type=Path, help=f"Default: {DEFAULT_DEST}")
|
|
51
58
|
get.add_argument("--force", action="store_true", help="Overwrite files already there.")
|
|
52
59
|
get.add_argument("--dry-run", action="store_true", help="List what would be fetched.")
|
|
60
|
+
aliases(get)
|
|
53
61
|
shared(get)
|
|
54
62
|
|
|
55
63
|
listing = subcommands.add_parser("list", help="What is published, without downloading.")
|
|
56
64
|
listing.add_argument("--format", action="append", default=[], metavar="FMT")
|
|
57
65
|
listing.add_argument("--tag", action="append", default=[], metavar="TAG")
|
|
58
66
|
listing.add_argument("--max-bytes", type=int, metavar="N")
|
|
67
|
+
aliases(listing)
|
|
59
68
|
shared(listing)
|
|
60
69
|
|
|
61
70
|
check = subcommands.add_parser("verify", help="Check files on disk against the manifest.")
|
|
62
71
|
check.add_argument("paths", nargs="*", metavar="PATH")
|
|
63
72
|
check.add_argument("--format", action="append", default=[], metavar="FMT")
|
|
64
73
|
check.add_argument("--dest", default=DEFAULT_DEST, type=Path, help=f"Default: {DEFAULT_DEST}")
|
|
74
|
+
aliases(check)
|
|
65
75
|
shared(check)
|
|
66
76
|
|
|
67
77
|
subcommands.add_parser(
|
|
@@ -88,6 +98,7 @@ def run_get(args: argparse.Namespace) -> int:
|
|
|
88
98
|
api.active(api.manifest(catalog_version=args.catalog_version)),
|
|
89
99
|
paths=args.paths,
|
|
90
100
|
formats=args.format,
|
|
101
|
+
include_aliases=args.include_aliases,
|
|
91
102
|
)
|
|
92
103
|
if args.dry_run:
|
|
93
104
|
total = sum(e["bytes"] for e in entries)
|
|
@@ -108,6 +119,8 @@ def run_get(args: argparse.Namespace) -> int:
|
|
|
108
119
|
|
|
109
120
|
def run_list(args: argparse.Namespace) -> int:
|
|
110
121
|
entries = api.active(api.manifest(catalog_version=args.catalog_version))
|
|
122
|
+
if not args.include_aliases:
|
|
123
|
+
entries = [e for e in entries if not api.is_alias(e)]
|
|
111
124
|
if args.format:
|
|
112
125
|
entries = [e for e in entries if e["format"] in set(args.format)]
|
|
113
126
|
if args.tag:
|
|
@@ -124,9 +137,12 @@ def run_list(args: argparse.Namespace) -> int:
|
|
|
124
137
|
def run_verify(args: argparse.Namespace) -> int:
|
|
125
138
|
entries = api.active(api.manifest(catalog_version=args.catalog_version))
|
|
126
139
|
if args.paths or args.format:
|
|
127
|
-
entries = api.select(
|
|
140
|
+
entries = api.select(
|
|
141
|
+
entries, paths=args.paths, formats=args.format, include_aliases=args.include_aliases
|
|
142
|
+
)
|
|
128
143
|
else:
|
|
129
|
-
# Everything the destination actually holds, so `verify` alone checks a directory
|
|
144
|
+
# Everything the destination actually holds, so `verify` alone checks a directory,
|
|
145
|
+
# aliases included: a copy that is on disk was put there on purpose.
|
|
130
146
|
entries = [e for e in entries if (args.dest / Path(e["path"])).is_file()]
|
|
131
147
|
results = [api.verify(entry, args.dest) for entry in entries]
|
|
132
148
|
failing = [r for r in results if not r.ok]
|
|
@@ -148,12 +148,15 @@ def list_fixtures(document: dict[str, Any], args: Any) -> dict[str, Any]: # noq
|
|
|
148
148
|
args.get("limit", MISSING), "limit", low=1, high=MAX_LIMIT, fallback=DEFAULT_LIMIT
|
|
149
149
|
)
|
|
150
150
|
offset = _integer(args.get("offset", MISSING), "offset", low=0, high=None, fallback=0)
|
|
151
|
-
# The same filters as `loremfile list`: any of the formats, any of the tags, all three.
|
|
151
|
+
# The same filters as `loremfile list`: any of the formats, any of the tags, all three. The
|
|
152
|
+
# sample.{format} aliases repeat another file's bytes and stay out; describe_fixture and
|
|
153
|
+
# verify_file still take one by path.
|
|
152
154
|
matches = sorted(
|
|
153
155
|
(
|
|
154
156
|
e
|
|
155
157
|
for e in api.active(document)
|
|
156
|
-
if
|
|
158
|
+
if not api.is_alias(e)
|
|
159
|
+
and (not formats or e["format"] in formats)
|
|
157
160
|
and (not tags or set(tags) & set(e.get("tags") or []))
|
|
158
161
|
and (max_bytes is None or e["bytes"] <= max_bytes)
|
|
159
162
|
),
|
|
@@ -10,7 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
import pytest
|
|
11
11
|
from loremfile_client import api, cli
|
|
12
12
|
|
|
13
|
-
from conftest import BODIES, Handler
|
|
13
|
+
from conftest import ALIASES, BODIES, MANIFEST, Handler
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def run(*argv: str) -> int:
|
|
@@ -28,6 +28,80 @@ def test_get_by_format_takes_every_file_of_it(server: str, dest: Path) -> None:
|
|
|
28
28
|
assert sorted(p.name for p in (dest / "pdf").iterdir()) == ["other.pdf", "small.pdf"]
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
def test_the_fake_manifest_carries_an_alias_the_way_the_real_one_does() -> None:
|
|
32
|
+
"""Control for the tests below: they mean nothing if the alias is not really there."""
|
|
33
|
+
aliases = [e for e in MANIFEST["fixtures"] if e.get("generator") == "alias.copy"]
|
|
34
|
+
assert [e["path"] for e in aliases] == list(ALIASES) == ["pdf/sample.pdf"]
|
|
35
|
+
assert aliases[0]["generator_params"] == {"of": "pdf/small.pdf"}
|
|
36
|
+
assert "aliasOf" not in aliases[0], "docs/04 §1.4: there is no such field"
|
|
37
|
+
assert aliases[0]["sha256"] == next(
|
|
38
|
+
e["sha256"] for e in MANIFEST["fixtures"] if e["path"] == "pdf/small.pdf"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_get_by_format_leaves_the_aliases_out_unless_asked(server: str, dest: Path) -> None:
|
|
43
|
+
assert run("get", "--format", "pdf", "--dest", str(dest)) == cli.OK
|
|
44
|
+
assert not (dest / "pdf" / "sample.pdf").exists()
|
|
45
|
+
assert run("get", "--format", "pdf", "--include-aliases", "--dest", str(dest)) == cli.OK
|
|
46
|
+
assert (dest / "pdf" / "sample.pdf").read_bytes() == BODIES["pdf/small.pdf"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_a_dry_run_counts_the_bytes_it_would_fetch_without_the_duplicate(
|
|
50
|
+
server: str, capsys: pytest.CaptureFixture
|
|
51
|
+
) -> None:
|
|
52
|
+
run("get", "--format", "pdf", "--dry-run", "--json")
|
|
53
|
+
without = json.loads(capsys.readouterr().out)["summary"]
|
|
54
|
+
run("get", "--format", "pdf", "--include-aliases", "--dry-run", "--json")
|
|
55
|
+
with_aliases = json.loads(capsys.readouterr().out)["summary"]
|
|
56
|
+
assert (without["files"], with_aliases["files"]) == (2, 3)
|
|
57
|
+
assert with_aliases["bytes"] - without["bytes"] == len(BODIES["pdf/small.pdf"])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_a_path_named_outright_is_fetched_even_when_it_is_an_alias(server: str, dest: Path) -> None:
|
|
61
|
+
assert run("get", "pdf/sample.pdf", "--dest", str(dest)) == cli.OK
|
|
62
|
+
assert (dest / "pdf" / "sample.pdf").read_bytes() == BODIES["pdf/small.pdf"]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_filtering_on_the_field_the_manifest_does_not_have_would_match_nothing() -> None:
|
|
66
|
+
"""The mistake the 1.15.1 brief warns about, made on purpose: `aliasOf` finds no alias."""
|
|
67
|
+
entries = api.active(MANIFEST)
|
|
68
|
+
assert [e["path"] for e in entries if "aliasOf" in e] == []
|
|
69
|
+
assert [e["path"] for e in entries if api.is_alias(e)] == ["pdf/sample.pdf"]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_list_leaves_the_aliases_out_unless_asked(
|
|
73
|
+
server: str, capsys: pytest.CaptureFixture
|
|
74
|
+
) -> None:
|
|
75
|
+
def listed(*argv: str) -> set[str]:
|
|
76
|
+
run("list", *argv, "--json")
|
|
77
|
+
return {item["path"] for item in json.loads(capsys.readouterr().out)["items"]}
|
|
78
|
+
|
|
79
|
+
assert "pdf/sample.pdf" not in listed()
|
|
80
|
+
assert "pdf/sample.pdf" not in listed("--format", "pdf")
|
|
81
|
+
assert "pdf/sample.pdf" in listed("--format", "pdf", "--include-aliases")
|
|
82
|
+
assert listed("--include-aliases") == listed() | set(ALIASES)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_verify_checks_an_alias_that_is_on_disk_and_by_format_only_with_the_flag(
|
|
86
|
+
server: str, dest: Path
|
|
87
|
+
) -> None:
|
|
88
|
+
assert run("get", "pdf/sample.pdf", "--dest", str(dest)) == cli.OK
|
|
89
|
+
assert run("verify", "--dest", str(dest)) == cli.OK
|
|
90
|
+
(dest / "pdf" / "sample.pdf").write_bytes(b"not what was published")
|
|
91
|
+
assert run("verify", "--dest", str(dest)) == cli.FAILED, "a held alias is checked"
|
|
92
|
+
assert run("verify", "--format", "pdf", "--dest", str(dest)) == cli.FAILED, (
|
|
93
|
+
"small.pdf and other.pdf are missing"
|
|
94
|
+
)
|
|
95
|
+
(dest / "pdf" / "small.pdf").write_bytes(BODIES["pdf/small.pdf"])
|
|
96
|
+
(dest / "pdf" / "other.pdf").write_bytes(BODIES["pdf/other.pdf"])
|
|
97
|
+
assert run("verify", "--format", "pdf", "--dest", str(dest)) == cli.OK, (
|
|
98
|
+
"the bad alias is skipped"
|
|
99
|
+
)
|
|
100
|
+
assert (
|
|
101
|
+
run("verify", "--format", "pdf", "--include-aliases", "--dest", str(dest)) == cli.FAILED
|
|
102
|
+
), "and asked for, it is checked"
|
|
103
|
+
|
|
104
|
+
|
|
31
105
|
def test_a_withdrawn_entry_is_not_offered(server: str, dest: Path) -> None:
|
|
32
106
|
assert run("get", "pdf/withdrawn.pdf", "--dest", str(dest)) == cli.REFUSED
|
|
33
107
|
|
|
@@ -232,6 +232,16 @@ def test_list_fixtures_pages_and_says_where_it_is(child: Client, server: str) ->
|
|
|
232
232
|
assert (last["count"], last["next_offset"]) == (1, None)
|
|
233
233
|
|
|
234
234
|
|
|
235
|
+
def test_list_fixtures_leaves_the_sample_aliases_out_and_describe_still_finds_one(
|
|
236
|
+
child: Client,
|
|
237
|
+
) -> None:
|
|
238
|
+
listed = child.call("list_fixtures", {"format": ["pdf"]})["structuredContent"]
|
|
239
|
+
assert [f["path"] for f in listed["fixtures"]] == ["pdf/other.pdf", "pdf/small.pdf"]
|
|
240
|
+
assert listed["total"] == 2
|
|
241
|
+
described = child.call("describe_fixture", {"path": "pdf/sample.pdf"})["structuredContent"]
|
|
242
|
+
assert described["path"] == "pdf/sample.pdf", "control: the alias exists and can be asked for"
|
|
243
|
+
|
|
244
|
+
|
|
235
245
|
def test_list_fixtures_filters_as_loremfile_list_does(child: Client) -> None:
|
|
236
246
|
def paths(arguments: dict[str, Any]) -> list[str]:
|
|
237
247
|
result = child.call("list_fixtures", arguments)["structuredContent"]
|
|
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
|