raw derive/fetch/pending: Länge als dritte Größenachse beim Ingest - eine CHECK-Zeile nennt jeden geschriebenen Text über LONG_TEXT_CHARS (Zeichen der .md wie geschrieben), raw pending markiert eine lange .md/.txt mit long:; ingest-large-tree liest ihn in Teilen nach der Gliederung, je Teil nur den Zeilenbereich, Extrakt als Faktenliste mit Fundstelle und Modalität (#226); 10.1.0-beta.10
CI / verify (push) Successful in 7m55s
CI / pwsh (push) Successful in 2m41s
Converters / converters (push) Successful in 9m32s
Release / converters (push) Successful in 27s
Release / release (push) Successful in 37s
Release / wiki-mirror (push) Skipped
CI / verify (push) Successful in 7m55s
CI / pwsh (push) Successful in 2m41s
Converters / converters (push) Successful in 9m32s
Release / converters (push) Successful in 27s
Release / release (push) Successful in 37s
Release / wiki-mirror (push) Skipped
Files changed: - CHANGES.md - VERSION - docs/how-to/ingest-a-source.md - docs/reference/commands/raw-material-and-uploads.md - instructions/ingest-large-tree.md - instructions/wiki-ingest/SKILL.md - raw/CONTRACT.md - tools/CONTRACT.md - tools/chemenu/commands/raw/_common.py - tools/chemenu/commands/raw/derive.py - tools/chemenu/commands/raw/fetch.py - tools/chemenu/commands/raw/pending.py - tools/chemenu/tests/test_raw_derive.py - tools/chemenu/tests/test_raw_fetch.py - types/source.guidance.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
This commit is contained in:
1 parent
9cb981a4cd
commit
eae4a427d2
15 files changed
+341
-37
No files matched your search
+5
-2
@@ -1495,6 +1495,7 @@ Capture a web page the user names into `incoming/`: the HTML as received plus a
|
||||
- Fetch routes: the `fetch` section of the settings sends a host's pages another way. `session` fetches them with the cookies of the session the user stored with `raw login`, still with the built-in fetch and the bytes as received; the header adds `session: <host>` and never the session's content. The host's site handler names how a page shows it arrived signed in: no stored session, or a page without that sign (an expired session, a teaser), stops with exit 42 and writes nothing. Any other route names an external fetcher of this checkout: run without a shell with the URL as its last argument (120 s, 25 MiB), its stdout is the `.html` byte for byte, and the header says `fetched_by: wikitool raw fetch (fetcher: <name>)` and carries no `http_status`. A fetcher that fails is never replaced by the built-in fetch. A host no route names is fetched as before.
|
||||
- Rendered pages: a site handler that declares `render` has its pages fetched through a headless browser - with the session, when the host has one - and the `.html` is the page as the browser drew it, not the bytes the server sent; the header says `rendered: true`. No page is rendered without that declaration, and an external fetcher's page never is. It needs the optional browser component; without it the fetch stops with exit 42. `--scope` fetches every page of the bundle the same way, and `_scope.json` names the `session`, `fetcher` or `rendered` it used.
|
||||
- `--html` derives the `.md` beside a page saved to `incoming/` from a logged-in browser - the way past a paywall or a script-rendered page. Its header carries `derived` (when the text was derived) instead of `retrieved`, `final_url`, `http_status` and `content_type`, and the `.html` is left untouched.
|
||||
- Length: a `CHECK` line names every `.md` this run wrote - and a `text/plain` or `text/markdown` response stored as received - that is longer than 50,000 characters, the threshold of `raw derive`: more than one ingest reads in one pass, so it is read in parts (`instructions/ingest-large-tree.md`).
|
||||
- Success prints the `raw accept` line for the written files and the `source_url` for the source page.
|
||||
|
||||
**SEE ALSO**
|
||||
@@ -1613,6 +1614,7 @@ Derive reading text from a PDF, e-mail, Office file or web page in `incoming/`,
|
||||
- A ZIP and a mail are containers. A ZIP at the top of `incoming/` becomes the folder `incoming/<stem>/` holding its files under their own names, each with a `<stem>.md` where a filter reads it; the ZIP is removed and the output names it with its sha256. A mail becomes a folder with `original.eml`, `derived.md` and its attachments under their own names; a ZIP attached to it is unpacked once into a folder of its own, and an attached mail is quoted in `derived.md`, not unpacked. A ZIP inside a ZIP, an absolute or `..` path, a symbolic link, an encrypted entry, more than 1000 entries, a file over 25 MiB or more than 250 MiB in total refuse the whole call. Hidden entries (`__MACOSX/`, `.DS_Store`, anything starting with a dot) are named, not written. A name taken twice, or reserved (`original.*` and `derived.md` at the top of a mail's folder, `_capture.json` anywhere), gets `-2`, `-3` before its extension, and the output lists each rename.
|
||||
- A name already taken under `raw/` is a WARN, not a refusal: whether the folder is a later edition of that source or a separate one is the human's call. A later edition of a derived source is replaced file by file with `raw accept --replaces`; there is no `--replaces-bundle` for it.
|
||||
- After a success the output prints the warnings the filters raised, at most three `CHECK` lines per format that was derived in this run - what to look at before the text is trusted - and the `raw accept` line for the result.
|
||||
- Length: one more `CHECK` line names every `.md` this run wrote that is longer than 50,000 characters, counted in the file as written, header included - more than one ingest reads in one pass, so it is read in parts (`instructions/ingest-large-tree.md`). Characters, not tokens: they do not depend on a model's tokenizer. `raw fetch` and `raw pending` measure against the same value.
|
||||
- Never edit `derived.md` by hand: a better derivation is a new `raw derive` of the original.
|
||||
|
||||
**SEE ALSO**
|
||||
@@ -1766,9 +1768,10 @@ List what waits in `incoming/`, oldest first, and name the entry an ingest witho
|
||||
- Order: oldest first by modification time. A bundle or folder counts as new as its newest file; a tie goes by name. The mtime is when a document last changed only if it was copied with its timestamps kept (`cp -p`, `rsync -a`, an unpacked archive) - for a download or a `raw fetch` it is merely when it was dropped.
|
||||
- Each candidate shows its path(s), kind, file count and mtime, and whether `raw accept` would take it as it stands - the same checks, minus `--fidelity`/`--authority`. One it would refuse is listed with the reason and skipped: it needs a human.
|
||||
- The default is the first candidate `raw accept` would take, and the output names it.
|
||||
- `--json` prints the same candidates in the same order: `kind`, `paths`, `files`, `mtime`, `acceptable`, `reason`, `default`, `hint`.
|
||||
- `--json` prints the same candidates in the same order: `kind`, `paths`, `files`, `mtime`, `acceptable`, `reason`, `default`, `hint`, `length`.
|
||||
- A lone file that a `raw derive` filter reads (a PDF, mail, Office file or web page) is listed with the `raw derive` line for it, in text output and as `hint` in JSON (`null` for every other candidate). The hint changes nothing about the order or the default.
|
||||
- Reads directory listings and `lstat` only, never a file's content; an empty `incoming/` is exit 0 with nothing to do.
|
||||
- A lone `.md` or `.txt` file longer than the length threshold of `raw derive` and `raw fetch` is listed with its length in characters, in text output and as `length` in JSON (`null` for every other candidate) - that text reaches a session with no derivation in front of it to print the length `CHECK`. Like the hint, it changes nothing about the order or the default.
|
||||
- Reads directory listings and `lstat`, and a file's content only to measure a lone `.md`/`.txt` whose size in bytes is over the threshold - a UTF-8 text has no more characters than bytes; an empty `incoming/` is exit 0 with nothing to do.
|
||||
|
||||
**SEE ALSO**
|
||||
|
||||
|
||||
@@ -508,6 +508,56 @@ def _derive_hint(path: Path) -> Optional[str]:
|
||||
return f"tools/wikitool raw derive {rel_path(path)}"
|
||||
|
||||
|
||||
# The length axis of an ingest (Gitea #226): a text longer than this many characters
|
||||
# is read in parts, through `instructions/ingest-large-tree.md`, not in one pass.
|
||||
# Characters rather than tokens, because they do not depend on a model's tokenizer:
|
||||
# ~15 k tokens of German at ~3.3 characters a token, set early because a session's
|
||||
# fixed load already fills much of its context. The value stands here and nowhere
|
||||
# else - `raw derive`, `raw fetch` and `raw pending` all read it from this line.
|
||||
LONG_TEXT_CHARS = 50_000
|
||||
|
||||
# The files `raw pending` measures: text a session reads as it is, with no derivation
|
||||
# - and so no `raw derive` or `raw fetch` - in front of it to print the length CHECK.
|
||||
_PLAIN_TEXT_SUFFIXES = (".md", ".txt")
|
||||
|
||||
|
||||
def _char_count(data: bytes) -> int:
|
||||
"""Characters of a UTF-8 text, the way a session reads it: an undecodable
|
||||
byte counts as the one replacement character it becomes."""
|
||||
return len(data.decode("utf-8", errors="replace"))
|
||||
|
||||
|
||||
def _long_text_check(texts: list[tuple[Path, bytes]]) -> Optional[str]:
|
||||
"""The one `CHECK` line for the texts longer than `LONG_TEXT_CHARS`, else None.
|
||||
A UTF-8 text has no more characters than bytes, so a short one is not decoded."""
|
||||
over = [
|
||||
(path, count) for path, data in texts
|
||||
if len(data) > LONG_TEXT_CHARS and (count := _char_count(data)) > LONG_TEXT_CHARS
|
||||
]
|
||||
if not over:
|
||||
return None
|
||||
named = ", ".join(f"{rel_path(path)} ({count:,} characters)" for path, count in over)
|
||||
return (
|
||||
f"Longer than one ingest reads in one pass ({LONG_TEXT_CHARS:,} characters): {named}. "
|
||||
"Read it in parts - instructions/ingest-large-tree.md, the length axis."
|
||||
)
|
||||
|
||||
|
||||
def _plain_text_length(path: Path) -> Optional[int]:
|
||||
"""The characters of a lone `.md`/`.txt` file when they are over
|
||||
`LONG_TEXT_CHARS`, else None. Reads the file only when its size could put
|
||||
it over - `lstat` decides every other case."""
|
||||
if path.suffix.lower() not in _PLAIN_TEXT_SUFFIXES or path.is_symlink() or not path.is_file():
|
||||
return None
|
||||
if path.lstat().st_size <= LONG_TEXT_CHARS:
|
||||
return None
|
||||
try:
|
||||
count = _char_count(path.read_bytes())
|
||||
except OSError:
|
||||
return None
|
||||
return count if count > LONG_TEXT_CHARS else None
|
||||
|
||||
|
||||
def _unusable_name(name: str) -> bool:
|
||||
"""Not one plain directory name: empty, padded, hidden, a path, a control
|
||||
character, or the manifest's reserved name."""
|
||||
|
||||
@@ -28,7 +28,9 @@ from chemenu.commands.raw._common import (
|
||||
_Refused,
|
||||
_check_directly_in_incoming,
|
||||
_folder_collision_message,
|
||||
LONG_TEXT_CHARS,
|
||||
_incoming_dir,
|
||||
_long_text_check,
|
||||
_occupied_stems,
|
||||
_refusals_fail,
|
||||
_resolve,
|
||||
@@ -284,19 +286,36 @@ def _plan_derive_folder(folder: Path):
|
||||
|
||||
|
||||
def _echo_notes(
|
||||
derivations: list[filters.Derivation], container: Optional[filters.Container] = None
|
||||
derivations: list[filters.Derivation],
|
||||
texts: list[tuple[Path, bytes]],
|
||||
container: Optional[filters.Container] = None,
|
||||
) -> None:
|
||||
"""The filters' warnings and CHECKs, then the length CHECK over `texts` -
|
||||
every `.md` this run wrote, as written."""
|
||||
warnings = [w for d in derivations for w in d.warnings]
|
||||
checks = [c for d in derivations for c in d.checks]
|
||||
if container is not None:
|
||||
warnings += container.warnings
|
||||
checks += container.checks
|
||||
long_text = _long_text_check(texts)
|
||||
if long_text is not None:
|
||||
checks.append(long_text)
|
||||
for warning in filters.unique(warnings):
|
||||
typer.echo(f"WARN {warning}")
|
||||
for line in filters.unique(checks):
|
||||
typer.echo(f"CHECK {line}")
|
||||
|
||||
|
||||
def _written_texts(plan: _FilePlan) -> list[tuple[Path, bytes]]:
|
||||
"""Every `.md` the single-file form writes: `derived.md`, and the `.md`
|
||||
beside each member of a container that a filter read."""
|
||||
texts = [(plan.derived, plan.markdown)] if plan.derived is not None else []
|
||||
if plan.container is not None:
|
||||
members = dict(plan.container.files)
|
||||
texts += [(plan.folder / md, members[md]) for _src, md, _name in plan.container.derived]
|
||||
return texts
|
||||
|
||||
|
||||
def _container_report(container: filters.Container) -> list[str]:
|
||||
lines = [f" {src} -> {md} ({name})" for src, md, name in container.derived]
|
||||
if container.plain:
|
||||
@@ -413,6 +432,11 @@ def _derive_next(target: Path) -> str:
|
||||
"After a success the output prints the warnings the filters raised, at most three "
|
||||
"`CHECK` lines per format that was derived in this run - what to look at before the "
|
||||
"text is trusted - and the `raw accept` line for the result.",
|
||||
f"Length: one more `CHECK` line names every `.md` this run wrote that is longer than "
|
||||
f"{LONG_TEXT_CHARS:,} characters, counted in the file as written, header included - "
|
||||
"more than one ingest reads in one pass, so it is read in parts "
|
||||
"(`instructions/ingest-large-tree.md`). Characters, not tokens: they do not depend on a "
|
||||
"model's tokenizer. `raw fetch` and `raw pending` measure against the same value.",
|
||||
"Never edit `derived.md` by hand: a better derivation is a new `raw derive` of the original.",
|
||||
),
|
||||
failures=(
|
||||
@@ -479,7 +503,7 @@ def raw_derive_command(
|
||||
warning = _collision_warning(resolved.name)
|
||||
if warning:
|
||||
typer.echo("WARN " + warning)
|
||||
_echo_notes([d.derivation for d in derived])
|
||||
_echo_notes([d.derivation for d in derived], [(d.target, d.markdown) for d in derived])
|
||||
typer.echo(_derive_next(resolved))
|
||||
return
|
||||
|
||||
@@ -512,5 +536,5 @@ def raw_derive_command(
|
||||
warning = _collision_warning(plan.stem)
|
||||
if warning:
|
||||
typer.echo("WARN " + warning)
|
||||
_echo_notes(plan.derivations, container)
|
||||
_echo_notes(plan.derivations, _written_texts(plan), container)
|
||||
typer.echo(_derive_next(plan.folder))
|
||||
@@ -34,7 +34,9 @@ from chemenu.errors import ChemenuError
|
||||
from chemenu.version import VersionError, read_version
|
||||
from chemenu.commands.raw._common import (
|
||||
_folder_collision_message,
|
||||
LONG_TEXT_CHARS,
|
||||
_incoming_dir,
|
||||
_long_text_check,
|
||||
_occupied_stems,
|
||||
_resolve,
|
||||
_shard_dir,
|
||||
@@ -154,9 +156,19 @@ def _route_entries(route: fetch_routes.Route) -> dict:
|
||||
return found
|
||||
|
||||
|
||||
def _echo_checks(plan: sites.Plan, derivation: web_capture.Derivation, subpages: int = 0) -> None:
|
||||
def _echo_checks(
|
||||
plan: sites.Plan,
|
||||
derivation: web_capture.Derivation,
|
||||
texts: list[tuple[Path, bytes]],
|
||||
subpages: int = 0,
|
||||
) -> None:
|
||||
"""The site handler's CHECKs for the page, then the length CHECK over
|
||||
`texts` - every `.md` this run wrote, as written."""
|
||||
for line in plan.checks(derivation.decoded.text, subpages):
|
||||
typer.echo(f"CHECK {line}")
|
||||
long_text = _long_text_check(texts)
|
||||
if long_text is not None:
|
||||
typer.echo(f"CHECK {long_text}")
|
||||
|
||||
|
||||
def _fetched(plan: sites.Plan) -> str:
|
||||
@@ -188,7 +200,7 @@ def _fetch_html_file(html: Path, source_url: Optional[str]) -> None:
|
||||
)
|
||||
_write_exclusive([(md_path, derivation.markdown)])
|
||||
_warn(derivation, md_path)
|
||||
_echo_checks(plan, derivation)
|
||||
_echo_checks(plan, derivation, [(md_path, derivation.markdown)])
|
||||
success(escape(
|
||||
f"Derived {rel_path(md_path)} from {rel_path(path)} (no network access).\n"
|
||||
+ _next_lines([path, md_path], source_url)
|
||||
@@ -355,7 +367,7 @@ def _fetch_scope(url: str, name: Optional[str]) -> None:
|
||||
|
||||
for page_name, derivation in derivations:
|
||||
_warn(derivation, target / f"{page_name}.md")
|
||||
_echo_checks(plan, index_derivation)
|
||||
_echo_checks(plan, index_derivation, [(target / f"{n}.md", d.markdown) for n, d in derivations])
|
||||
success(escape(
|
||||
f"Fetched {_fetched(plan)} with {len(urls)} subpage(s) to {rel_path(target)}/ "
|
||||
f"({len(entries)} page pairs and {SCOPE_MANIFEST}).\n"
|
||||
@@ -470,6 +482,10 @@ def _fetch_scope(url: str, name: Optional[str]) -> None:
|
||||
"- the way past a paywall or a script-rendered page. Its header carries `derived` (when "
|
||||
"the text was derived) instead of `retrieved`, `final_url`, `http_status` and "
|
||||
"`content_type`, and the `.html` is left untouched.",
|
||||
f"Length: a `CHECK` line names every `.md` this run wrote - and a `text/plain` or "
|
||||
f"`text/markdown` response stored as received - that is longer than "
|
||||
f"{LONG_TEXT_CHARS:,} characters, the threshold of `raw derive`: more than one ingest "
|
||||
"reads in one pass, so it is read in parts (`instructions/ingest-large-tree.md`).",
|
||||
"Success prints the `raw accept` line for the written files and the `source_url` for the "
|
||||
"source page.",
|
||||
),
|
||||
@@ -628,7 +644,10 @@ def raw_fetch_command(
|
||||
)
|
||||
_write_exclusive([(html_path, response.body), (md_path, derivation.markdown)])
|
||||
_warn(derivation, md_path)
|
||||
_echo_checks(plan, derivation, len(plan.subpages(derivation.decoded.text, response.final_url)))
|
||||
_echo_checks(
|
||||
plan, derivation, [(md_path, derivation.markdown)],
|
||||
len(plan.subpages(derivation.decoded.text, response.final_url)),
|
||||
)
|
||||
anonymous = route.kind == fetch_routes.PLAIN and not route.render
|
||||
success(escape(
|
||||
f"Fetched {_fetched(plan)} to {rel_path(html_path)}, {rel_path(md_path)}\n"
|
||||
@@ -640,6 +659,10 @@ def raw_fetch_command(
|
||||
target = incoming / f"{stem}{web_capture.extension_for(response.content_type)}"
|
||||
_refuse_existing([target])
|
||||
_write_exclusive([(target, response.body)])
|
||||
if kind in web_capture.PLAIN_TEXT_EXTENSIONS:
|
||||
long_text = _long_text_check([(target, response.body)])
|
||||
if long_text is not None:
|
||||
typer.echo(f"CHECK {long_text}")
|
||||
success(escape(
|
||||
f"Fetched {_fetched(plan)} to {rel_path(target)} ({kind or 'no content type'}, stored as received - "
|
||||
f"no text derived; retrieved {web_capture.utc_now()}).\n"
|
||||
|
||||
@@ -22,6 +22,7 @@ import typer
|
||||
from chemenu import cli_contract, repo_capture
|
||||
from chemenu.commands._util import rel_path
|
||||
from chemenu.commands.raw._common import (
|
||||
LONG_TEXT_CHARS,
|
||||
_Refused,
|
||||
_check_files,
|
||||
_derive_hint,
|
||||
@@ -29,6 +30,7 @@ from chemenu.commands.raw._common import (
|
||||
_plan_captured_folder,
|
||||
_plan_file_moves,
|
||||
_plan_folder,
|
||||
_plain_text_length,
|
||||
)
|
||||
|
||||
|
||||
@@ -40,6 +42,7 @@ class _Candidate:
|
||||
mtime_ns: int
|
||||
reason: Optional[str] # None when `raw accept` would take it as it stands
|
||||
hint: Optional[str] = None # the `raw derive` line for a lone file a filter reads
|
||||
length: Optional[int] = None # characters of a lone text file over the length threshold
|
||||
|
||||
def label(self) -> str:
|
||||
return ", ".join(rel_path(p) + ("/" if self.kind == "folder" else "") for p in self.paths)
|
||||
@@ -111,6 +114,7 @@ def _pending_candidates() -> list[_Candidate]:
|
||||
max(p.lstat().st_mtime_ns for p in paths),
|
||||
_refusal(_plain_files, paths),
|
||||
_derive_hint(paths[0]) if len(paths) == 1 else None,
|
||||
_plain_text_length(paths[0]) if len(paths) == 1 else None,
|
||||
))
|
||||
candidates.sort(key=lambda c: (c.mtime_ns, os.fsencode(c.paths[0].name)))
|
||||
return candidates
|
||||
@@ -142,13 +146,19 @@ def _pending_candidates() -> list[_Candidate]:
|
||||
"human.",
|
||||
"The default is the first candidate `raw accept` would take, and the output names it.",
|
||||
"`--json` prints the same candidates in the same order: `kind`, `paths`, `files`, "
|
||||
"`mtime`, `acceptable`, `reason`, `default`, `hint`.",
|
||||
"`mtime`, `acceptable`, `reason`, `default`, `hint`, `length`.",
|
||||
"A lone file that a `raw derive` filter reads (a PDF, mail, Office file or web page) is "
|
||||
"listed with the `raw derive` line for it, in text output and as `hint` in JSON "
|
||||
"(`null` for every other candidate). The hint changes nothing about the order or the "
|
||||
"default.",
|
||||
"Reads directory listings and `lstat` only, never a file's content; an empty "
|
||||
"`incoming/` is exit 0 with nothing to do.",
|
||||
"A lone `.md` or `.txt` file longer than the length threshold of `raw derive` and `raw "
|
||||
"fetch` is listed with its length in characters, in text output and as `length` in "
|
||||
"JSON (`null` for every other candidate) - that text reaches a session with no "
|
||||
"derivation in front of it to print the length `CHECK`. Like the hint, it changes "
|
||||
"nothing about the order or the default.",
|
||||
"Reads directory listings and `lstat`, and a file's content only to measure a lone "
|
||||
"`.md`/`.txt` whose size in bytes is over the threshold - a UTF-8 text has no more "
|
||||
"characters than bytes; an empty `incoming/` is exit 0 with nothing to do.",
|
||||
),
|
||||
failures=(),
|
||||
examples=(
|
||||
@@ -180,6 +190,7 @@ def raw_pending_command(
|
||||
"reason": c.reason,
|
||||
"default": c is default,
|
||||
"hint": c.hint,
|
||||
"length": c.length,
|
||||
}
|
||||
for c in candidates
|
||||
], indent=2, ensure_ascii=False))
|
||||
@@ -199,6 +210,11 @@ def raw_pending_command(
|
||||
typer.echo(f" {line}")
|
||||
if c.hint is not None:
|
||||
typer.echo(f" text: {c.hint} (derives readable text, keeps the original)")
|
||||
if c.length is not None:
|
||||
typer.echo(
|
||||
f" long: {c.length:,} characters, over the {LONG_TEXT_CHARS:,} one ingest "
|
||||
"reads in one pass - read it in parts (instructions/ingest-large-tree.md)"
|
||||
)
|
||||
if default is None:
|
||||
typer.echo("No candidate can be accepted as it stands - each needs a human (reasons above).")
|
||||
return
|
||||
|
||||
@@ -20,6 +20,7 @@ import typer
|
||||
import yaml
|
||||
|
||||
from chemenu import filters, web_capture
|
||||
from chemenu.commands.raw._common import LONG_TEXT_CHARS
|
||||
from chemenu.commands.raw.accept import raw_accept_command
|
||||
from chemenu.commands.raw.derive import raw_derive_command
|
||||
from chemenu.commands.raw.pending import raw_pending_command
|
||||
@@ -883,6 +884,71 @@ def test_header_renders_a_list_as_a_flow_sequence_and_a_string_as_before():
|
||||
assert yaml.safe_load(rendered.strip("-\n").replace("---", "")) == {"a": "plain", "b": ["x y", "z"], "c": []}
|
||||
|
||||
|
||||
# --- the length axis (Gitea #226) -------------------------------------------
|
||||
|
||||
|
||||
def _long_checks(out: str) -> list[str]:
|
||||
return [line for line in out.splitlines() if line.startswith("CHECK Longer than one ingest")]
|
||||
|
||||
|
||||
def test_the_length_check_fires_one_character_past_the_constant_and_not_at_it(tree, capsys):
|
||||
# one paragraph, the same file-name length: every header is as long as the probe's
|
||||
_put(tree, "a.docx", make_docx(_p("x")))
|
||||
_derive("incoming/a.docx")
|
||||
overhead = len((tree / "incoming/a/derived.md").read_text(encoding="utf-8")) - 1
|
||||
capsys.readouterr()
|
||||
|
||||
_put(tree, "b.docx", make_docx(_p("x" * (LONG_TEXT_CHARS - overhead))))
|
||||
_derive("incoming/b.docx")
|
||||
assert len((tree / "incoming/b/derived.md").read_text(encoding="utf-8")) == LONG_TEXT_CHARS
|
||||
assert _long_checks(capsys.readouterr().out) == []
|
||||
|
||||
_put(tree, "c.docx", make_docx(_p("x" * (LONG_TEXT_CHARS - overhead + 1))))
|
||||
_derive("incoming/c.docx")
|
||||
[line] = _long_checks(capsys.readouterr().out)
|
||||
assert f"incoming/c/derived.md ({LONG_TEXT_CHARS + 1:,} characters)" in line
|
||||
assert "instructions/ingest-large-tree.md" in line
|
||||
|
||||
|
||||
def test_the_length_is_counted_in_characters_not_in_bytes(tree, capsys):
|
||||
_put(tree, "umlaute.docx", make_docx(_p("ä" * (LONG_TEXT_CHARS // 2 + 1000))))
|
||||
_derive("incoming/umlaute.docx")
|
||||
assert (tree / "incoming/umlaute/derived.md").stat().st_size > LONG_TEXT_CHARS
|
||||
assert _long_checks(capsys.readouterr().out) == []
|
||||
|
||||
|
||||
def test_the_length_check_names_each_long_md_of_a_folder_and_of_a_zip(tree, capsys):
|
||||
folder = tree / "incoming/mappe"
|
||||
folder.mkdir()
|
||||
(folder / "lang.docx").write_bytes(make_docx(_p("x" * (LONG_TEXT_CHARS + 1))))
|
||||
(folder / "kurz.docx").write_bytes(make_docx(_p("x")))
|
||||
_derive("incoming/mappe")
|
||||
[line] = _long_checks(capsys.readouterr().out)
|
||||
assert "incoming/mappe/lang.md (" in line and "kurz.md" not in line
|
||||
|
||||
_put(tree, "paket.zip", _zip({"lang.docx": make_docx(_p("x" * (LONG_TEXT_CHARS + 1)))}))
|
||||
_derive("incoming/paket.zip")
|
||||
[line] = _long_checks(capsys.readouterr().out)
|
||||
assert "incoming/paket/lang.md (" in line
|
||||
|
||||
|
||||
def test_pending_names_the_length_of_a_long_lone_text_file_and_of_nothing_else(tree, capsys):
|
||||
_put(tree, "lang.md", b"x" * (LONG_TEXT_CHARS + 1))
|
||||
_put(tree, "grenze.txt", b"x" * LONG_TEXT_CHARS)
|
||||
_put(tree, "umlaute.md", ("ä" * (LONG_TEXT_CHARS // 2 + 1000)).encode("utf-8"))
|
||||
_put(tree, "page.html", b"x" * (LONG_TEXT_CHARS + 1))
|
||||
_put(tree, "page.md", b"x" * (LONG_TEXT_CHARS + 1))
|
||||
raw_pending_command(json_out=True)
|
||||
by_path = {c["paths"][0]: c for c in json.loads(capsys.readouterr().out)}
|
||||
assert by_path["incoming/lang.md"]["length"] == LONG_TEXT_CHARS + 1
|
||||
assert by_path["incoming/grenze.txt"]["length"] is None
|
||||
assert by_path["incoming/umlaute.md"]["length"] is None
|
||||
assert by_path["incoming/page.html"]["length"] is None # a fetch pair: raw fetch printed it
|
||||
|
||||
raw_pending_command(json_out=False)
|
||||
assert f"long: {LONG_TEXT_CHARS + 1:,} characters" in capsys.readouterr().out
|
||||
|
||||
|
||||
# --- the nets in raw pending and raw accept ---------------------------------
|
||||
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import typer
|
||||
import yaml
|
||||
|
||||
from chemenu import web_capture
|
||||
from chemenu.commands.raw._common import LONG_TEXT_CHARS
|
||||
from chemenu.commands.raw.accept import raw_accept_command
|
||||
from chemenu.commands.raw.fetch import raw_fetch_command
|
||||
|
||||
@@ -330,6 +331,27 @@ def test_plain_text_is_stored_as_txt_unchanged(tree, site):
|
||||
assert (tree / "incoming/notes.txt").read_bytes() == b"line one\r\nline two\n"
|
||||
|
||||
|
||||
def _long_checks(out: str) -> list[str]:
|
||||
return [line for line in out.splitlines() if line.startswith("CHECK Longer than one ingest")]
|
||||
|
||||
|
||||
def test_a_derived_page_over_the_length_threshold_gets_the_length_check(tree, site, capsys):
|
||||
_fetch(site.html("/short", ARTICLE))
|
||||
assert _long_checks(capsys.readouterr().out) == []
|
||||
|
||||
long_page = b"<html><body><main><p>" + b"x" * (LONG_TEXT_CHARS + 1) + b"</p></main></body></html>"
|
||||
_fetch(site.html("/long", long_page))
|
||||
[line] = _long_checks(capsys.readouterr().out)
|
||||
assert "incoming/long.md (" in line and "instructions/ingest-large-tree.md" in line
|
||||
|
||||
|
||||
def test_plain_text_over_the_length_threshold_gets_the_length_check(tree, site, capsys):
|
||||
site.routes["/notes"] = (200, {"Content-Type": "text/plain; charset=utf-8"}, b"x" * (LONG_TEXT_CHARS + 1))
|
||||
_fetch(site.url("/notes"))
|
||||
[line] = _long_checks(capsys.readouterr().out)
|
||||
assert f"incoming/notes.txt ({LONG_TEXT_CHARS + 1:,} characters)" in line
|
||||
|
||||
|
||||
# --- --html: a page saved from a browser ------------------------------------
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user