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.
Files changed (21) hide show
  1. {loremfile-0.3.0 → loremfile-0.3.1}/PKG-INFO +4 -2
  2. {loremfile-0.3.0 → loremfile-0.3.1}/README.md +3 -1
  3. {loremfile-0.3.0 → loremfile-0.3.1}/pyproject.toml +1 -1
  4. {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile.egg-info/PKG-INFO +4 -2
  5. {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile_client/__init__.py +1 -1
  6. {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile_client/api.py +20 -2
  7. {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile_client/cli.py +18 -2
  8. {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile_client/mcp.py +5 -2
  9. {loremfile-0.3.0 → loremfile-0.3.1}/tests/test_commands.py +75 -1
  10. {loremfile-0.3.0 → loremfile-0.3.1}/tests/test_mcp.py +10 -0
  11. {loremfile-0.3.0 → loremfile-0.3.1}/setup.cfg +0 -0
  12. {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile.egg-info/SOURCES.txt +0 -0
  13. {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile.egg-info/dependency_links.txt +0 -0
  14. {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile.egg-info/entry_points.txt +0 -0
  15. {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile.egg-info/top_level.txt +0 -0
  16. {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile_client/__main__.py +0 -0
  17. {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile_client/pytest_plugin.py +0 -0
  18. {loremfile-0.3.0 → loremfile-0.3.1}/src/loremfile_client/tools.json +0 -0
  19. {loremfile-0.3.0 → loremfile-0.3.1}/tests/test_only_loremfile_dev.py +0 -0
  20. {loremfile-0.3.0 → loremfile-0.3.1}/tests/test_packaging.py +0 -0
  21. {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.0
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.0"
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.0
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.
@@ -1,3 +1,3 @@
1
1
  """A small client for loremfile.dev: download CC0 sample files and verify every byte."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.3.1"
@@ -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 for e in entries if e["path"] in wanted_paths or e["format"] in wanted_formats
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(entries, paths=args.paths, formats=args.format)
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 (not formats or e["format"] in formats)
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