curated_feed.validate ===================== .. py:module:: curated_feed.validate .. autoapi-nested-parse:: Preflight checks on a runner's configuration and the files it names. The settings model refuses what is provably wrong — an unknown key, a floor that does not match the consequence scale — and refusing is the right answer there, because a config that cannot load is a config nobody can run against. This module is for what is *probably* wrong: an editorial policy that was copied but never written, a subscription list that lists nothing, two runners that name one state directory. None of those stops a run. Each of them ruins one, silently, at six in the morning. The line the two sit either side of is who pays for a false positive. A model validator that guesses wrong makes the config unloadable and the only escape is patching the engine; a check here that guesses wrong is a line to argue with. Nothing here touches the network, resolves a feed or needs a credential, which is what lets the same command serve as a pull-request gate, an unattended preflight inside a container that holds no secrets, and something run by hand after copying a runner's directory. Attributes ---------- .. autoapisummary:: curated_feed.validate.WORKERS_DEV curated_feed.validate.RESOURCE_READERS curated_feed.validate.UNSET_RESOURCES curated_feed.validate.RUNNER_CHECKS Functions --------- .. autoapisummary:: curated_feed.validate.check_the_editorial_policy_is_written curated_feed.validate.check_the_subscription_list_holds_feeds curated_feed.validate.check_the_feed_has_an_id curated_feed.validate.check_the_site_url_is_absolute curated_feed.validate.check_the_worker_matches_the_host curated_feed.validate.validate_config curated_feed.validate.validate_runner curated_feed.validate.find_runners curated_feed.validate.check_runners_share_nothing curated_feed.validate.validate_directory curated_feed.validate.validate_path curated_feed.validate.build_parser curated_feed.validate.main Module Contents --------------- .. py:data:: WORKERS_DEV :value: '.workers.dev' .. py:data:: RESOURCE_READERS :type: dict[str, collections.abc.Callable[[curated_feed.config.Config], object]] .. py:data:: UNSET_RESOURCES .. py:function:: check_the_editorial_policy_is_written(paths: curated_feed.config.Paths) -> str | None The runner's half of the prompt, which the engine ships the other half of. Present but empty is the state a copied template is left in, and it reaches the model as a heading and nothing else: every article scores on consequence alone, and the day looks merely disappointing rather than broken. .. py:function:: check_the_subscription_list_holds_feeds(paths: curated_feed.config.Paths) -> str | None The runner's subscriptions, parsed rather than resolved. Reading the file is filesystem work; asking each feed whether it answers is the network call this command may not make. A list that is present but lists nothing fails at fetch instead, unattended, with a run summary reporting zero items and no reason for it. .. py:function:: check_the_feed_has_an_id(publish: curated_feed.config.PublishSettings, deploy: curated_feed.config.DeploySettings) -> str | None A feed that is uploaded needs an address to call its own. `feed_url` falls back to a fixed urn when `site_url` is empty, so the published document's id and self-link are a placeholder shared with every other runner in that state — and a reader keys on the id. >>> print(check_the_feed_has_an_id( ... PublishSettings(site_url=""), DeploySettings(provider="wrangler"))) [deploy] provider is 'wrangler' but [publish] site_url is empty, so the feed's id is the placeholder 'urn:curated-feed' >>> check_the_feed_has_an_id(PublishSettings(site_url=""), DeploySettings()) .. py:function:: check_the_site_url_is_absolute(publish: curated_feed.config.PublishSettings) -> str | None The feed's own address, which is published verbatim. It becomes the id and the self-link of every document rendered, so a value a reader cannot resolve is not caught by anything downstream. >>> print(check_the_site_url_is_absolute(PublishSettings(site_url="a.example"))) [publish] site_url 'a.example' is not an absolute URL; it needs a scheme and a host >>> check_the_site_url_is_absolute(PublishSettings(site_url="https://a.example")) .. py:function:: check_the_worker_matches_the_host(publish: curated_feed.config.PublishSettings, deploy: curated_feed.config.DeploySettings) -> str | None Where the bytes go, against what the bytes claim about themselves. `deploy.project` becomes `wrangler deploy --name ` and decides the first; `publish.site_url` feeds `feed_url` and decides the second. Nothing ties them together but a comment in each config, and the edit where they drift is rotating the random name after a leak — which is exactly when a feed served at one address and advertising another matters most. Only inferable on a `workers.dev` host: the convention is Cloudflare's rather than a rule, and a custom domain breaks it entirely. Being an inference is also why it lives here and not in a model validator — a wrong guess there makes the config unloadable and stops the 06:00 run outright. >>> print(check_the_worker_matches_the_host( ... PublishSettings(site_url="https://frog-a8b1.someone.workers.dev"), ... DeploySettings(provider="wrangler", project="frog-a8b2"))) [publish] site_url is served by Worker 'frog-a8b1', but [deploy] project is 'frog-a8b2' >>> check_the_worker_matches_the_host( ... PublishSettings(site_url="https://feed.example.com"), ... DeploySettings(provider="wrangler", project="frog-a8b2")) .. py:data:: RUNNER_CHECKS :type: tuple[collections.abc.Callable[[curated_feed.config.Config], str | None], ...] .. py:function:: validate_config(config: curated_feed.config.Config) -> list[str] Every problem in one runner's settings and the files they name. What the runner writes to — its corpus, state and build directories — is deliberately not required to exist. A fresh checkout has none of them, and neither does the container this runs in as a preflight. .. py:function:: validate_runner(config_path: pathlib.Path) -> list[str] Every problem in the runner whose config is at `config_path`. The directory names the runner, as it does everywhere else (`ARCHITECTURE.md` D8), so that is what each line is prefixed with. .. py:function:: find_runners(root: pathlib.Path) -> list[pathlib.Path] The runner directories under `root`, in name order. A runner is a subdirectory holding a config, which is the same rule the workflow's matrix is held to. Anything else under `root` is reported rather than curated, so that a directory copied but not yet given a config says so. .. py:function:: check_runners_share_nothing(config_by_runner: dict[str, curated_feed.config.Config]) -> list[str] No two runners may name one resource. Nothing derives these locations from the runner's name, so a config that was copied and half-edited would publish two feeds into one log and upload each over the other. Both feeds still render, which is why this is checked rather than left to be noticed. .. py:function:: validate_directory(root: pathlib.Path) -> list[str] Every problem in every runner under `root`, and between them. .. py:function:: validate_path(path: pathlib.Path) -> list[str] Every problem under one config file, or under one directory of runners. .. py:function:: build_parser() -> argparse.ArgumentParser .. py:function:: main(argv: collections.abc.Sequence[str] | None = None) -> int