API reference¶
Everything below is exported from django_spec_cli directly, and from the
subpackage it is listed under. Import from the package root in application
code; the subpackage paths are where each name is defined. Kernel names the
command reads - SpecRegistry, dispatch, the refusals - are documented on
the kernel's site.
Settings¶
One Django setting, read at the call rather than at import, so
override_settings reaches a command built before it.
SPEC_CLI_OUTPUT: the format aSpecCommandwrites its result in."auto"when unset, a table on a terminal and JSON Lines on anything else;"table"or"json"pins one project-wide, and a command's own--tableor--jsonstill wins. Anything else, a different case included, is refused asImproperlyConfiguredwhere it is read. Seewants_table.
System checks¶
Registered by DjangoSpecCliConfig, so a project gets them by listing
"django_spec_cli" in INSTALLED_APPS.
django_spec_cli.W001: a spec aSpecCommandserves authorizes nothing, every permission check it declares beingUnrestricted. Once per open spec, worded byunguarded_message.django_spec_cli.W003: a command could not be loaded to be checked.
The command¶
SpecCommand ¶
Bases: BaseCommand
A management command serving every spec in a registry, one subcommand each.
Declared by subclassing, in the project's own management/commands/
module, which names the registry::
class Command(SpecCommand):
registry = notes_registry
The registry is the subclass's to name, so one project can expose two
registries under two command names. It is read when the parser is built,
and a command naming none is refused then, as ImproperlyConfigured.
pool_seeds names the seeds the specs' callables are resolved with, as
pool_seeds on django-spec-html's SpecView does, for a registry
whose callables read one.
It is run as manage.py <command> <spec> --as-user USER [--json|--table]
[--option value ...], or manage.py <command> --explain:
- One subcommand per registry entry, named after the entry with each
_shown as-. Its options are the spec's parameters, asadd_spec_argumentsspells them, less any key the server fills (server_owned_keys). No option is matched by a prefix: an abbreviation is refused rather than read as whichever option it begins. --as-useris required. Its value goes throughresolve_as_user, and the user it returns is the principaldispatchruns the spec for, with the permission checks enforced: the command passes noGrant, and opens no transaction of its own.- The result goes through the spec's presenter and is written as a table
or as JSON Lines:
--tableand--jsonchoose, then theSPEC_CLI_OUTPUTsetting, then whether stdout is a terminal (wants_table). - A refusal ends the run with a
CommandErrorcarrying its message and anExitCode. --explainruns no spec: it lists the ones that authorize nothing, as the system check does.
The command is a mistake-prevention and attribution surface, never an
authorization boundary: whoever can run manage.py can run any code
the project can, and names any user they like with --as-user. What
it guarantees is that a run is attributed to a user and checked against
the permissions that user holds.
Sync only: it dispatches with dispatch, never adispatch.
create_parser ¶
Django's parser, with this command's options added and no option read by its prefix.
add_arguments ¶
--explain, then one subcommand per registry entry, each with its spec's options.
Every subcommand is built here, on every run: one spec's options are
a handful of add_argument calls, and building them eagerly is
what lets --help list every spec.
Raises:
| Type | Description |
|---|---|
ImproperlyConfigured
|
the class names no |
resolve_as_user ¶
The user --as-user VALUE names, or PrincipalUnavailable.
By primary key, through
resolve_principal,
which refuses a missing row, a malformed key and a deactivated
account. A project whose runbooks name people by username or email
overrides this method with that lookup.
The contract is: return an authenticated user, or raise
PrincipalUnavailable,
which ends the run with ExitCode.PRINCIPAL_UNAVAILABLE. Anything
else an override raises is a bug in the lookup, and propagates. An
override need not refuse a deactivated user itself: dispatch
refuses one whatever the lookup returns. A lookup answering None
or an anonymous user is refused the same way, by the command. There is
no fallback to an anonymous user and no user of the command's own.
exit_code_for ¶
The ExitCode error ends a run with.
Every DispatchError
and ServiceError
has a code, a subclass a later release adds included: it takes its
nearest listed base's, so a new refusal of the call is INVALID_ARGUMENTS
as the HTTP transport answers it 400, and a new refusal by the operation
REFUSED. Anything else is ERROR, which a SpecCommand never
catches: an exception nobody declared is a bug, and its traceback is how
it is found.
Rendering a result¶
render_result ¶
Write presented to stream, as a table when table and JSON Lines when not.
presented is what
present returned for a
spec, and output what the spec declares (spec.output()), which
gives a table its columns. Read in this order:
None, what a service that returned nothing presents: nothing is written, in either format. A header over no rows would read as an empty list.- a mapping, one row: a one-row table, or one line.
- a string or bytes, a scalar, though both are iterable: a string is
never read as rows of its characters. Bytes are refused by the JSON
encoder with
TypeError, as an HTTP response refuses them. - any other iterable, rows: a table, or one line each, streamed, so a
generator is read one row at a time and nothing here turns it into a
list. A member that is no mapping - a spec with no presenter listing
plain values - is a row of one
resultcolumn in a table, and its own JSON on its own line. - anything else, a scalar: a table of one
resultcolumn and one row, or one line holding its JSON (42,"done",true).
A table reads every row before writing its first line; see
render_table
for its columns, cells and markings, and
render_json_lines
for the encoding. Only stream is written to.
render_table ¶
Write rows to stream as a table with a header line.
The columns are the declared output's, never the rows': its fields
in declaration order, each headed by its label, else its name. So an
empty result still prints its header, a row with a null never shifts a
column, and a key a row carries that output does not declare - the
affordances that present adds - is no column. JSON Lines, which
writes each row whole, is where such a key is read.
A cell is:
- empty for a null, and for a key the row leaves out, as a presenter
may for a field not
always_present. The table cannot tell the two apart; JSON Lines can. - a choice's display rather than its value, read in the active language, member by member for an array of choices. A value no choice names is shown as itself. A field marked as a handle keeps its value, because the operator pastes it into the next command, which takes the value.
- compact JSON for a nested object or array, in one cell.
- otherwise the value in JSON's spelling with a string's quotes left off:
true,12.50for aDecimal, ISO 8601 for a date-time.
A line break, tab or other control character inside a value is written
escaped (\n, \x1b), so a row stays on one line and a value
cannot send the terminal an escape sequence.
Markings. A field marked hidden is no column. Its marking says it is plumbing, and the projection for an agent drops it for that reason; a person reading a table has no more use for it. That is a choice of readability and not a secret kept: no marking means a field never leaves the server, so an HTTP response and JSON Lines both carry a hidden field. Label, handle and ordinary content fields are columns. A marking's formatter is how an agent audience reads a value, and a command names no audience, so it is not applied.
output=None, a spec that declares no output, is the one case a row is
read for its columns: the first row's keys, in its order. With no rows
there are then no columns, and nothing is printed; an Output with no
fields prints nothing either.
Plain text: columns padded with spaces, separated by two, numbers
(integer and number fields) right-aligned and everything else
left-aligned, no colour and no box drawing, and no line ending in
whitespace. Widths are measured in terminal columns, per character: an
East Asian wide or full-width character counts two, most emoji among
them; a combining or enclosing mark and a format character (a zero-width
joiner, a variation selector) count none; anything else counts one. That
is an approximation of what a terminal draws, not a measurement of it:
an East Asian ambiguous character a terminal draws wide, or an emoji
sequence it draws as one glyph, leaves its row a column or two off.
Every row is read before the first line is written, because a column's width is its widest cell. A table therefore holds the whole result in memory; JSON Lines is the format that streams.
render_json_lines ¶
Write each row to stream as one line of JSON.
Streamed: each row is encoded and written before the next is read, so a generator over a large result is never held in memory whole. That is why the format is JSON Lines rather than one JSON array.
Each row is written as it was presented: every key, in the row's own
order, including a key no Output declares (the affordances
present adds) and a field marked hidden, which is how an HTTP response
carries it too. Text is written as itself (ensure_ascii=False), and
DjangoJSONEncoder encodes what JSON has no type for: a Decimal as
its string, a date-time, date or time in ISO 8601, a UUID, a lazy
translation in the active language. A value the encoder refuses raises
TypeError before its line is written.
One write per line, each ending in "\n": Django's
OutputWrapper appends a newline to any message that does not end in
one, so a line written in pieces would come out broken.
wants_table ¶
Whether to write a table to stream rather than JSON Lines.
Three answers, the first that decides winning:
requested: what the operator asked for with--tableor--json. The setting is not read at all when the flag decides, so a misconfigured setting never overrides an explicit request.- The project's
SPEC_CLI_OUTPUTsetting, when it pins"table"or"json". stream.isatty(): a table for a person at a terminal, JSON Lines for a pipe, a file or a test's buffer. A stream with noisattycannot say it is a terminal and gets JSON Lines.
That third answer means an interactive run and CI print different things
by default, which is why both the flag and the setting exist. Django's
OutputWrapper, which is what a command's self.stdout is, answers
isatty for the stream it wraps.
Parameters as options¶
describe_parameters ¶
One ParameterDescriptor per parameter, in declaration order.
Never refuses a declaration: a parameter with no flat spelling is
described with argv=False rather than left out, so a command can still
say in --help that a spec takes something argv cannot send.
ParameterDescriptor
dataclass
¶
A Parameter seen from argv.
What a command line can say about a parameter, built by
describe_parameters:
the option it is sent with, and what --help can honestly show about
the value. Each field is the declaration's, read for a person typing it,
and three are narrower than the Parameter they come from:
requiredis whether an omitted option is refused: a parameter declared required with a default is not, since the shape check lets the Validator supply the default. A spec declaredpartialrefuses no omission at all, which the declaration cannot see; the command that knows its spec says so.defaultis the declared default only where JSON can spell it:Noneincluded, and never a callable, a sentinel or NaN, whosereprin the help would be a concrete falsehood.UNSETmeans nothing is shown, not that nothing is declared.itemsis an array's element type, orNone: an element that is a row of nested Parameters is no type a command line could show.
argv is False for a parameter that has no honest flat spelling -
an object, an array of objects, of arrays or of files, and a file - so
no option is invented for it: a dotted option for a nested field would be
a syntax only this command spoke. Sent anyway, under its name by a
caller that fills the arguments itself, it still reaches coerce_flat's
refusal; left out, the shape check's required.
option
property
¶
The option string, --due-at for due_at, or None with no option.
add_spec_arguments ¶
add_spec_arguments(
parser: ArgumentParser, parameters: Parameters, *, reserved: Collection[str] = ()
) -> None
Add an option to parser for each parameter a command line can send.
The option is -- and the name with _ as -: due_at is
--due-at. Argparse only collects strings here, and every decision about
them is the kernel's, so a command line refuses what an HTTP caller would
be refused, in the same words:
- No value is converted. No option sets
type=; every value stays the string typed, andcoerce_flatreads it as it reads a query string. - No option is required by argparse. The shape check owns "missing",
so a required parameter is refused as HTTP refuses it, beside every other
mistake in the call, and
--helpmarks it(required). - No choice is enforced by argparse. The choices are listed in
--helpand the shape check refuses any other value. Argparse's ownchoices=would answer one mistake with a second message and exit code, and only for a parameter whose choices are all strings. - An omitted option is absent from the namespace, never
None: its default isargparse.SUPPRESS. That keeps it apart from an empty string and from--no-flag. - A
booleanis a--flag/--no-flagpair, and anarrayis a repeated option,--ids 1 --ids 2, each value one element. - Each value lands under a namespaced destination rather than the
parameter's name, so a parameter named
verbosityorsettingscannot overwriteBaseCommand's own. Read the namespace back withparsed_arguments.
A parameter with no honest flat spelling - an object, an array of objects,
a file - gets no option: see
ParameterDescriptor.
reserved is the option strings the command owns itself, such as
--as-user and Django's --verbosity; --help is always reserved,
since every parser owns its own.
Raises:
| Type | Description |
|---|---|
ImproperlyConfigured
|
naming the parameter and the option, when an
option would be one in |
parsed_arguments ¶
The spec's arguments from a namespace filled by add_spec_arguments's options.
The raw dict a command hands to
coerce_flat,
keyed by parameter name, so argv crosses the one coercion and the one
shape check every transport shares and is refused, or validated, exactly
as the same values off a JSON wire would be:
- An omitted option is absent, so the shape check decides what its absence means, as it does for a JSON caller who left the key out.
- A
booleanis the string"true"or"false": one of the four spellingscoerce_flatreads, rather than a Pythonboolonly this transport would hand on. - An
arrayis the list of strings given, one per repeated option. - Everything else is the string given, an empty one included.
Only the namespaced destinations are read, so a command's own options in
the same namespace - verbosity, settings - never become arguments.
A value set there by a caller other than argparse, as call_command
does with keyword arguments, is handed on as it was set, except that a
bool for a boolean is spelled as argparse's would have been.
Reporting open specs¶
unguarded_message ¶
The one-line report of spec, served by the command command_label.
command_label is the name an operator types after manage.py. The
system check and the command's own listing both print this, so the two
read the same.
The sentence says "any active user": dispatch refuses a deactivated principal whatever the spec's checks allow, so an open spec is open to every account that can still sign in, and to no other.
Types¶
ExitCode ¶
Bases: IntEnum
The exit status of a SpecCommand run, a public contract.
A script branches on these, so a member's number never changes and a number is never reused. Each says who refused and what the caller can do about it, read the way the HTTP transport reads the same refusals:
| Code | Member | The run | Raised for |
|---|---|---|---|
| 0 | SUCCESS |
ran, and its result was written | |
| 1 | ERROR |
failed in a way nobody declared | anything else, with a traceback |
| 2 | USAGE |
never started: the command line was malformed | argparse's own refusals |
| 3 | INVALID_ARGUMENTS |
was refused its arguments | InvalidArguments, ServiceValidationError, any other DispatchError |
| 4 | NOT_PERMITTED |
was refused to the principal | NotPermitted |
| 5 | PRINCIPAL_UNAVAILABLE |
had nobody to run as | PrincipalUnavailable |
| 6 | NOT_FOUND |
found no row to act on | a not_found result, ServiceNotFound |
| 7 | CONFLICT |
was refused by the row's current state | ServiceConflict, ActionUnavailable |
| 8 | INPUT_REQUIRED |
needs a value it was not given | AdditionalInputRequired |
| 9 | REFUSED |
was refused by the operation's own rule | any other ServiceError |
INPUT_REQUIRED has its own code because a command line cannot ask:
the operator re-runs the command with the option the message names, and
a script that knows it may answer does so. ActionUnavailable is a
conflict, as its base is; its code is in the message.
USAGE is the command line's: manage.py exits 2 on a malformed
line. Through call_command Django raises every parser error as a
CommandError with returncode 1 instead, and the command's own
usage errors take the same path, so a caller there reads a usage error
by its message.