curated_feed.deploy =================== .. py:module:: curated_feed.deploy .. autoapi-nested-parse:: Putting the built feed in front of the internet. Rendering stops at the filesystem, because the build host and the publish host are separate (see `docs/ARCHITECTURE.md`, D6). This stage is the only one that knows where the feed is served from, and the only one whose failure is invisible: a reader that stops receiving items looks exactly like a quiet week. Three things follow from that. - **Uploading is gated on the content hash.** Rendering records the hash of what it wrote; this records the hash of what it sent. A quiet day therefore costs nothing, and the host's deployment history stays a list of real changes rather than a daily heartbeat. - **Nothing is written unless an upload actually happened.** A deployer that reports what it would do must not be recorded as having done it, or the next run would skip an upload that never took place. - **No credential is read from configuration.** The deploy tool resolves its own from the environment, exactly as the scorer does. Deployment belongs to CI. The devcontainer can rehearse it, and does not have to be trusted with a token to be useful. Attributes ---------- .. autoapisummary:: curated_feed.deploy.DEPLOY_META curated_feed.deploy.WRANGLER_PACKAGE_ENV curated_feed.deploy.DEFAULT_WRANGLER_PACKAGE curated_feed.deploy.ERROR_LINES curated_feed.deploy.CompletedCommand curated_feed.deploy.Runner Exceptions ---------- .. autoapisummary:: curated_feed.deploy.DeployError Classes ------- .. autoapisummary:: curated_feed.deploy.Deployer curated_feed.deploy.NullDeployer curated_feed.deploy.WranglerDeployer Functions --------- .. autoapisummary:: curated_feed.deploy.wrangler_package curated_feed.deploy.run_command curated_feed.deploy.build_command curated_feed.deploy.make_deployer curated_feed.deploy.rendered_hash curated_feed.deploy.deployed_hash curated_feed.deploy.build_parser curated_feed.deploy.main Module Contents --------------- .. py:data:: DEPLOY_META :value: 'deploy.json' .. py:data:: WRANGLER_PACKAGE_ENV :value: 'CURATED_FEED_WRANGLER' .. py:data:: DEFAULT_WRANGLER_PACKAGE :value: 'wrangler@4' .. py:data:: ERROR_LINES :value: 10 .. py:function:: wrangler_package(env: collections.abc.Mapping[str, str] | None = None) -> str The wrangler `npx` is asked for. An empty variable means the default rather than an empty argument, as an empty `[score] effort` means "leave it out" rather than "send nothing". A bare package name is npx's "run what is already installed" form, which is what the image sets it to: nothing is resolved and nothing is downloaded. >>> wrangler_package({}) 'wrangler@4' >>> wrangler_package({"CURATED_FEED_WRANGLER": "wrangler"}) 'wrangler' .. py:exception:: DeployError Bases: :py:obj:`Exception` Raised when the built feed could not be uploaded. .. py:class:: Deployer Bases: :py:obj:`Protocol` Anything that can publish a directory of files. `is_live` says whether `deploy` really publishes. It is part of the protocol rather than a detail of one backend, because the caller decides on it whether to record the upload. .. py:attribute:: name :type: str .. py:attribute:: is_live :type: bool .. py:method:: deploy(directory: pathlib.Path) -> str .. py:class:: NullDeployer Explains that no host is configured, and uploads nothing. The default, and deliberately so: a replay of a stub day is the commonest thing to run locally, and it must not be able to reach the internet. .. py:attribute:: name :value: 'none' .. py:attribute:: is_live :value: False .. py:method:: deploy(directory: pathlib.Path) -> str .. py:data:: CompletedCommand .. py:data:: Runner .. py:function:: run_command(command: collections.abc.Sequence[str], *, timeout: float) -> CompletedCommand Run a command to completion, capturing what it printed. .. py:function:: build_command(directory: pathlib.Path, *, settings: curated_feed.config.DeploySettings, package: str | None = None) -> list[str] The wrangler invocation for one upload. Every value wrangler needs is passed as a flag, so that no `wrangler.jsonc` has to be committed. That file would hold the Worker's name, which is the hostname the feed is served from and therefore the secret. Without a config file wrangler asks for a compatibility date, and asks interactively when it is missing — which in CI is a hang rather than a question. It has no effect on a Worker that is only static files. >>> settings = DeploySettings(project="p", compatibility_date="2026-08-14") >>> build_command(Path("/build"), settings=settings)[3:] ['deploy', '--assets', '/build', '--name', 'p', '--compatibility-date', '2026-08-14'] .. py:class:: WranglerDeployer(settings: curated_feed.config.DeploySettings, *, dry_run: bool = False, runner: Runner = run_command) Uploads a directory to a Cloudflare Worker with `wrangler deploy`. Wrangler resolves `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` from the environment, so neither appears in configuration. A Worker serving static assets, rather than Cloudflare Pages: the two put the same files on the same edge network, but a Pages deployment additionally mints a permanent `..pages.dev` address every time it runs. A daily job would therefore accumulate public addresses for a feed whose privacy is its URL. `wrangler deploy` updates one address in place. .. py:attribute:: name :value: 'wrangler' .. py:attribute:: settings .. py:attribute:: is_live :value: True .. py:method:: deploy(directory: pathlib.Path) -> str .. py:function:: make_deployer(config: curated_feed.config.Config, *, dry_run: bool = False) -> Deployer The deployer named in configuration. .. py:function:: rendered_hash(path: pathlib.Path) -> str The content hash of the build on disk, as the last render recorded it. .. py:function:: deployed_hash(path: pathlib.Path) -> str | None The content hash of the last successful upload, or `None` if there is none. .. py:function:: build_parser() -> argparse.ArgumentParser .. py:function:: main(argv: collections.abc.Sequence[str] | None = None) -> int