repomatic.cloudflare.api module¶

Reach the Cloudflare v4 API: the credential, the account and the transport.

repomatic.cloudflare.pages and repomatic.cloudflare.r2 share this module. It also holds the errors they raise and the status labels of their reports.

The credential resolves in this order, so the same command works in CI and on a laptop without a token ever landing on a command line:

  1. CLOUDFLARE_API_TOKEN from the environment (what CI uses).

  2. The OAuth token wrangler login stores locally.

The account is never declared: the credential settles it. GET /accounts answers what the token sees, and one scoped to nothing but Cloudflare Pages: Edit still enumerates the account it belongs to, so CI carries the token alone. When a credential sees several accounts, account() asks which one owns the project, and fails rather than guesses when that question has no single answer. No identifier is ever hardcoded either: repositories using this are public, and account IDs do not belong in them.

Caution

Never gate anything on GET /user/tokens/verify: that endpoint is user-scoped, so an account-owned token (the recommended kind, cfat_ prefix) answers 401 there while every project call succeeds. Proving the credential against the project it is meant to touch is the only verification that means anything, which is what every mode of both commands does implicitly.

repomatic.cloudflare.api.API_ROOT: Final = 'https://api.cloudflare.com/client/v4'¶

Cloudflare v4 API root every call is relative to.

repomatic.cloudflare.api.API_TIMEOUT: Final = 30¶

Socket timeout in seconds, wider than repomatic’s JSON default: a PATCH that stalls mid-write is worth waiting out rather than retrying blind.

repomatic.cloudflare.api.EXPIRY_WARNING_DAYS: Final = 30¶

How close a token’s expiry gets before --check starts warning.

Cloudflare notifies about neither an approaching expiry nor a passed one, so the monthly Docs run carrying this check is the only calendar the token has. A month of warnings is enough to rotate without ever reaching the red run.

repomatic.cloudflare.api.STATUS_WIDTH: Final = 6¶

Columns the label of a report line takes, so the messages line up.

repomatic.cloudflare.api.WRANGLER_CONFIG_FILE: Final = PosixPath('.wrangler/config/default.toml')¶

Where wrangler login stores its OAuth token, under a configuration directory. See wrangler_config_paths().

exception repomatic.cloudflare.api.CloudflareError[source]¶

Bases: RuntimeError

Raised when the Cloudflare API refuses or a credential cannot be found.

exception repomatic.cloudflare.api.CloudflareHTTPError(message, status)[source]¶

Bases: CloudflareError

An HTTP-level refusal from the Cloudflare API, carrying its status.

Lets a caller act on which refusal arrived (a 404 meaning “create it” is a different instruction than a 403 meaning “stop”) without parsing the message string it was raised with.

status¶

The HTTP status the API answered with.

class repomatic.cloudflare.api.Status(*values)[source]¶

Bases: StrEnum

The label that opens one line of a Cloudflare command’s report.

DRIFT = 'DRIFT'¶

The live state differs from the declared one.

NOTE = 'note'¶

A step the command could not do, left to do by hand.

OK = 'ok'¶

A step done, or a state that matches the declared one.

WARN = 'WARN'¶

Nothing fails yet, but something needs a hand soon.

repomatic.cloudflare.api.print_status(label, message)[source]¶

Print one line of a report, its message past the label column.

Parameters:
  • label (Status | None) – What the line reports. None continues the line above it.

  • message (str) – The text of the line.

Return type:

None

repomatic.cloudflare.api.refusal(label, status, detail)[source]¶

Build the error a failed request raises, on the REST or the S3 API.

Parameters:
  • label (str) – The request, like GET /accounts. It names the request without its URL, which embeds the account ID on the S3 API.

  • status (int | None) – The HTTP status of a refusal. None when no answer came.

  • detail (str) – The start of the answer’s body for a refusal, else the reason the transport gave.

Return type:

CloudflareError

Returns:

A CloudflareHTTPError for a refusal, else a CloudflareError.

repomatic.cloudflare.api.wrangler_config_paths()[source]¶

Where wrangler login may have stored its OAuth token, in probe order.

Resolved on each call, so the home directory and XDG_CONFIG_HOME are the ones the process sees now, not the ones it saw at import. wrangler keeps its configuration in the XDG configuration directory, which macOS puts under ~/Library/Preferences. That path comes first on macOS, and is not probed on any other platform.

Return type:

tuple[Path, ...]

Returns:

The candidate files, most likely first.

repomatic.cloudflare.api.api_token()[source]¶

Find the credential every call sends.

Return type:

str

Returns:

CLOUDFLARE_API_TOKEN, else the OAuth token of a local wrangler login.

Raises:

CloudflareError – When neither holds a credential, or when the wrangler login session expired.

repomatic.cloudflare.api.call(path, token, method='GET', body=None)[source]¶

Send one request to the REST API, and return the result it answers.

The request goes through repomatic.http.request(). A refusal, a timeout and a dropped connection all arrive as one error, which this turns into a CloudflareError the commands catch.

Parameters:
  • path (str) – The endpoint, relative to API_ROOT.

  • token (str) – The credential, from api_token().

  • method (str) – The HTTP method.

  • body (Mapping[str, Any] | None) – The JSON body of a write. None for a read.

Return type:

Any

Returns:

The result member of the answer.

Raises:
repomatic.cloudflare.api.account(token, project)[source]¶

Find the account to act on, from the credential alone.

The token settles the usual case on its own: it enumerates its own account through GET /accounts even when scoped to nothing but Cloudflare Pages: Edit. A credential seeing several accounts falls back to asking where project lives, probing each account’s copy of it: exactly one owner is an answer, anything else fails rather than guesses, since deploying into a guessed account is worse than not deploying.

Parameters:
  • token (str) – The credential, from api_token().

  • project (str) – The Pages project, which settles a credential that sees several accounts.

Return type:

str

Returns:

The account ID.

Raises:

CloudflareError – When no account, or more than one, answers.

repomatic.cloudflare.api.token_expiry_warning(token, account_id)[source]¶

Warn when the credential expires soon.

Best effort by design, and it must never fail a run: the account-scoped verify endpoint answers for account-owned tokens, the user-scoped one for user tokens, and each rejects the other kind. A credential that verifies nowhere still deploys fine, so an unreadable expiry is silence, not an error.

Parameters:
Return type:

str | None

Returns:

The warning, or None when the expiry is far, absent or unreadable.

repomatic.cloudflare.api.zone_for(domain, token)[source]¶

Find the zone hosting domain, or None if the credential sees none.

The zone is rarely the domain itself: docs.example.com lives in example.com. Labels are stripped from the left until a zone answers, stopping at two, which is as far as a registrable name goes for the ordinary suffixes. Reading zones needs Zone → Read, which a deploy token scoped to Pages alone does not carry, so this answers None rather than raising: the DNS half of attaching a domain is optional work that a narrower credential cannot do.

Parameters:
  • domain (str) – The hostname to place.

  • token (str) – The credential, from api_token().

Return type:

dict[str, Any] | None

Returns:

The zone object, or None when no zone answers.

repomatic.cloudflare.api.resolve_project(project=None)[source]¶

Name the Pages project a command acts on.

Parameters:

project (str | None) – The project the caller named, if any.

Return type:

str

Returns:

project, else the cloudflare_project the deploy job reads too. Empty when neither names a project.