cli.meta.roles.applications.complexity package¶
Submodules¶
cli.meta.roles.applications.complexity.cli module¶
Argument parsing, sorting/filtering and the main entry point.
- cli.meta.roles.applications.complexity.cli.build_parser() ArgumentParser¶
- cli.meta.roles.applications.complexity.cli.main(argv: list[str] | None = None) int¶
- cli.meta.roles.applications.complexity.cli.parse_lifecycles(tokens: list[str] | None) set[str] | None¶
Normalise
--lifecyclestokens into a lowercase set, splitting each on commas and whitespace so'alpha,beta'and'alpha beta'are equivalent.None(flag absent) staysNoneso the model falls back to its built-in default envelope.
- cli.meta.roles.applications.complexity.cli.parse_sort_spec(spec: str) list[tuple[str, bool]]¶
Parse a
--sortvalue into an ordered[(column, reverse), ...].- Parameters:
spec – Comma-separated clauses, each a column optionally prefixed or suffixed with a direction, e.g.
"desc embeds, asc total". Direction defaults toasc.- Returns:
Clauses in significance order (first = primary sort key).
reverseis True fordesc.- Raises:
ValueError – An unknown token, or a clause naming no column.
cli.meta.roles.applications.complexity.filter module¶
A tiny boolean filter language over ComplexityRow fields.
Grammar (precedence low → high):
or_expr := xor_expr ('or' xor_expr)*
xor_expr := and_expr ('xor' and_expr)*
and_expr := not_expr ('and' not_expr)*
not_expr := 'not' not_expr | atom
atom := '(' or_expr ')' | comparison
comparison:= FIELD OP value
OP := '%%' | '==' | '!=' | '<=' | '>=' | '<' | '>'
value := scalar | '{' scalar (',' scalar)* '}'
scalar := NUMBER | WORD | QUOTED
%% is “contains” for a scalar string value and “is a member of” for a set
value; == / != against a set mean membership / non-membership. String
comparisons (== != %%) are case-insensitive; < > <= >= are
numeric. A bare expression with no operator (e.g. nextcloud) is shorthand
for name %% nextcloud.
- exception cli.meta.roles.applications.complexity.filter.FilterError¶
Bases:
ValueErrorA malformed filter expression.
cli.meta.roles.applications.complexity.graph module¶
Role discovery and the shared-service dependency graph it induces.
- cli.meta.roles.applications.complexity.graph.build_graphs(roles_dir: Path, *, truth: TruthFn) tuple[dict[str, list[str]], dict[str, list[str]]]¶
Return
(forward, reverse)adjacency maps over all role dirs.forward[A]is the list of provider roles thatAdirectly depends on (services it embeds).reverse[B]is the list of roles that directly depend onB(consumers that embedB).
- cli.meta.roles.applications.complexity.graph.build_service_registry(roles_dir: Path) dict[str, dict[str, Any]]¶
- cli.meta.roles.applications.complexity.graph.direct_dep_roles(services_map: dict[str, Any], registry: dict[str, dict[str, Any]], *, truth: Callable[[Any], bool]) list[str]¶
Provider roles a services map directly embeds (enabled AND shared truthy under
truth), deduped in first-seen order.
- cli.meta.roles.applications.complexity.graph.resolve_transitively(start_role: str, forward_graph: dict[str, list[str]], *, max_level: int | None = None) list[str]¶
BFS over an adjacency map.
max_levelcaps recursion depth:1returns direct neighbours only,2adds their direct neighbours, etc.Nonewalks the full closure. The start role itself is never included in the result.
cli.meta.roles.applications.complexity.model module¶
The per-role complexity score, its dna cluster key and siblings.
- class cli.meta.roles.applications.complexity.model.ComplexityRow(name: str, embeds: int, services: list[str], consumers: int, consumed_by: list[str], embeds_direct: int, services_direct: list[str], consumers_direct: int, consumed_by_direct: list[str], weight: int, dna: str, siblings: list[str], random: int = 0, variant: int | None = None, id: int = 0, covered_by: int = 0, row: int = -1, variants: int = 1, lifecycle: str = '', compose: bool = False, swarm: bool = False, host: bool = False, stack: bool = False, test_compose: bool = False, test_swarm: bool = False, test_host: bool = False, integrated: bool = True, in_main: bool = True, clone: bool = False)¶
Bases:
NamedTupleOne application role. The field order is the column order; legacy positional access (
row[0]…row[11]) still resolves.The transitive fields (
services/consumed_byand their counts) are the BFS closure capped atmax_level; the*_directfields are always the one-hop neighbours.weightis the sum of the four numeric columns.dnais a hash of the role’s own name unioned with its embedded services (sorted), so two roles covering the same service set share a dna.siblingsare the other roles sharing that dna;cloneis True for every row of a dna group except the heaviest one (ties broken by name), so sorting clones last keeps one representative per service set ahead of the budget cut.randomis a per-row 6-digit nonce, rolled fresh per invocation, so tie order deliberately varies between runs.variantis themeta/variants.ymlindex this row was computed against, orNonefor a whole-role row.idandcovered_byare set by the CLI after sorting:idis the row’s numeric position in sort order (1-based), andcovered_byis theidof the first earlier (green) row of a DIFFERENT role that embeds this row, or the sentinel0when this row is itself green (no realidis 0). Two variants of the same role never cover each other.rowis the 1-based line number in the final rendered output (assigned last, after filtering), so it stays sequential even when sorting bycovered_byscramblesid.variantsis the number ofmeta/variants.ymlvariants of the role in whole-role mode; under--varianteach row already is a single variant, so it is1.lifecycleis the role’smeta/services.ymllifecycle stage (alpha/beta/pre/…).compose/swarmare True when the CI test-deploy matrix exercises the role in that mode, honouring the discovery skip logic: invokable + lifecycle in the tested envelope + the role’sskiplist. Under--variantswarmis per-variant (every variant deploys, so it tracks the role-levelswarmandstack).stackis True when the role renders its own container stack (ships atemplates/*compose*.yml.j2); host-only roles (backup, wireguard, swapfile) and pure service-injectors are False.swarmimpliesstack: a role with no compose template of its own is never a swarm stack-deploy target, soswarmis forced False whenstackis False.hostmirrors the primary entity’smodes.host.enabledflag (default True) for non-stack roles, and is forced False for a stack role: it marks invokable roles that configure the host instead of shipping a stack.test_compose/test_swarm/test_hostduplicate the mode columns minus the role’smeta/tests.ymlskiplist:modesstates where a role RUNS,skipdeactivates TESTING a mode, and CI discovery filters on thesetest_*columns.integratedis False when the row’s direct service map keeps no foreign provider enabled (the role deploys isolated): per variant on variant rows, from the basemeta/services.ymlon whole-role rows.- clone: bool¶
Alias for field number 28
- compose: bool¶
Alias for field number 19
- consumed_by: list[str]¶
Alias for field number 4
- consumed_by_direct: list[str]¶
Alias for field number 8
- consumers: int¶
Alias for field number 3
- consumers_direct: int¶
Alias for field number 7
- covered_by: int¶
Alias for field number 15
- dna: str¶
Alias for field number 10
- embeds: int¶
Alias for field number 1
- embeds_direct: int¶
Alias for field number 5
- host: bool¶
Alias for field number 21
- id: int¶
Alias for field number 14
- in_main: bool¶
Alias for field number 27
- integrated: bool¶
Alias for field number 26
- lifecycle: str¶
Alias for field number 18
- name: str¶
Alias for field number 0
- random: int¶
Alias for field number 12
- row: int¶
Alias for field number 16
- services: list[str]¶
Alias for field number 2
- services_direct: list[str]¶
Alias for field number 6
- siblings: list[str]¶
Alias for field number 11
- stack: bool¶
Alias for field number 22
- swarm: bool¶
Alias for field number 20
- test_compose: bool¶
Alias for field number 23
- test_host: bool¶
Alias for field number 25
- test_swarm: bool¶
Alias for field number 24
- variant: int | None¶
Alias for field number 13
- variants: int¶
Alias for field number 17
- weight: int¶
Alias for field number 9
- cli.meta.roles.applications.complexity.model.TESTED_LIFECYCLES = frozenset({'alpha', 'beta', 'maintenance', 'rc', 'stable'})¶
Lifecycle stages the CI test-deploy discovery exercises, sourced from the
INFINITO_LIFECYCLESvalue indefault.env(the single source).
- cli.meta.roles.applications.complexity.model.attach_siblings(rows: list[ComplexityRow]) list[ComplexityRow]¶
Group rows by dna and elect the heaviest of each group the original, marking the rest
cloneso the budget cut keeps one representative.Election is relative to the rows handed in, so a caller that filters MUST call this again on the survivors: a stand-in the filter dropped is not deployed, and the row deferring to it is cut in favour of nothing.
- cli.meta.roles.applications.complexity.model.compute_complexity_rows(roles_dir: Path, *, include_group_names: bool = True, max_level: int | None = None, lifecycles: set[str] | None = None) list[ComplexityRow]¶
- cli.meta.roles.applications.complexity.model.compute_variant_complexity_rows(roles_dir: Path, *, include_group_names: bool = True, max_level: int | None = None, lifecycles: set[str] | None = None) list[ComplexityRow]¶
One row per
meta/variants.ymlvariant of every application role.Each row recomputes the role’s embedded (forward) service deps from that variant’s merged
servicesmap, so toggling a sidecar off in a variant drops it from the variant’sembeds/weight. The consumer (reverse) side is the variant-independent role-level graph: who else in the catalog embeds this role does not change with the role’s own variant selection.
cli.meta.roles.applications.complexity.render module¶
Render complexity rows as a counts table, JSON, or bare role names.
- cli.meta.roles.applications.complexity.render.render_json(rows: list[ComplexityRow]) str¶
- cli.meta.roles.applications.complexity.render.render_string(rows: list[ComplexityRow]) str¶
- cli.meta.roles.applications.complexity.render.render_table(rows: list[ComplexityRow], *, symbol: bool = False) str¶
Counts only (plus the short
dnaand the sibling count). Use--format jsonfor the full role and sibling name lists. Withsymbol=Truethe headers are emoji-only (compact), the boolean/lifecycle cells render as emojis, and a legend is appended.
- cli.meta.roles.applications.complexity.render.render_yaml(rows: list[ComplexityRow]) str¶