---------------------------------------------------------------------- This is the API documentation for the excel_grapher library. ---------------------------------------------------------------------- ## Public Modules Namespaced public API surfaces GoogleSeriesDocstringRenderer() NumpySeriesDocstringRenderer() PlainSeriesDocstringRenderer() RstSeriesDocstringRenderer() CodeGenerator(graph: 'DependencyGraph | GraphLike', *, iterate_enabled: 'bool | None' = None, iterate_count: 'int' = 100, iterate_delta: 'float' = 0.001) -> 'None' Generates Python code from Excel formulas. IdentityTransitCompression() Collapse pure identity transit nodes into a projected export graph. FieldDoc(description: 'str') -> None FieldDoc(description: 'str') SeriesBindingDocstringContext(graph: 'DependencyGraph', workbook: 'Path | str', bindings: 'WorkbookSeriesBindings', series: 'dict[str, Any]', resolution: 'SeriesResolution', contract: 'SeriesBindingDocstringContract', function_kind: 'SeriesFunctionKind', function_name: 'str') -> None SeriesBindingDocstringContext(graph: 'DependencyGraph', workbook: 'Path | str', bindings: 'WorkbookSeriesBindings', series: 'dict[str, Any]', resolution: 'SeriesResolution', contract: 'SeriesBindingDocstringContract', function_kind: 'SeriesFunctionKind', function_name: 'str') SeriesBindingDocstringContract(series_id: 'str', function_name: 'str', function_kind: 'SeriesFunctionKind', data_range: 'str', layout: 'str', value_type: 'str | None', required_fields: 'tuple[str, ...]', fields: 'Mapping[str, FieldContract]', example_records: 'tuple[Mapping[str, object], ...]', notes: 'str') -> None SeriesBindingDocstringContract(series_id: 'str', function_name: 'str', function_kind: 'SeriesFunctionKind', data_range: 'str', layout: 'str', value_type: 'str | None', required_fields: 'tuple[str, ...]', fields: 'Mapping[str, FieldContract]', example_records: 'tuple[Mapping[str, object], ...]', notes: 'str') SeriesFunctionDoc(summary: 'str', purpose: 'str', record_matching: 'str', field_descriptions: 'Mapping[str, FieldDoc]' = ) -> None SeriesFunctionDoc(summary: 'str', purpose: 'str', record_matching: 'str', field_descriptions: 'Mapping[str, FieldDoc]' = ) BaseProjectionManifest(kind: 'str', forwarding_map: 'dict[str, str]', retained_to_collapsed_sources: 'dict[str, tuple[str, ...]]', removed_node_snapshots: 'dict[str, ProjectedNodeSnapshot]', formula_rewrites: 'tuple[FormulaRewrite, ...]', collapsed_groups: 'tuple[CollapsedGroup, ...]') -> None General, serializable projection manifest for node-collapsing projections. Directly usable by any projection that removes nodes and (optionally) maps value-equivalent removed addresses to retained computations; custom projections may also subclass to add kind-specific fields. Register `from_dict` under the manifest `kind` via `register_projection_manifest` to enable deserialization. CollapsedGroup(retained: 'str', collapsed_sources: 'tuple[str, ...]', statement_order: 'tuple[str, ...]', external_dependencies: 'tuple[str, ...]') -> None Source nodes that folded into one retained computation address. Node snapshots are stored once on the manifest's `removed_node_snapshots` and referenced here by address via `collapsed_sources` / `statement_order`. CompositeProjectionManifest(forwarding_map: 'dict[str, str]', steps: 'tuple[ProjectionManifest, ...]', kind: 'str' = 'composite') -> None Lineage for a sequence of composed projection steps. Codegen consumes `map_to_projected`, which chains each step's mapping in order and works for any step satisfying the `ProjectionManifest` protocol. `forwarding_map` is a precomputed, serializable view of that chained mapping for audit and round-trips; it is derived only from steps that expose a `forwarding_map` field, so it may be empty or partial for protocol-only steps even when `map_to_projected` still maps correctly. `steps` preserves each component manifest. Heterogeneous step kinds are supported. FormulaRewrite(dependent: 'str', before_formula: 'str | None', after_formula: 'str | None', before_normalized: 'str | None', after_normalized: 'str | None') -> None Dependent formula rewrite performed during projection. ProjectedNodeSnapshot(address: 'str', sheet: 'str', column: 'str', row: 'int', formula: 'str | None', normalized_formula: 'str | None', value: 'Any', is_target: 'bool', is_leaf: 'bool', metadata: 'dict[str, Any]') -> None Workbook node state captured before projection removes or rewrites it. ProjectionResult(original_graph: 'DependencyGraph', projected_graph: 'DependencyGraph', manifest: 'ProjectionManifest') -> None Projected graph facade with durable lineage back to the canonical graph. SeriesDocstringRenderCallable(*args, **kwargs) SeriesDocstringRenderer(*args, **kwargs) SeriesBindingDocstringCallback(*args, **kwargs) ProjectionManifest(*args, **kwargs) Stable contract a projection manifest exposes to export and composition. Implementations must surface a `kind` tag, an address-mapping helper that codegen consumes, plus a serializable `to_dict`. Kind-specific forwarding maps and lineage may be carried as additional fields. ProjectionStep(*args, **kwargs) Projection step that builds a `ProjectionResult` from a graph. WebVizLayoutPlugin(*args, **kwargs) resolve_series_docstring_renderer(renderer: 'SeriesDocstringRendererSpec') -> 'SeriesDocstringRenderer' list_series_docstring_callbacks() -> 'tuple[str, ...]' register_series_docstring_callback(name: 'str', callback: 'SeriesBindingDocstringCallback', *, replace: 'bool' = False) -> 'None' resolve_series_docstring_callback(callback: 'SeriesBindingDocstringCallbackSpec') -> 'SeriesBindingDocstringCallback' Look up a registered callback name or return a direct callback object. unregister_series_docstring_callback(name: 'str') -> 'None' Remove a registered callback (for tests and notebook cleanup). to_web_viz_payload(nx_graph: 'Any', *, max_local_nodes: 'int | None' = None, max_local_edges: 'int | None' = None, include_guarded_edges: 'bool' = True, include_guarded_edges_for_partition: 'bool' = False, layout: 'WebVizLayoutSpec' = 'stratified_multipartite', layout_config: 'dict[str, Any] | None' = None, include_formula_on_nodes: 'bool' = True, max_formula_length: 'int | None' = 120, seed: 'int' = 0, weight_attr: 'str | None' = None, include_module_overlay: 'bool' = True) -> 'WebVizPayload' Build a web-visualization payload from a NetworkX DiGraph. Layout is selected by `layout` (registered web layout plugin id or a direct `WebVizLayoutPlugin` callable). The default `stratified_multipartite` uses SCC-condensation longest-path rank on the vertical axis and Louvain community ordering on the horizontal axis when `include_module_overlay` is true. Other built-in ids include `spring`, `forceatlas2`, `multipartite` (NetworkX `multipartite_layout`), `graphviz_dot`, and `graphviz_sfdp`. Set `include_module_overlay=False` to skip the partition overlay (single module color; overview still draws local graph edges in the viewer). apply_projection(graph: 'DependencyGraph', projections: 'Iterable[ProjectionStep]') -> 'ProjectionResult' Apply one or more projection steps, returning the final projection result. Steps are applied in order, each projecting the previous step's projected graph. A single step's manifest is returned as-is; multiple steps are folded into a `CompositeProjectionManifest` carrying the chained forwarding map and every component manifest. Heterogeneous step kinds are supported. build_forwarding_projection_manifest(original_graph: 'DependencyGraph', record: 'IdentityTransitCompressionRecord', *, kind: 'str') -> 'BaseProjectionManifest' Build a `BaseProjectionManifest` from a graph compression `record`. All removed nodes are treated as value-equivalent forwarding aliases (the identity-transit contract). The collapse lineage is keyed back to `original_graph` for statement order and external-dependency boundaries. register_projection_manifest(kind: 'str', from_dict: 'ProjectionManifestFromDict', *, replace: 'bool' = False) -> 'None' Register a `from_dict` deserializer for projection manifests of `kind`. Args: kind: Manifest `kind` tag to register. from_dict: Callable building a manifest from its serialized mapping. replace: Allow overwriting an existing registration. Raises: ValueError: If `kind` is already registered and `replace` is False. resolve_projection_manifest(data: 'Mapping[str, Any]') -> 'ProjectionManifest' Deserialize a manifest by dispatching on its `kind` via the registry. Raises: ValueError: If the manifest `kind` has no registered deserializer. unregister_projection_manifest(kind: 'str') -> 'None' Remove a registered projection manifest `kind` (no-op if absent). list_web_viz_layouts() -> 'tuple[str, ...]' register_web_viz_layout(layout_id: 'str', plugin: 'WebVizLayoutPlugin', *, replace: 'bool' = False) -> 'None' resolve_web_viz_layout(layout: 'WebVizLayoutSpec') -> 'WebVizLayoutPlugin' Look up a registered layout ID or return a direct layout plugin. unregister_web_viz_layout(layout_id: 'str') -> 'None' Remove a registered layout plugin (for tests and notebook cleanup). Literal(*args, **kwargs) Union(*args, **kwargs) exporter.SeriesBindingDocstringCallbackSpec Represent a PEP 604 union type E.g. for int | str LightweightVizPayload(version: 'int', core: 'LightweightVizCore', overlays: 'tuple[LightweightVizOverlay, ...]', annotations: 'Mapping[str, Any] | None' = None, viewer_hints: 'Mapping[str, Any] | None' = None) -> None LightweightVizPayload(version: 'int', core: 'LightweightVizCore', overlays: 'tuple[LightweightVizOverlay, ...]', annotations: 'Mapping[str, Any] | None' = None, viewer_hints: 'Mapping[str, Any] | None' = None) exporter.LAYOUT_FORCEATLAS2 str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'. exporter.LAYOUT_GRAPHVIZ_DOT str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'. exporter.LAYOUT_GRAPHVIZ_SFDP str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'. exporter.LAYOUT_MULTIPARTITE str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'. exporter.LAYOUT_SPRING str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'. exporter.LAYOUT_STRATIFIED_MULTIPARTITE str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'. exporter.WebVizLayoutSpec Represent a PEP 604 union type E.g. for int | str GreaterThanCell(other: 'str') -> None Metadata marker: the annotated cell is always greater than another cell. NotEqualCell(other: 'str') -> None Metadata marker: the annotated cell is never equal to another cell. RealBetween(min: 'float | int | None' = None, max: 'float | int | None' = None) -> None Real-valued interval constraint for Annotated float types (not enumerable for dynamic refs). RealIntervalDomain(min: 'float | None' = None, max: 'float | None' = None) -> None Closed real-valued interval metadata; not enumerable for dynamic-ref branching. EdgeProvenance(causes: 'frozenset[DependencyCause]', direct_sites_formula: 'tuple[tuple[int, int], ...]' = (), direct_sites_normalized: 'tuple[tuple[int, int], ...]' = ()) -> None Metadata for a single directed edge, possibly from multiple mechanisms in one formula. DynamicRefConfig(cell_type_env: 'CellTypeEnv', limits: 'DynamicRefLimits') -> None Configuration for resolving OFFSET/INDIRECT via constraint-based inference. Prefer building via `from_constraints`; the constructor is for internal use. DynamicRefLimits(max_branches: 'int' = 1024, max_cells: 'int' = 10000, max_depth: 'int' = 10) -> None Tuneable safety limits for dynamic-reference inference. Pass a custom instance via the `limits` parameter of `DynamicRefConfig.from_constraints` or `DynamicRefConfig.from_constraints_and_workbook` to override any of these defaults. Attributes: max_branches: Maximum number of discrete value assignments explored during constraint enumeration. This cap is applied in two places: * **Per-dependency domain size** - a single cell constrained to an integer interval wider than *max_branches* values cannot be enumerated; the caller must either tighten the constraint or rely on the symbolic (abstract) analysis path. * **Cartesian-product size** - when a formula cell falls back to brute-force evaluation over all combinations of its dependencies' domains, the product of those domain sizes must not exceed *max_branches*. If it does, a `DynamicRefError` is raised immediately (rather than hanging) with a breakdown of which dependencies contributed to the explosion. Raise this limit or tighten the offending constraints to resolve the error. Default: `1024`. max_cells: Maximum number of cells collected when expanding a range reference. Default: `10_000`. max_depth: Maximum AST-evaluation recursion depth. Default: `10`. DynamicRefTraceEvent(kind: 'str', name: 'str', elapsed_s: 'float' = 0.0, detail: 'dict[str, Any]' = ) -> None A single trace event emitted during dynamic-ref inference. Attributes: kind: Event type (`"infer"`, `"expand-env"`, `"build-domains"`, `"build-value-domains"`, `"offset-scalar-fallback"`, `"offset-scalar-wide"`, plus `"-error"` variants). name: Function that emitted the event. elapsed_s: Wall-clock seconds spent in the traced operation. Defaults to `0.0` for structural events (e.g. `"index-abstract"`, `"index-enumerated"`) that report *what* happened rather than how long it took. detail: Flexible per-event payload (target counts, branch estimates, expressions, etc.). FromWorkbook() -> None Metadata marker: resolve domain from the current cached workbook value. Use `Annotated[T, FromWorkbook()]` in the constraints schema to derive a singleton domain at config-build time instead of hardcoding a `Literal`. This eliminates maintenance when the workbook template changes, at the cost of a slower `DynamicRefConfig.from_constraints_and_workbook` call (the workbook must be opened and each marked cell read via a streaming parser). LightweightVizLocalEdges(offsets: 'tuple[int, ...]', targets: 'tuple[int, ...]', guarded: 'tuple[bool, ...]', complete: 'tuple[bool, ...]') -> None LightweightVizLocalEdges(offsets: 'tuple[int, ...]', targets: 'tuple[int, ...]', guarded: 'tuple[bool, ...]', complete: 'tuple[bool, ...]') LightweightVizModule(id: 'int', node_count: 'int', rank_min: 'int', rank_max: 'int', centroid_x: 'float', centroid_y: 'float', density_mode: 'bool') -> None LightweightVizModule(id: 'int', node_count: 'int', rank_min: 'int', rank_max: 'int', centroid_x: 'float', centroid_y: 'float', density_mode: 'bool') LightweightVizModuleEdge(source_module_id: 'int', target_module_id: 'int', unconditional_weight: 'int', guarded_weight: 'int') -> None LightweightVizModuleEdge(source_module_id: 'int', target_module_id: 'int', unconditional_weight: 'int', guarded_weight: 'int') LightweightVizNodeColumns(sheet_index: 'tuple[int, ...]', row: 'tuple[int, ...]', column: 'tuple[str, ...]', is_leaf: 'tuple[bool, ...]', formula: 'tuple[str | None, ...]', in_degree: 'tuple[int, ...]', out_degree: 'tuple[int, ...]', module_id: 'tuple[int, ...]', rank: 'tuple[int, ...]', x: 'tuple[float, ...]', y: 'tuple[float, ...]', bucket_density: 'tuple[int, ...]') -> None LightweightVizNodeColumns(sheet_index: 'tuple[int, ...]', row: 'tuple[int, ...]', column: 'tuple[str, ...]', is_leaf: 'tuple[bool, ...]', formula: 'tuple[str | None, ...]', in_degree: 'tuple[int, ...]', out_degree: 'tuple[int, ...]', module_id: 'tuple[int, ...]', rank: 'tuple[int, ...]', x: 'tuple[float, ...]', y: 'tuple[float, ...]', bucket_density: 'tuple[int, ...]') LightweightVizPayload(version: 'int', core: 'LightweightVizCore', overlays: 'tuple[LightweightVizOverlay, ...]', annotations: 'Mapping[str, Any] | None' = None, viewer_hints: 'Mapping[str, Any] | None' = None) -> None LightweightVizPayload(version: 'int', core: 'LightweightVizCore', overlays: 'tuple[LightweightVizOverlay, ...]', annotations: 'Mapping[str, Any] | None' = None, viewer_hints: 'Mapping[str, Any] | None' = None) LightweightVizStats(node_count: 'int', scc_count: 'int', module_count: 'int', module_edge_count: 'int', local_edge_count: 'int', truncated_local_nodes: 'int', dense_bucket_count: 'int') -> None LightweightVizStats(node_count: 'int', scc_count: 'int', module_count: 'int', module_edge_count: 'int', local_edge_count: 'int', truncated_local_nodes: 'int', dense_bucket_count: 'int') LocalForceSubgraph(node_ids: 'tuple[int, ...]', edges_from: 'tuple[int, ...]', edges_to: 'tuple[int, ...]', edges_guarded: 'tuple[bool, ...]', is_module_scope: 'bool', truncated: 'bool') -> None LocalForceSubgraph(node_ids: 'tuple[int, ...]', edges_from: 'tuple[int, ...]', edges_to: 'tuple[int, ...]', edges_guarded: 'tuple[bool, ...]', is_module_scope: 'bool', truncated: 'bool') CycleReport(has_must_cycles: 'bool', has_may_cycles: 'bool', must_cycles: 'list[set[NodeKey]]', may_cycles: 'list[set[NodeKey]]', example_must_cycle_path: 'list[NodeKey] | None' = None, example_may_cycle_path: 'list[NodeKey] | None' = None) -> None Result of cycle analysis. DependencyGraph(_nodes: 'dict[NodeKey, Node]' = , _edges: 'dict[NodeKey, set[NodeKey]]' = , _reverse_edges: 'dict[NodeKey, set[NodeKey]]' = , _guards: 'dict[EdgeKey, GuardExpr]' = , _edge_extra: 'dict[EdgeKey, dict[str, Any]]' = , _hooks: 'list[NodeHook]' = , leaf_classification: 'dict[str, str] | None' = None, sheet_order: 'list[str] | None' = None, sheet_bounds: 'dict[str, tuple[int, int]] | None' = None, named_ranges: 'dict[str, tuple[str, str]] | None' = None, named_range_ranges: 'dict[str, tuple[str, str, str]] | None' = None) -> None DependencyGraph(_nodes: 'dict[NodeKey, Node]' = , _edges: 'dict[NodeKey, set[NodeKey]]' = , _reverse_edges: 'dict[NodeKey, set[NodeKey]]' = , _guards: 'dict[EdgeKey, GuardExpr]' = , _edge_extra: 'dict[EdgeKey, dict[str, Any]]' = , _hooks: 'list[NodeHook]' = , leaf_classification: 'dict[str, str] | None' = None, sheet_order: 'list[str] | None' = None, sheet_bounds: 'dict[str, tuple[int, int]] | None' = None, named_ranges: 'dict[str, tuple[str, str]] | None' = None, named_range_ranges: 'dict[str, tuple[str, str, str]] | None' = None) And(operands: 'tuple[GuardExpr, ...]') -> None Logical AND. Compare(left: 'GuardExpr', op: 'str', right: 'GuardExpr') -> None Comparison: left op right. GuardExpr() -> None Base type for conditional dependency guards. Literal(value: 'Any') -> None A literal value in a condition. Not(operand: 'GuardExpr') -> None Logical negation. Or(operands: 'tuple[GuardExpr, ...]') -> None Logical OR. CellRef(key: 'NodeKey') -> None A cell reference used in a condition. Node(sheet: 'str', column: 'str', row: 'int', formula: 'str | None', normalized_formula: 'str | None', value: 'Any', is_leaf: 'bool', is_target: 'bool' = False, metadata: 'dict[str, Any]' = ) -> None Workbook cell in a dependency graph. `is_leaf` is true when the node has no outgoing dependency edges (value-only cells and literal-only formulas such as `=1+1`). WorkbookCalcSettings(iterate_enabled: 'bool', iterate_count: 'int', iterate_delta: 'float') -> None WorkbookCalcSettings(iterate_enabled: 'bool', iterate_count: 'int', iterate_delta: 'float') GraphReadView(*args, **kwargs) Read-only dependency-graph surface shared by graphs and projected views. Consumers that only read a graph (for example `to_networkx` and `CodeGenerator`) can accept any object satisfying this protocol, including projected facades such as `ProjectionResult`, without depending on the concrete `DependencyGraph` type. It captures node iteration, node and edge lookups, key listings, leaf/formula/target classification, and evaluation order; mutation is intentionally excluded. CacheValidationPolicy(*values) DependencyCause(*values) How a dependency edge arises from a formula. DynamicRefError Raised when dynamic reference analysis cannot proceed. When building a dependency graph, pass a `DynamicRefConfig` (e.g. via `DynamicRefConfig.from_constraints`) or set `use_cached_dynamic_refs=True` to resolve OFFSET/INDIRECT instead of raising. CycleError(message: 'str', cycle_path: 'list[NodeKey]', is_must_cycle: 'bool') Raised when a cycle prevents computing evaluation order. ValidationResult(is_valid: ForwardRef('bool'), in_graph_not_in_chain: ForwardRef('set[str]'), in_chain_not_in_graph: ForwardRef('set[str]'), messages: ForwardRef('list[str]')) ValidationResult(is_valid, in_graph_not_in_chain, in_chain_not_in_graph, messages) normalize_blank_range_specs(specs: 'Iterable[str] | None') -> 'tuple[BlankRangeRect, ...]' Normalize a sequence of sheet-qualified range strings. create_dependency_graph(workbook: 'Path | str', targets: 'Iterable[str]', *, max_depth: 'int' = 50, expand_ranges: 'bool' = True, max_range_cells: 'int' = 5000, hooks: 'list[NodeHook] | None' = None, load_values: 'bool' = True, dynamic_refs: 'DynamicRefConfig | None' = None, use_cached_dynamic_refs: 'bool' = False, capture_dependency_provenance: 'bool' = False, blank_ranges: 'Iterable[str] | None' = None, type_analysis_cache: 'TypeAnalysisCache | None' = None) -> 'DependencyGraph' Build a dependency graph starting from target cells. `targets` accepts any mix of: - sheet-qualified single cells (`"Sheet1!A1"`, `"'My Sheet'!B2"`); - sheet-qualified rectangular ranges (`"Sheet1!B12:F12"`, `"Sheet1!A1:Sheet1!B2"`, `"'My Sheet'!A1:B2"`); and - defined names that resolve to a single cell or a rectangular range (`"MyInput"`, `"DataRange"`). Range and named-range targets are expanded to one BFS root per cell (subject to `max_range_cells`); the deduplicated union seeds traversal. Targets that are neither sheet-qualified nor a known defined name raise `ValueError`. Supports basic A1 references, sheet-qualified references, and dynamic references (OFFSET/INDIRECT). For OFFSET/INDIRECT: - **use_cached_dynamic_refs=True**: Resolve using cached workbook values (existing path). `dynamic_refs` is ignored. - **use_cached_dynamic_refs=False** (default), **dynamic_refs=None**: On any formula that contains OFFSET or INDIRECT requiring resolution, raise `DynamicRefError`. Callers can pass a `DynamicRefConfig` or set `use_cached_dynamic_refs=True` to avoid. - **use_cached_dynamic_refs=False**, **dynamic_refs** set: Resolve OFFSET/INDIRECT via the config's `cell_type_env` and `limits`; missing or invalid domains raise `DynamicRefError`. To build a config from a `dict[str, type]` constraints schema, use `DynamicRefConfig.from_constraints`. When `capture_dependency_provenance` is True, each edge stores merged `excel_grapher.grapher.dependency_provenance.EdgeProvenance` under the `\"provenance\"` key in `DependencyGraph.edge_attrs` (how the dependency arises: direct reference, static range, dynamic OFFSET/INDIRECT). `blank_ranges` is an optional iterable of sheet-qualified A1 rectangles (e.g. `\"Sheet1!B2:D10\"`) treated as structurally empty: no nodes are created for those cells (edges into them are kept), and dynamic-ref leaf constraints are not required for addresses inside these ranges. Pair with the same declarations on `excel_grapher.FormulaEvaluator` and `excel_grapher.exporter.codegen.CodeGenerator.generate` for **evaluator <-> export** parity (consistent behavior between evaluation and generated code). **Cost model**: constraint-based dynamic-ref expansion (`dynamic_refs` set, `use_cached_dynamic_refs=False`) runs `expand_leaf_env_to_argument_env` once per formula regardless of `capture_dependency_provenance`. A shared per-graph cache ensures provenance collection reuses the already-computed expansion instead of repeating it. Callers doing iterative constraint-tuning workflows can still set `capture_dependency_provenance=False` to avoid any provenance overhead (formula-string span collection, branch-union merging, etc.). list_dynamic_ref_constraint_candidates(workbook: 'Path | str | fastpyxl.Workbook', targets: 'Iterable[str]', *, dynamic_refs: 'DynamicRefConfig | None' = None, max_depth: 'int' = 50, max_range_cells: 'int' = 5000, type_analysis_cache: 'TypeAnalysisCache | None' = None) -> 'list[str]' Return sorted leaf cells missing dynamic-ref constraint entries. These are leaf cell addresses that feed dynamic-ref arguments (OFFSET/INDIRECT/INDEX) but have no entry in `dynamic_refs.cell_type_env`. Unlike `create_dependency_graph`, this function does **not** raise `DynamicRefError` when constraints are missing. Instead it collects all missing leaf addresses in a single pass and returns them sorted. When `dynamic_refs` is `None` the function treats it as an empty constraint environment: all leaf cells that feed dynamic-ref arguments are returned as candidates. **Completeness caveat**: Cells reachable only through unresolvable dynamic refs will not be visited, so their constraint candidates won't appear in the output. A second call after adding the first batch of constraints will quickly find any remaining missing entries. build_graph_cache_meta(workbook_path: 'Path', targets: 'list[str]', *, extraction_params: 'dict[str, Any] | None' = None, schema_version: 'int' = 1) -> 'GraphCacheMeta' build_graph_cache_meta_portable(targets: 'list[str]', *, extraction_params: 'dict[str, Any] | None' = None, schema_version: 'int' = 1, excel_grapher_version: 'str | None' = None) -> 'GraphCacheMeta' Build expected cache metadata without access to the workbook file. Portable validation enforces only: - schema_version - excel_grapher_version - targets_sha256 - extraction_params save_graph_cache(path: 'Path', graph: 'DependencyGraph', meta: 'GraphCacheMeta') -> 'None' try_load_graph_cache(path: 'Path', *, expected_meta: 'GraphCacheMeta', policy: 'CacheValidationPolicy' = ) -> 'DependencyGraph | None' infer_dynamic_index_targets(formula: 'str', *, current_sheet: 'str', cell_type_env: 'CellTypeEnv', limits: 'DynamicRefLimits | None' = None, bounds: 'WorkbookBoundsProtocol | None' = None, named_ranges: 'Mapping[str, tuple[str, str]] | None' = None, named_range_ranges: 'Mapping[str, tuple[str, str, str]] | None' = None, current_row: 'int | None' = None, current_col: 'int | None' = None) -> 'set[str]' Infer the union of all possible standalone INDEX targets for a formula. INDEX calls that appear as the first argument of OFFSET are skipped - those are already handled by `infer_dynamic_offset_targets`. infer_dynamic_indirect_targets(formula: 'str', *, current_sheet: 'str', cell_type_env: 'CellTypeEnv', limits: 'DynamicRefLimits | None' = None, bounds: 'WorkbookBoundsProtocol | None' = None, named_ranges: 'Mapping[str, tuple[str, str]] | None' = None, named_range_ranges: 'Mapping[str, tuple[str, str, str]] | None' = None) -> 'set[str]' Infer the union of all possible INDIRECT targets for a formula. infer_dynamic_offset_targets(formula: 'str', *, current_sheet: 'str', cell_type_env: 'CellTypeEnv', limits: 'DynamicRefLimits | None' = None, bounds: 'WorkbookBoundsProtocol | None' = None, named_ranges: 'Mapping[str, tuple[str, str]] | None' = None, named_range_ranges: 'Mapping[str, tuple[str, str, str]] | None' = None, current_row: 'int | None' = None, current_col: 'int | None' = None) -> 'set[str]' Infer the union of all possible OFFSET targets for a formula. This helper is intentionally focused and conservative: - Only OFFSET calls are analysed (INDIRECT is currently ignored). - Arguments may use a small Excel expression subset supported by `core.expr_eval.evaluate_expr`. - Leaf cells referenced by OFFSET/INDEX arguments must have a numeric domain in `cell_type_env` unless they appear only in ref_only argument positions (see `excel_grapher.core.excel_function_meta`). - Integer interval domains must be finite and small enough to enumerate. trace_dynamic_refs(callback: 'DynamicRefTraceFn') -> 'Iterator[None]' Activate *callback* as the dynamic-ref tracer for the enclosed block. Nestable: an inner `trace_dynamic_refs` overrides the outer one; the outer tracer is restored when the inner block exits. select_local_force_subgraph(payload: 'LightweightVizPayload', *, node_id: 'int') -> 'LocalForceSubgraph' select_path_induced_subgraph(graph: 'DependencyGraph', *, source_keys: 'Sequence[NodeKey]', target_keys: 'Sequence[NodeKey]', max_path_length: 'int | None' = None, max_paths: 'int | None' = None, include_endpoints: 'bool' = True) -> 'DependencyGraph' Return the induced subgraph over nodes lying on directed source->target paths. Edge direction follows `DependencyGraph` semantics: `A -> B` means `A` depends on `B`. to_graphviz(graph: 'DependencyGraph', *, label_fn: 'Callable[[NodeKey, Node | NodeView], str] | None' = None, highlight: 'set[NodeKey] | None' = None, rankdir: 'str' = 'TB', include_formula_on_nodes: 'bool' = True, max_formula_length: 'int | None' = 120) -> 'str' to_mermaid(graph: 'DependencyGraph', *, label_fn: 'Callable[[NodeKey, Node | NodeView], str] | None' = None, max_nodes: 'int' = 100, include_formula_on_nodes: 'bool' = True, max_formula_length: 'int | None' = 120) -> 'str' to_networkx(graph: 'DependencyGraph | GraphReadView', *, include_formula_on_nodes: 'bool' = True, max_formula_length: 'int | None' = 120) Convert a dependency graph to a NetworkX DiGraph. Accepts `DependencyGraph` or any graph-like object with node iteration, dependency lookup, and edge attributes (for example `ProjectionResult`). NetworkX is an optional dependency. If not installed, raises ImportError with a helpful message. write_lightweight_viz_data(payload: 'LightweightVizPayload', path: 'Path | str') -> 'None' write_lightweight_viz_html(payload: 'LightweightVizPayload', path: 'Path | str', *, title: 'str' = 'Workbook dependency graph', data_mode: "Literal['inline', 'sidecar', 'auto']" = 'auto', data_path: 'Path | str | None' = None, inline_size_budget_mb: 'int' = 50, template_path: 'Path | str | None' = None) -> 'None' write_web_viz_html(payload: 'LightweightVizPayload', path: 'Path | str', *, title: 'str' = 'Workbook dependency graph', data_mode: "Literal['inline', 'sidecar', 'auto']" = 'auto', data_path: 'Path | str | None' = None, inline_size_budget_mb: 'int' = 50, template_path: 'Path | str | None' = None) -> 'None' Write a web visualization HTML bundle from a web-viz payload. format_cell_key(sheet: 'str', column: 'str', row: 'int') -> 'NormalizedAddress' Format a (sheet, column_letters, row) triple into a canonical address. needs_quoting(sheet: 'str') -> 'bool' Return True if a sheet name must be wrapped in single quotes in a formula. get_calc_settings(workbook_path: 'Path') -> 'WorkbookCalcSettings' Extract calculation settings from xl/workbook.xml. Defaults follow Excel's typical defaults when attributes are missing. validate_graph(graph: 'DependencyGraph', workbook_path: 'Path', *, scope: 'set[str] | None' = None) -> 'ValidationResult' grapher.GRAPH_CACHE_SCHEMA_VERSION int([x]) -> integer int(x, base=10) -> integer Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.__int__(). For floating-point numbers, this truncates towards zero. If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer iteral. >>> int('0b100', base=0) 4 Callable(*args, **kwargs) Callable(*args, **kwargs) str str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'. format_key(sheet: 'str', cell: 'str') -> 'NormalizedAddress' Format a sheet and A1 cell coordinate into a canonical address string. GoogleSeriesDocstringRenderer() NumpySeriesDocstringRenderer() PlainSeriesDocstringRenderer() RstSeriesDocstringRenderer() ResolutionIssue Alias for resolution-time validation issues. FieldDoc(description: 'str') -> None FieldDoc(description: 'str') SeriesBindingDocstringContext(graph: 'DependencyGraph', workbook: 'Path | str', bindings: 'WorkbookSeriesBindings', series: 'dict[str, Any]', resolution: 'SeriesResolution', contract: 'SeriesBindingDocstringContract', function_kind: 'SeriesFunctionKind', function_name: 'str') -> None SeriesBindingDocstringContext(graph: 'DependencyGraph', workbook: 'Path | str', bindings: 'WorkbookSeriesBindings', series: 'dict[str, Any]', resolution: 'SeriesResolution', contract: 'SeriesBindingDocstringContract', function_kind: 'SeriesFunctionKind', function_name: 'str') SeriesBindingDocstringContract(series_id: 'str', function_name: 'str', function_kind: 'SeriesFunctionKind', data_range: 'str', layout: 'str', value_type: 'str | None', required_fields: 'tuple[str, ...]', fields: 'Mapping[str, FieldContract]', example_records: 'tuple[Mapping[str, object], ...]', notes: 'str') -> None SeriesBindingDocstringContract(series_id: 'str', function_name: 'str', function_kind: 'SeriesFunctionKind', data_range: 'str', layout: 'str', value_type: 'str | None', required_fields: 'tuple[str, ...]', fields: 'Mapping[str, FieldContract]', example_records: 'tuple[Mapping[str, object], ...]', notes: 'str') SeriesFunctionDoc(summary: 'str', purpose: 'str', record_matching: 'str', field_descriptions: 'Mapping[str, FieldDoc]' = ) -> None SeriesFunctionDoc(summary: 'str', purpose: 'str', record_matching: 'str', field_descriptions: 'Mapping[str, FieldDoc]' = ) SeriesDocstringRenderCallable(*args, **kwargs) SeriesDocstringRenderer(*args, **kwargs) SeriesBindingDocstringCallback(*args, **kwargs) SeriesBindingsLoadError Raised when binding files cannot be parsed or merged. SeriesBindingsSchemaError Raised when a binding manifest fails JSON Schema validation. InputSeries InputSeriesCell LeafResolution OutputSeries OutputSeriesCell ResolutionReport SeriesResolution ValidationIssue ValidationReport WorkbookSeriesBindings Top-level workbook binding manifest (post-merge, post-schema validation). emit_series_bindings_block(graph: 'DependencyGraph', workbook: 'Path | str', bindings: 'WorkbookSeriesBindings', *, export_addresses: 'Iterable[str] | None' = None, series_docstring_callback: 'SeriesBindingDocstringCallbackSpec | None' = None, docstring_renderer: 'SeriesDocstringRendererSpec' = 'google') -> 'list[str]' Emit setter and/or output compute functions for a binding manifest. bindings_canonical_sha256(bindings: 'Mapping[str, Any]') -> 'str' SHA-256 hex digest of the canonical binding manifest. emit_compute_function(series: 'dict[str, Any]', resolved: 'SeriesResolution', *, graph: 'DependencyGraph | None' = None, workbook: 'Path | str | None' = None, bindings: 'WorkbookSeriesBindings | None' = None, series_docstring_callback: 'SeriesBindingDocstringCallbackSpec | None' = None, docstring_renderer: 'SeriesDocstringRendererSpec' = 'google', include_datetime_import: 'bool' = True) -> 'list[str]' Emit Python source lines for one series binding output compute function. emit_computes_block(graph: 'DependencyGraph', workbook: 'Path | str', bindings: 'WorkbookSeriesBindings', *, export_addresses: 'Iterable[str] | None' = None, include_type_aliases: 'bool' = True, series_docstring_callback: 'SeriesBindingDocstringCallbackSpec | None' = None, docstring_renderer: 'SeriesDocstringRendererSpec' = 'google') -> 'list[str]' Emit all series output compute functions for a validated binding manifest. generate_computes_module(graph: 'DependencyGraph', workbook: 'Path | str', bindings: 'WorkbookSeriesBindings') -> 'str' Generate a standalone module fragment with output compute functions. resolve_series_docstring_renderer(renderer: 'SeriesDocstringRendererSpec') -> 'SeriesDocstringRenderer' list_series_docstring_callbacks() -> 'tuple[str, ...]' register_series_docstring_callback(name: 'str', callback: 'SeriesBindingDocstringCallback', *, replace: 'bool' = False) -> 'None' resolve_series_docstring_callback(callback: 'SeriesBindingDocstringCallbackSpec') -> 'SeriesBindingDocstringCallback' Look up a registered callback name or return a direct callback object. unregister_series_docstring_callback(name: 'str') -> 'None' Remove a registered callback (for tests and notebook cleanup). derive_input_series(graph: 'DependencyGraph', bindings: 'WorkbookSeriesBindings', *, workbook: 'Path | str') -> 'list[InputSeries]' Return one input series per binding series with graph-leaf overlap. Series bindings are the semantic source of truth: each input series corresponds to one manifest `series[]` entry, and each cell corresponds to a resolved graph leaf participating in that series. load_series_bindings(path: 'Path | str', *, validate: 'bool' = True) -> 'WorkbookSeriesBindings' Load a binding sidecar file or directory of shards. When `path` is a directory, all `*.bindings.yaml` / `*.bindings.json` files are merged in sorted filename order before schema validation. merge_series_binding_documents(documents: 'list[dict[str, Any]]') -> 'dict[str, Any]' Merge partial manifests (e.g. one per sheet) into one workbook document. parse_bindings_file(path: 'Path | str') -> 'dict[str, Any]' Parse one binding sidecar file (YAML or JSON) without schema validation. has_input_direction(series: 'dict[str, Any]') -> 'bool' has_output_direction(series: 'dict[str, Any]') -> 'bool' merge_series_entries(existing: 'dict[str, Any]', incoming: 'dict[str, Any]', *, shard_index: 'int') -> 'dict[str, Any]' Merge two series entries with the same id from different shards. normalize_bindings_document(document: 'dict[str, Any]') -> 'dict[str, Any]' Normalize all series entries in a binding manifest. normalize_series_entry(series: 'dict[str, Any]') -> 'dict[str, Any]' Return a copy with legacy aliases normalized for schema validation and codegen. derive_output_series(graph: 'DependencyGraph', bindings: 'WorkbookSeriesBindings', *, workbook: 'Path | str') -> 'list[OutputSeries]' Return one output series per binding entry with output.compute and graph overlap. expand_data_range(data_range: 'str', *, workbook: 'Path | str | None' = None, sheetnames: 'Sequence[str] | None' = None, named_ranges: 'Mapping[str, tuple[str, str]] | None' = None, named_range_ranges: 'Mapping[str, tuple[str, str, str]] | None' = None, max_range_cells: 'int' = 5000) -> 'list[str]' Expand a binding `data_range` to canonical sheet-qualified cell addresses. Uses the same target expansion as `create_dependency_graph` (including both-end sheet-qualified ranges like `Sheet1!A1:Sheet1!B2` and defined names). expand_data_range_for_graph(graph: 'DependencyGraph', data_range: 'str', *, workbook: 'Path | str | None' = None, max_range_cells: 'int' = 5000) -> 'list[str]' Expand `data_range` using named-range maps (and optional workbook) from a graph. resolve_series_binding(graph: 'DependencyGraph', workbook: 'Path | str', series: 'dict[str, Any]', *, direction: 'BindingDirection' = 'input', export_addresses: 'Iterable[str] | None' = None, concept_scheme: 'dict[str, Any] | None' = None) -> 'SeriesResolution' Resolve each participating cell in a series binding to coordinates and record fields. resolve_series_bindings(graph: 'DependencyGraph', bindings: 'WorkbookSeriesBindings', *, workbook: 'Path | str | None' = None, direction: 'BindingDirection' = 'input', export_addresses: 'Iterable[str] | None' = None) -> 'ResolutionReport' Resolve all series in a binding manifest for the given direction. format_schema_errors(document: 'dict[str, Any]') -> 'list[str]' Return human-readable schema error strings (for tests and CLI). validate_bindings_document(document: 'dict[str, Any]') -> 'WorkbookSeriesBindings' Validate `document` against the series binding JSON Schema. Returns the document unchanged on success (typed for callers). Raises `SeriesBindingsSchemaError` on failure. emit_setter_function(series: 'dict[str, Any]', resolved: 'SeriesResolution', *, graph: 'DependencyGraph | None' = None, workbook: 'Path | str | None' = None, bindings: 'WorkbookSeriesBindings | None' = None, series_docstring_callback: 'SeriesBindingDocstringCallbackSpec | None' = None, docstring_renderer: 'SeriesDocstringRendererSpec' = 'google', include_datetime_import: 'bool' = True) -> 'list[str]' Emit Python source lines for one series binding setter. emit_setter_helpers() -> 'list[str]' Emit shared private helpers used by generated `set_*` functions. emit_setters_block(graph: 'DependencyGraph', workbook: 'Path | str', bindings: 'WorkbookSeriesBindings', *, export_addresses: 'Iterable[str] | None' = None, include_type_aliases: 'bool' = True, series_docstring_callback: 'SeriesBindingDocstringCallbackSpec | None' = None, docstring_renderer: 'SeriesDocstringRendererSpec' = 'google') -> 'list[str]' Emit all series setter functions for a validated binding manifest. generate_setters_module(graph: 'DependencyGraph', workbook: 'Path | str', bindings: 'WorkbookSeriesBindings') -> 'str' Generate a standalone module fragment with setters (requires EvalContext imports). validate_series_bindings(graph: 'DependencyGraph', bindings: 'WorkbookSeriesBindings', *, workbook: 'Path | str | None' = None) -> 'ValidationReport' Validate binding manifests against an extracted dependency graph. is_bind_implemented(kind: 'str | None') -> 'bool' is_layout_implemented(layout: 'str | None') -> 'bool' Literal(*args, **kwargs) Union(*args, **kwargs) series_bindings.SeriesBindingDocstringCallbackSpec Represent a PEP 604 union type E.g. for int | str dict(*args, **kwargs) dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2) list(iterable=(), /) Built-in mutable sequence. If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified. series_bindings.Scalar Represent a PEP 604 union type E.g. for int | str Literal(*args, **kwargs) series_bindings.IMPLEMENTED_BIND_KINDS Build an immutable unordered collection of unique elements. series_bindings.IMPLEMENTED_LAYOUTS Build an immutable unordered collection of unique elements. series_bindings.PLANNED_BIND_KINDS Build an immutable unordered collection of unique elements. series_bindings.PLANNED_LAYOUTS Build an immutable unordered collection of unique elements. series_bindings.SUPPORTED_SCHEMA_VERSIONS Build an immutable unordered collection of unique elements. ## Exporter API Export and extension APIs under excel_grapher.exporter CodeGenerator(graph: 'DependencyGraph | GraphLike', *, iterate_enabled: 'bool | None' = None, iterate_count: 'int' = 100, iterate_delta: 'float' = 0.001) -> 'None' Generates Python code from Excel formulas. IdentityTransitCompression() Collapse pure identity transit nodes into a projected export graph. ProjectionStep(*args, **kwargs) Projection step that builds a `ProjectionResult` from a graph. ProjectionResult(original_graph: 'DependencyGraph', projected_graph: 'DependencyGraph', manifest: 'ProjectionManifest') -> None Projected graph facade with durable lineage back to the canonical graph. ProjectionManifest(*args, **kwargs) Stable contract a projection manifest exposes to export and composition. Implementations must surface a `kind` tag, an address-mapping helper that codegen consumes, plus a serializable `to_dict`. Kind-specific forwarding maps and lineage may be carried as additional fields. BaseProjectionManifest(kind: 'str', forwarding_map: 'dict[str, str]', retained_to_collapsed_sources: 'dict[str, tuple[str, ...]]', removed_node_snapshots: 'dict[str, ProjectedNodeSnapshot]', formula_rewrites: 'tuple[FormulaRewrite, ...]', collapsed_groups: 'tuple[CollapsedGroup, ...]') -> None General, serializable projection manifest for node-collapsing projections. Directly usable by any projection that removes nodes and (optionally) maps value-equivalent removed addresses to retained computations; custom projections may also subclass to add kind-specific fields. Register `from_dict` under the manifest `kind` via `register_projection_manifest` to enable deserialization. CompositeProjectionManifest(forwarding_map: 'dict[str, str]', steps: 'tuple[ProjectionManifest, ...]', kind: 'str' = 'composite') -> None Lineage for a sequence of composed projection steps. Codegen consumes `map_to_projected`, which chains each step's mapping in order and works for any step satisfying the `ProjectionManifest` protocol. `forwarding_map` is a precomputed, serializable view of that chained mapping for audit and round-trips; it is derived only from steps that expose a `forwarding_map` field, so it may be empty or partial for protocol-only steps even when `map_to_projected` still maps correctly. `steps` preserves each component manifest. Heterogeneous step kinds are supported. ProjectedNodeSnapshot(address: 'str', sheet: 'str', column: 'str', row: 'int', formula: 'str | None', normalized_formula: 'str | None', value: 'Any', is_target: 'bool', is_leaf: 'bool', metadata: 'dict[str, Any]') -> None Workbook node state captured before projection removes or rewrites it. FormulaRewrite(dependent: 'str', before_formula: 'str | None', after_formula: 'str | None', before_normalized: 'str | None', after_normalized: 'str | None') -> None Dependent formula rewrite performed during projection. CollapsedGroup(retained: 'str', collapsed_sources: 'tuple[str, ...]', statement_order: 'tuple[str, ...]', external_dependencies: 'tuple[str, ...]') -> None Source nodes that folded into one retained computation address. Node snapshots are stored once on the manifest's `removed_node_snapshots` and referenced here by address via `collapsed_sources` / `statement_order`. apply_projection(graph: 'DependencyGraph', projections: 'Iterable[ProjectionStep]') -> 'ProjectionResult' Apply one or more projection steps, returning the final projection result. Steps are applied in order, each projecting the previous step's projected graph. A single step's manifest is returned as-is; multiple steps are folded into a `CompositeProjectionManifest` carrying the chained forwarding map and every component manifest. Heterogeneous step kinds are supported. build_forwarding_projection_manifest(original_graph: 'DependencyGraph', record: 'IdentityTransitCompressionRecord', *, kind: 'str') -> 'BaseProjectionManifest' Build a `BaseProjectionManifest` from a graph compression `record`. All removed nodes are treated as value-equivalent forwarding aliases (the identity-transit contract). The collapse lineage is keyed back to `original_graph` for statement order and external-dependency boundaries. register_projection_manifest(kind: 'str', from_dict: 'ProjectionManifestFromDict', *, replace: 'bool' = False) -> 'None' Register a `from_dict` deserializer for projection manifests of `kind`. Args: kind: Manifest `kind` tag to register. from_dict: Callable building a manifest from its serialized mapping. replace: Allow overwriting an existing registration. Raises: ValueError: If `kind` is already registered and `replace` is False. resolve_projection_manifest(data: 'Mapping[str, Any]') -> 'ProjectionManifest' Deserialize a manifest by dispatching on its `kind` via the registry. Raises: ValueError: If the manifest `kind` has no registered deserializer. unregister_projection_manifest(kind: 'str') -> 'None' Remove a registered projection manifest `kind` (no-op if absent). LightweightVizPayload(version: 'int', core: 'LightweightVizCore', overlays: 'tuple[LightweightVizOverlay, ...]', annotations: 'Mapping[str, Any] | None' = None, viewer_hints: 'Mapping[str, Any] | None' = None) -> None LightweightVizPayload(version: 'int', core: 'LightweightVizCore', overlays: 'tuple[LightweightVizOverlay, ...]', annotations: 'Mapping[str, Any] | None' = None, viewer_hints: 'Mapping[str, Any] | None' = None) to_web_viz_payload(nx_graph: 'Any', *, max_local_nodes: 'int | None' = None, max_local_edges: 'int | None' = None, include_guarded_edges: 'bool' = True, include_guarded_edges_for_partition: 'bool' = False, layout: 'WebVizLayoutSpec' = 'stratified_multipartite', layout_config: 'dict[str, Any] | None' = None, include_formula_on_nodes: 'bool' = True, max_formula_length: 'int | None' = 120, seed: 'int' = 0, weight_attr: 'str | None' = None, include_module_overlay: 'bool' = True) -> 'WebVizPayload' Build a web-visualization payload from a NetworkX DiGraph. Layout is selected by `layout` (registered web layout plugin id or a direct `WebVizLayoutPlugin` callable). The default `stratified_multipartite` uses SCC-condensation longest-path rank on the vertical axis and Louvain community ordering on the horizontal axis when `include_module_overlay` is true. Other built-in ids include `spring`, `forceatlas2`, `multipartite` (NetworkX `multipartite_layout`), `graphviz_dot`, and `graphviz_sfdp`. Set `include_module_overlay=False` to skip the partition overlay (single module color; overview still draws local graph edges in the viewer). exporter.LAYOUT_FORCEATLAS2 str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'. exporter.LAYOUT_GRAPHVIZ_DOT str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'. exporter.LAYOUT_GRAPHVIZ_SFDP str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'. exporter.LAYOUT_MULTIPARTITE str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'. exporter.LAYOUT_SPRING str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'. exporter.LAYOUT_STRATIFIED_MULTIPARTITE str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'. WebVizLayoutPlugin(*args, **kwargs) exporter.WebVizLayoutSpec Represent a PEP 604 union type E.g. for int | str list_web_viz_layouts() -> 'tuple[str, ...]' register_web_viz_layout(layout_id: 'str', plugin: 'WebVizLayoutPlugin', *, replace: 'bool' = False) -> 'None' resolve_web_viz_layout(layout: 'WebVizLayoutSpec') -> 'WebVizLayoutPlugin' Look up a registered layout ID or return a direct layout plugin. unregister_web_viz_layout(layout_id: 'str') -> 'None' Remove a registered layout plugin (for tests and notebook cleanup). FieldDoc(description: 'str') -> None FieldDoc(description: 'str') GoogleSeriesDocstringRenderer() NumpySeriesDocstringRenderer() PlainSeriesDocstringRenderer() RstSeriesDocstringRenderer() SeriesDocstringRenderCallable(*args, **kwargs) SeriesDocstringRenderer(*args, **kwargs) Literal(*args, **kwargs) Union(*args, **kwargs) SeriesBindingDocstringCallback(*args, **kwargs) exporter.SeriesBindingDocstringCallbackSpec Represent a PEP 604 union type E.g. for int | str SeriesBindingDocstringContext(graph: 'DependencyGraph', workbook: 'Path | str', bindings: 'WorkbookSeriesBindings', series: 'dict[str, Any]', resolution: 'SeriesResolution', contract: 'SeriesBindingDocstringContract', function_kind: 'SeriesFunctionKind', function_name: 'str') -> None SeriesBindingDocstringContext(graph: 'DependencyGraph', workbook: 'Path | str', bindings: 'WorkbookSeriesBindings', series: 'dict[str, Any]', resolution: 'SeriesResolution', contract: 'SeriesBindingDocstringContract', function_kind: 'SeriesFunctionKind', function_name: 'str') SeriesBindingDocstringContract(series_id: 'str', function_name: 'str', function_kind: 'SeriesFunctionKind', data_range: 'str', layout: 'str', value_type: 'str | None', required_fields: 'tuple[str, ...]', fields: 'Mapping[str, FieldContract]', example_records: 'tuple[Mapping[str, object], ...]', notes: 'str') -> None SeriesBindingDocstringContract(series_id: 'str', function_name: 'str', function_kind: 'SeriesFunctionKind', data_range: 'str', layout: 'str', value_type: 'str | None', required_fields: 'tuple[str, ...]', fields: 'Mapping[str, FieldContract]', example_records: 'tuple[Mapping[str, object], ...]', notes: 'str') SeriesFunctionDoc(summary: 'str', purpose: 'str', record_matching: 'str', field_descriptions: 'Mapping[str, FieldDoc]' = ) -> None SeriesFunctionDoc(summary: 'str', purpose: 'str', record_matching: 'str', field_descriptions: 'Mapping[str, FieldDoc]' = ) list_series_docstring_callbacks() -> 'tuple[str, ...]' register_series_docstring_callback(name: 'str', callback: 'SeriesBindingDocstringCallback', *, replace: 'bool' = False) -> 'None' resolve_series_docstring_callback(callback: 'SeriesBindingDocstringCallbackSpec') -> 'SeriesBindingDocstringCallback' Look up a registered callback name or return a direct callback object. unregister_series_docstring_callback(name: 'str') -> 'None' Remove a registered callback (for tests and notebook cleanup). resolve_series_docstring_renderer(renderer: 'SeriesDocstringRendererSpec') -> 'SeriesDocstringRenderer' ## Grapher Namespaced API Public grapher APIs available under excel_grapher.grapher DependencyCause(*values) How a dependency edge arises from a formula. EdgeProvenance(causes: 'frozenset[DependencyCause]', direct_sites_formula: 'tuple[tuple[int, int], ...]' = (), direct_sites_normalized: 'tuple[tuple[int, int], ...]' = ()) -> None Metadata for a single directed edge, possibly from multiple mechanisms in one formula. LightweightVizLocalEdges(offsets: 'tuple[int, ...]', targets: 'tuple[int, ...]', guarded: 'tuple[bool, ...]', complete: 'tuple[bool, ...]') -> None LightweightVizLocalEdges(offsets: 'tuple[int, ...]', targets: 'tuple[int, ...]', guarded: 'tuple[bool, ...]', complete: 'tuple[bool, ...]') LightweightVizModule(id: 'int', node_count: 'int', rank_min: 'int', rank_max: 'int', centroid_x: 'float', centroid_y: 'float', density_mode: 'bool') -> None LightweightVizModule(id: 'int', node_count: 'int', rank_min: 'int', rank_max: 'int', centroid_x: 'float', centroid_y: 'float', density_mode: 'bool') LightweightVizModuleEdge(source_module_id: 'int', target_module_id: 'int', unconditional_weight: 'int', guarded_weight: 'int') -> None LightweightVizModuleEdge(source_module_id: 'int', target_module_id: 'int', unconditional_weight: 'int', guarded_weight: 'int') LightweightVizNodeColumns(sheet_index: 'tuple[int, ...]', row: 'tuple[int, ...]', column: 'tuple[str, ...]', is_leaf: 'tuple[bool, ...]', formula: 'tuple[str | None, ...]', in_degree: 'tuple[int, ...]', out_degree: 'tuple[int, ...]', module_id: 'tuple[int, ...]', rank: 'tuple[int, ...]', x: 'tuple[float, ...]', y: 'tuple[float, ...]', bucket_density: 'tuple[int, ...]') -> None LightweightVizNodeColumns(sheet_index: 'tuple[int, ...]', row: 'tuple[int, ...]', column: 'tuple[str, ...]', is_leaf: 'tuple[bool, ...]', formula: 'tuple[str | None, ...]', in_degree: 'tuple[int, ...]', out_degree: 'tuple[int, ...]', module_id: 'tuple[int, ...]', rank: 'tuple[int, ...]', x: 'tuple[float, ...]', y: 'tuple[float, ...]', bucket_density: 'tuple[int, ...]') LightweightVizStats(node_count: 'int', scc_count: 'int', module_count: 'int', module_edge_count: 'int', local_edge_count: 'int', truncated_local_nodes: 'int', dense_bucket_count: 'int') -> None LightweightVizStats(node_count: 'int', scc_count: 'int', module_count: 'int', module_edge_count: 'int', local_edge_count: 'int', truncated_local_nodes: 'int', dense_bucket_count: 'int') grapher.GRAPH_CACHE_SCHEMA_VERSION int([x]) -> integer int(x, base=10) -> integer Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.__int__(). For floating-point numbers, this truncates towards zero. If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer iteral. >>> int('0b100', base=0) 4 Callable(*args, **kwargs) GraphReadView(*args, **kwargs) Read-only dependency-graph surface shared by graphs and projected views. Consumers that only read a graph (for example `to_networkx` and `CodeGenerator`) can accept any object satisfying this protocol, including projected facades such as `ProjectionResult`, without depending on the concrete `DependencyGraph` type. It captures node iteration, node and edge lookups, key listings, leaf/formula/target classification, and evaluation order; mutation is intentionally excluded. Callable(*args, **kwargs) str str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'. infer_dynamic_index_targets(formula: 'str', *, current_sheet: 'str', cell_type_env: 'CellTypeEnv', limits: 'DynamicRefLimits | None' = None, bounds: 'WorkbookBoundsProtocol | None' = None, named_ranges: 'Mapping[str, tuple[str, str]] | None' = None, named_range_ranges: 'Mapping[str, tuple[str, str, str]] | None' = None, current_row: 'int | None' = None, current_col: 'int | None' = None) -> 'set[str]' Infer the union of all possible standalone INDEX targets for a formula. INDEX calls that appear as the first argument of OFFSET are skipped - those are already handled by `infer_dynamic_offset_targets`. infer_dynamic_indirect_targets(formula: 'str', *, current_sheet: 'str', cell_type_env: 'CellTypeEnv', limits: 'DynamicRefLimits | None' = None, bounds: 'WorkbookBoundsProtocol | None' = None, named_ranges: 'Mapping[str, tuple[str, str]] | None' = None, named_range_ranges: 'Mapping[str, tuple[str, str, str]] | None' = None) -> 'set[str]' Infer the union of all possible INDIRECT targets for a formula. infer_dynamic_offset_targets(formula: 'str', *, current_sheet: 'str', cell_type_env: 'CellTypeEnv', limits: 'DynamicRefLimits | None' = None, bounds: 'WorkbookBoundsProtocol | None' = None, named_ranges: 'Mapping[str, tuple[str, str]] | None' = None, named_range_ranges: 'Mapping[str, tuple[str, str, str]] | None' = None, current_row: 'int | None' = None, current_col: 'int | None' = None) -> 'set[str]' Infer the union of all possible OFFSET targets for a formula. This helper is intentionally focused and conservative: - Only OFFSET calls are analysed (INDIRECT is currently ignored). - Arguments may use a small Excel expression subset supported by `core.expr_eval.evaluate_expr`. - Leaf cells referenced by OFFSET/INDEX arguments must have a numeric domain in `cell_type_env` unless they appear only in ref_only argument positions (see `excel_grapher.core.excel_function_meta`). - Integer interval domains must be finite and small enough to enumerate. ## Series Bindings API Series binding manifest and codegen APIs InputSeries InputSeriesCell OutputSeries OutputSeriesCell dict(*args, **kwargs) dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2) list(iterable=(), /) Built-in mutable sequence. If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified. series_bindings.Scalar Represent a PEP 604 union type E.g. for int | str FieldDoc(description: 'str') -> None FieldDoc(description: 'str') GoogleSeriesDocstringRenderer() NumpySeriesDocstringRenderer() PlainSeriesDocstringRenderer() RstSeriesDocstringRenderer() SeriesDocstringRenderCallable(*args, **kwargs) SeriesDocstringRenderer(*args, **kwargs) Literal(*args, **kwargs) Union(*args, **kwargs) SeriesBindingDocstringCallback(*args, **kwargs) series_bindings.SeriesBindingDocstringCallbackSpec Represent a PEP 604 union type E.g. for int | str SeriesBindingDocstringContext(graph: 'DependencyGraph', workbook: 'Path | str', bindings: 'WorkbookSeriesBindings', series: 'dict[str, Any]', resolution: 'SeriesResolution', contract: 'SeriesBindingDocstringContract', function_kind: 'SeriesFunctionKind', function_name: 'str') -> None SeriesBindingDocstringContext(graph: 'DependencyGraph', workbook: 'Path | str', bindings: 'WorkbookSeriesBindings', series: 'dict[str, Any]', resolution: 'SeriesResolution', contract: 'SeriesBindingDocstringContract', function_kind: 'SeriesFunctionKind', function_name: 'str') SeriesBindingDocstringContract(series_id: 'str', function_name: 'str', function_kind: 'SeriesFunctionKind', data_range: 'str', layout: 'str', value_type: 'str | None', required_fields: 'tuple[str, ...]', fields: 'Mapping[str, FieldContract]', example_records: 'tuple[Mapping[str, object], ...]', notes: 'str') -> None SeriesBindingDocstringContract(series_id: 'str', function_name: 'str', function_kind: 'SeriesFunctionKind', data_range: 'str', layout: 'str', value_type: 'str | None', required_fields: 'tuple[str, ...]', fields: 'Mapping[str, FieldContract]', example_records: 'tuple[Mapping[str, object], ...]', notes: 'str') SeriesFunctionDoc(summary: 'str', purpose: 'str', record_matching: 'str', field_descriptions: 'Mapping[str, FieldDoc]' = ) -> None SeriesFunctionDoc(summary: 'str', purpose: 'str', record_matching: 'str', field_descriptions: 'Mapping[str, FieldDoc]' = ) LeafResolution ResolutionIssue Alias for resolution-time validation issues. ResolutionReport SeriesResolution ValidationIssue Literal(*args, **kwargs) ValidationReport WorkbookSeriesBindings Top-level workbook binding manifest (post-merge, post-schema validation). series_bindings.IMPLEMENTED_BIND_KINDS Build an immutable unordered collection of unique elements. series_bindings.IMPLEMENTED_LAYOUTS Build an immutable unordered collection of unique elements. series_bindings.PLANNED_BIND_KINDS Build an immutable unordered collection of unique elements. series_bindings.PLANNED_LAYOUTS Build an immutable unordered collection of unique elements. series_bindings.SUPPORTED_SCHEMA_VERSIONS Build an immutable unordered collection of unique elements. bindings_canonical_sha256(bindings: 'Mapping[str, Any]') -> 'str' SHA-256 hex digest of the canonical binding manifest. derive_input_series(graph: 'DependencyGraph', bindings: 'WorkbookSeriesBindings', *, workbook: 'Path | str') -> 'list[InputSeries]' Return one input series per binding series with graph-leaf overlap. Series bindings are the semantic source of truth: each input series corresponds to one manifest `series[]` entry, and each cell corresponds to a resolved graph leaf participating in that series. derive_output_series(graph: 'DependencyGraph', bindings: 'WorkbookSeriesBindings', *, workbook: 'Path | str') -> 'list[OutputSeries]' Return one output series per binding entry with output.compute and graph overlap. emit_compute_function(series: 'dict[str, Any]', resolved: 'SeriesResolution', *, graph: 'DependencyGraph | None' = None, workbook: 'Path | str | None' = None, bindings: 'WorkbookSeriesBindings | None' = None, series_docstring_callback: 'SeriesBindingDocstringCallbackSpec | None' = None, docstring_renderer: 'SeriesDocstringRendererSpec' = 'google', include_datetime_import: 'bool' = True) -> 'list[str]' Emit Python source lines for one series binding output compute function. emit_computes_block(graph: 'DependencyGraph', workbook: 'Path | str', bindings: 'WorkbookSeriesBindings', *, export_addresses: 'Iterable[str] | None' = None, include_type_aliases: 'bool' = True, series_docstring_callback: 'SeriesBindingDocstringCallbackSpec | None' = None, docstring_renderer: 'SeriesDocstringRendererSpec' = 'google') -> 'list[str]' Emit all series output compute functions for a validated binding manifest. emit_series_bindings_block(graph: 'DependencyGraph', workbook: 'Path | str', bindings: 'WorkbookSeriesBindings', *, export_addresses: 'Iterable[str] | None' = None, series_docstring_callback: 'SeriesBindingDocstringCallbackSpec | None' = None, docstring_renderer: 'SeriesDocstringRendererSpec' = 'google') -> 'list[str]' Emit setter and/or output compute functions for a binding manifest. emit_setter_function(series: 'dict[str, Any]', resolved: 'SeriesResolution', *, graph: 'DependencyGraph | None' = None, workbook: 'Path | str | None' = None, bindings: 'WorkbookSeriesBindings | None' = None, series_docstring_callback: 'SeriesBindingDocstringCallbackSpec | None' = None, docstring_renderer: 'SeriesDocstringRendererSpec' = 'google', include_datetime_import: 'bool' = True) -> 'list[str]' Emit Python source lines for one series binding setter. emit_setter_helpers() -> 'list[str]' Emit shared private helpers used by generated `set_*` functions. emit_setters_block(graph: 'DependencyGraph', workbook: 'Path | str', bindings: 'WorkbookSeriesBindings', *, export_addresses: 'Iterable[str] | None' = None, include_type_aliases: 'bool' = True, series_docstring_callback: 'SeriesBindingDocstringCallbackSpec | None' = None, docstring_renderer: 'SeriesDocstringRendererSpec' = 'google') -> 'list[str]' Emit all series setter functions for a validated binding manifest. generate_computes_module(graph: 'DependencyGraph', workbook: 'Path | str', bindings: 'WorkbookSeriesBindings') -> 'str' Generate a standalone module fragment with output compute functions. generate_setters_module(graph: 'DependencyGraph', workbook: 'Path | str', bindings: 'WorkbookSeriesBindings') -> 'str' Generate a standalone module fragment with setters (requires EvalContext imports). has_input_direction(series: 'dict[str, Any]') -> 'bool' has_output_direction(series: 'dict[str, Any]') -> 'bool' merge_series_entries(existing: 'dict[str, Any]', incoming: 'dict[str, Any]', *, shard_index: 'int') -> 'dict[str, Any]' Merge two series entries with the same id from different shards. normalize_bindings_document(document: 'dict[str, Any]') -> 'dict[str, Any]' Normalize all series entries in a binding manifest. normalize_series_entry(series: 'dict[str, Any]') -> 'dict[str, Any]' Return a copy with legacy aliases normalized for schema validation and codegen. expand_data_range(data_range: 'str', *, workbook: 'Path | str | None' = None, sheetnames: 'Sequence[str] | None' = None, named_ranges: 'Mapping[str, tuple[str, str]] | None' = None, named_range_ranges: 'Mapping[str, tuple[str, str, str]] | None' = None, max_range_cells: 'int' = 5000) -> 'list[str]' Expand a binding `data_range` to canonical sheet-qualified cell addresses. Uses the same target expansion as `create_dependency_graph` (including both-end sheet-qualified ranges like `Sheet1!A1:Sheet1!B2` and defined names). expand_data_range_for_graph(graph: 'DependencyGraph', data_range: 'str', *, workbook: 'Path | str | None' = None, max_range_cells: 'int' = 5000) -> 'list[str]' Expand `data_range` using named-range maps (and optional workbook) from a graph. format_schema_errors(document: 'dict[str, Any]') -> 'list[str]' Return human-readable schema error strings (for tests and CLI). list_series_docstring_callbacks() -> 'tuple[str, ...]' load_series_bindings(path: 'Path | str', *, validate: 'bool' = True) -> 'WorkbookSeriesBindings' Load a binding sidecar file or directory of shards. When `path` is a directory, all `*.bindings.yaml` / `*.bindings.json` files are merged in sorted filename order before schema validation. merge_series_binding_documents(documents: 'list[dict[str, Any]]') -> 'dict[str, Any]' Merge partial manifests (e.g. one per sheet) into one workbook document. parse_bindings_file(path: 'Path | str') -> 'dict[str, Any]' Parse one binding sidecar file (YAML or JSON) without schema validation. register_series_docstring_callback(name: 'str', callback: 'SeriesBindingDocstringCallback', *, replace: 'bool' = False) -> 'None' resolve_series_docstring_callback(callback: 'SeriesBindingDocstringCallbackSpec') -> 'SeriesBindingDocstringCallback' Look up a registered callback name or return a direct callback object. unregister_series_docstring_callback(name: 'str') -> 'None' Remove a registered callback (for tests and notebook cleanup). resolve_series_binding(graph: 'DependencyGraph', workbook: 'Path | str', series: 'dict[str, Any]', *, direction: 'BindingDirection' = 'input', export_addresses: 'Iterable[str] | None' = None, concept_scheme: 'dict[str, Any] | None' = None) -> 'SeriesResolution' Resolve each participating cell in a series binding to coordinates and record fields. resolve_series_bindings(graph: 'DependencyGraph', bindings: 'WorkbookSeriesBindings', *, workbook: 'Path | str | None' = None, direction: 'BindingDirection' = 'input', export_addresses: 'Iterable[str] | None' = None) -> 'ResolutionReport' Resolve all series in a binding manifest for the given direction. resolve_series_docstring_renderer(renderer: 'SeriesDocstringRendererSpec') -> 'SeriesDocstringRenderer' validate_bindings_document(document: 'dict[str, Any]') -> 'WorkbookSeriesBindings' Validate `document` against the series binding JSON Schema. Returns the document unchanged on success (typed for callers). Raises `SeriesBindingsSchemaError` on failure. validate_series_bindings(graph: 'DependencyGraph', bindings: 'WorkbookSeriesBindings', *, workbook: 'Path | str | None' = None) -> 'ValidationReport' Validate binding manifests against an extracted dependency graph. is_bind_implemented(kind: 'str | None') -> 'bool' is_layout_implemented(layout: 'str | None') -> 'bool' ## Classes Main classes provided by the package CodeGenerator(graph: 'DependencyGraph | GraphLike', *, iterate_enabled: 'bool | None' = None, iterate_count: 'int' = 100, iterate_delta: 'float' = 0.001) -> 'None' Generates Python code from Excel formulas. ParseError(formula: 'str', message: 'str') -> 'None' Raised when a formula cannot be parsed into an AST. ## CodeGenerator Methods Methods for the CodeGenerator class __enter__(self) -> 'CodeGenerator' __exit__(self, *args: 'object') -> 'None' derive_input_series(self, bindings: 'WorkbookSeriesBindings', *, workbook: 'Path | str') -> 'list[InputSeries]' Derive input-series metadata from explicit series bindings. classify_leaf_nodes(self, targets: 'list[str]', *, constant_types: 'set[str] | None' = None, constant_ranges: 'Sequence[str] | None' = None, constant_blanks: 'bool' = False, input_ranges: 'Sequence[str] | None' = None, attach_to_graph: 'bool' = False) -> 'tuple[set[str], set[str]]' generate(self, targets: 'Sequence[str] | None' = None, *, constant_types: 'set[str] | None' = None, constant_ranges: 'Sequence[str] | None' = None, constant_blanks: 'bool' = False, input_ranges: 'Sequence[str] | None' = None, blank_ranges: 'Sequence[str] | None' = None, series_bindings: 'WorkbookSeriesBindings | None' = None, bindings_workbook: 'Path | str | None' = None, series_docstring_callback: 'SeriesBindingDocstringCallbackSpec | None' = None, docstring_renderer: 'SeriesDocstringRendererSpec' = 'google') -> 'str' Generate standalone Python code for target cells. Args: targets: List of target cell addresses to compute. constant_types: Cell value kinds emitted as typed constants in generated code. constant_ranges: Sheet-qualified ranges whose cells are emitted as constants. constant_blanks: When True, blank cells in constant ranges become `None`. input_ranges: Sheet-qualified ranges whose leaf cells are treated as inputs. When a cell would otherwise be a constant, `input_ranges` take precedence. blank_ranges: Sheet-qualified rectangles whose cells are omitted from the graph but resolve as empty (`None`) at runtime; must match builder/evaluator. series_bindings: Optional workbook binding manifest; when set with `bindings_workbook`, emits `set_*` functions that accept Records. bindings_workbook: Path to the `.xlsx` file used to resolve binds. series_docstring_callback: Optional registered callback name for structured docstrings on generated `set_*` and series output `compute_*` functions. docstring_renderer: Built-in renderer name or custom renderer object/callable for structured series-binding docstrings (`plain`, `rst`, `google`, `numpy`). Returns: Standalone Python source code as a string. generate_modules(self, targets: 'Sequence[str] | None' = None, *, constant_types: 'set[str] | None' = None, constant_ranges: 'Sequence[str] | None' = None, constant_blanks: 'bool' = False, input_ranges: 'Sequence[str] | None' = None, blank_ranges: 'Sequence[str] | None' = None, series_bindings: 'WorkbookSeriesBindings | None' = None, bindings_workbook: 'Path | str | None' = None, series_docstring_callback: 'SeriesBindingDocstringCallbackSpec | None' = None, docstring_renderer: 'SeriesDocstringRendererSpec' = 'google') -> 'dict[str, str]' Generate a multi-module Python package for target cells. Returns a mapping of module filenames to file contents. Callers choose how to lay out files on disk (e.g. a package directory). The generated package has five flat files: - __init__.py: exports compute_all and DEFAULT_INPUTS - api.py: compute_all, make_context, and series-binding set_* / compute_* functions - data.py: DEFAULT_INPUTS and CONSTANTS - runtime.py: embedded Excel runtime (emit_runtime) - internals.py: formula cell functions + resolver dispatch ## Dataclasses Dataclass definitions And(operands: 'tuple[GuardExpr, ...]') -> None Logical AND. Compare(left: 'GuardExpr', op: 'str', right: 'GuardExpr') -> None Comparison: left op right. CycleReport(has_must_cycles: 'bool', has_may_cycles: 'bool', must_cycles: 'list[set[NodeKey]]', may_cycles: 'list[set[NodeKey]]', example_must_cycle_path: 'list[NodeKey] | None' = None, example_may_cycle_path: 'list[NodeKey] | None' = None) -> None Result of cycle analysis. DependencyGraph(_nodes: 'dict[NodeKey, Node]' = , _edges: 'dict[NodeKey, set[NodeKey]]' = , _reverse_edges: 'dict[NodeKey, set[NodeKey]]' = , _guards: 'dict[EdgeKey, GuardExpr]' = , _edge_extra: 'dict[EdgeKey, dict[str, Any]]' = , _hooks: 'list[NodeHook]' = , leaf_classification: 'dict[str, str] | None' = None, sheet_order: 'list[str] | None' = None, sheet_bounds: 'dict[str, tuple[int, int]] | None' = None, named_ranges: 'dict[str, tuple[str, str]] | None' = None, named_range_ranges: 'dict[str, tuple[str, str, str]] | None' = None) -> None DependencyGraph(_nodes: 'dict[NodeKey, Node]' = , _edges: 'dict[NodeKey, set[NodeKey]]' = , _reverse_edges: 'dict[NodeKey, set[NodeKey]]' = , _guards: 'dict[EdgeKey, GuardExpr]' = , _edge_extra: 'dict[EdgeKey, dict[str, Any]]' = , _hooks: 'list[NodeHook]' = , leaf_classification: 'dict[str, str] | None' = None, sheet_order: 'list[str] | None' = None, sheet_bounds: 'dict[str, tuple[int, int]] | None' = None, named_ranges: 'dict[str, tuple[str, str]] | None' = None, named_range_ranges: 'dict[str, tuple[str, str, str]] | None' = None) DynamicRefConfig(cell_type_env: 'CellTypeEnv', limits: 'DynamicRefLimits') -> None Configuration for resolving OFFSET/INDIRECT via constraint-based inference. Prefer building via `from_constraints`; the constructor is for internal use. DynamicRefLimits(max_branches: 'int' = 1024, max_cells: 'int' = 10000, max_depth: 'int' = 10) -> None Tuneable safety limits for dynamic-reference inference. Pass a custom instance via the `limits` parameter of `DynamicRefConfig.from_constraints` or `DynamicRefConfig.from_constraints_and_workbook` to override any of these defaults. Attributes: max_branches: Maximum number of discrete value assignments explored during constraint enumeration. This cap is applied in two places: * **Per-dependency domain size** - a single cell constrained to an integer interval wider than *max_branches* values cannot be enumerated; the caller must either tighten the constraint or rely on the symbolic (abstract) analysis path. * **Cartesian-product size** - when a formula cell falls back to brute-force evaluation over all combinations of its dependencies' domains, the product of those domain sizes must not exceed *max_branches*. If it does, a `DynamicRefError` is raised immediately (rather than hanging) with a breakdown of which dependencies contributed to the explosion. Raise this limit or tighten the offending constraints to resolve the error. Default: `1024`. max_cells: Maximum number of cells collected when expanding a range reference. Default: `10_000`. max_depth: Maximum AST-evaluation recursion depth. Default: `10`. DynamicRefTraceEvent(kind: 'str', name: 'str', elapsed_s: 'float' = 0.0, detail: 'dict[str, Any]' = ) -> None A single trace event emitted during dynamic-ref inference. Attributes: kind: Event type (`"infer"`, `"expand-env"`, `"build-domains"`, `"build-value-domains"`, `"offset-scalar-fallback"`, `"offset-scalar-wide"`, plus `"-error"` variants). name: Function that emitted the event. elapsed_s: Wall-clock seconds spent in the traced operation. Defaults to `0.0` for structural events (e.g. `"index-abstract"`, `"index-enumerated"`) that report *what* happened rather than how long it took. detail: Flexible per-event payload (target counts, branch estimates, expressions, etc.). ExcelRange(sheet: 'str', start_row: 'int', start_col: 'int', end_row: 'int', end_col: 'int') -> None ExcelRange(sheet: 'str', start_row: 'int', start_col: 'int', end_row: 'int', end_col: 'int') FormulaEvaluator(graph: 'DependencyGraph', auto_detect_changes: 'bool' = True, eager_invalidation: 'bool' = True, on_cell_evaluated: 'Callable[[str, CellValue], None] | None' = None, iterate_enabled: 'bool' = False, iterate_count: 'int' = 100, iterate_delta: 'float' = 0.001, blank_ranges: 'tuple[str, ...] | None' = None) -> None FormulaEvaluator(graph: 'DependencyGraph', auto_detect_changes: 'bool' = True, eager_invalidation: 'bool' = True, on_cell_evaluated: 'Callable[[str, CellValue], None] | None' = None, iterate_enabled: 'bool' = False, iterate_count: 'int' = 100, iterate_delta: 'float' = 0.001, blank_ranges: 'tuple[str, ...] | None' = None) FromWorkbook() -> None Metadata marker: resolve domain from the current cached workbook value. Use `Annotated[T, FromWorkbook()]` in the constraints schema to derive a singleton domain at config-build time instead of hardcoding a `Literal`. This eliminates maintenance when the workbook template changes, at the cost of a slower `DynamicRefConfig.from_constraints_and_workbook` call (the workbook must be opened and each marked cell read via a streaming parser). GreaterThanCell(other: 'str') -> None Metadata marker: the annotated cell is always greater than another cell. CellRef(key: 'NodeKey') -> None A cell reference used in a condition. GuardExpr() -> None Base type for conditional dependency guards. LightweightVizPayload(version: 'int', core: 'LightweightVizCore', overlays: 'tuple[LightweightVizOverlay, ...]', annotations: 'Mapping[str, Any] | None' = None, viewer_hints: 'Mapping[str, Any] | None' = None) -> None LightweightVizPayload(version: 'int', core: 'LightweightVizCore', overlays: 'tuple[LightweightVizOverlay, ...]', annotations: 'Mapping[str, Any] | None' = None, viewer_hints: 'Mapping[str, Any] | None' = None) Literal(value: 'Any') -> None A literal value in a condition. LocalForceSubgraph(node_ids: 'tuple[int, ...]', edges_from: 'tuple[int, ...]', edges_to: 'tuple[int, ...]', edges_guarded: 'tuple[bool, ...]', is_module_scope: 'bool', truncated: 'bool') -> None LocalForceSubgraph(node_ids: 'tuple[int, ...]', edges_from: 'tuple[int, ...]', edges_to: 'tuple[int, ...]', edges_guarded: 'tuple[bool, ...]', is_module_scope: 'bool', truncated: 'bool') Node(sheet: 'str', column: 'str', row: 'int', formula: 'str | None', normalized_formula: 'str | None', value: 'Any', is_leaf: 'bool', is_target: 'bool' = False, metadata: 'dict[str, Any]' = ) -> None Workbook cell in a dependency graph. `is_leaf` is true when the node has no outgoing dependency edges (value-only cells and literal-only formulas such as `=1+1`). Not(operand: 'GuardExpr') -> None Logical negation. NotEqualCell(other: 'str') -> None Metadata marker: the annotated cell is never equal to another cell. Or(operands: 'tuple[GuardExpr, ...]') -> None Logical OR. RealBetween(min: 'float | int | None' = None, max: 'float | int | None' = None) -> None Real-valued interval constraint for Annotated float types (not enumerable for dynamic refs). RealIntervalDomain(min: 'float | None' = None, max: 'float | None' = None) -> None Closed real-valued interval metadata; not enumerable for dynamic-ref branching. WorkbookCalcSettings(iterate_enabled: 'bool', iterate_count: 'int', iterate_delta: 'float') -> None WorkbookCalcSettings(iterate_enabled: 'bool', iterate_count: 'int', iterate_delta: 'float') ## DependencyGraph Methods Methods for the DependencyGraph class add_node(self, node: 'Node') -> 'None' __contains__(self, key: 'NodeKey') -> 'bool' __iter__(self) -> 'Iterator[NodeKey]' __len__(self) -> 'int' keys(self, *, order: "Literal['insertion', 'lexical', 'workbook']" = 'insertion', source: 'Iterable[NodeKey] | None' = None) -> 'list[NodeKey]' Return node keys from `source` (or the graph) using the selected order. add_edge(self, from_key: 'NodeKey', to_key: 'NodeKey', *, guard: 'GuardExpr | None' = None, **attrs: 'Any') -> 'None' Add edge: from_key depends on to_key (from_key -> to_key). get_node(self, key: 'NodeKey') -> 'NodeView | None' Return an immutable `NodeView` snapshot, or `None` if missing. get_dependencies(self, key: 'NodeKey') -> 'frozenset[NodeKey]' Return an immutable snapshot of `key`'s dependencies (cells it reads). get_dependents(self, key: 'NodeKey') -> 'frozenset[NodeKey]' Return an immutable snapshot of cells that depend on `key`. get_edge_attrs(self, from_key: 'NodeKey', to_key: 'NodeKey') -> 'EdgeAttrs' Return a typed snapshot of the attributes on edge `from_key -> to_key`. When the edge does not exist, returns an `EdgeAttrs` with all fields set to `None`. get_edge_guard(self, from_key: 'NodeKey', to_key: 'NodeKey') -> 'GuardExpr | None' Return the guard on edge `from_key -> to_key`, or `None` if none. set_node_value(self, key: 'NodeKey', value: 'Any') -> 'None' Set a node's `value` field durably. Raises `KeyError` if missing. set_node_metadata(self, key: 'NodeKey', metadata: 'Mapping[str, Any]') -> 'None' Replace a node's metadata mapping durably. The provided mapping is copied; subsequent mutations to the caller's object do not affect graph state. Raises `KeyError` if the node is missing. register_hook(self, hook: 'NodeHook') -> 'None' leaves(self) -> 'Iterator[NodeKey]' Iterate over keys of leaf nodes (no dependencies). formula_nodes(self) -> 'Iterator[tuple[NodeKey, Node]]' Iterate over (key, node) pairs for nodes that contain formulas. leaf_node_items(self) -> 'Iterator[tuple[NodeKey, Node]]' Iterate over (key, node) pairs for leaf nodes (no cell dependencies). formula_keys(self) -> 'list[NodeKey]' Return sorted list of keys for nodes that contain formulas. leaf_keys(self) -> 'list[NodeKey]' Return sorted list of keys for nodes with no dependency edges (leaves). target_keys(self) -> 'list[NodeKey]' Return sorted list of keys marked as original build targets. roots(self) -> 'Iterator[NodeKey]' cycle_report(self) -> 'CycleReport' evaluation_order(self, *, strict: 'bool' = True, iterate_enabled: 'bool | None' = None) -> 'list[NodeKey]' Return nodes in dependency-first order (leaves before formulas that use them). Edge direction is A -> B meaning A depends on B. This method returns an ordering suitable for sequential evaluation (dependencies first). If `iterate_enabled` is True (workbook has iterative calculation on), any must-cycle or may-cycle is rejected: generated Python does not emulate Excel's iterative convergence. Pass `False` or `None` to apply the usual strict / non-strict rules without this check. compress_identity_transits(self, *, record: 'IdentityTransitCompressionRecord | None' = None) -> 'list[NodeKey]' Remove identity transit nodes and rewire dependents. Transit nodes whose formula is a single cell reference to one dependency are removed, dependents' formulas are rewritten, and edges are rewired. Requires dependency provenance from graph construction with `capture_dependency_provenance=True` for safe edges. Node hooks are not invoked for removed or updated nodes. Args: record: When provided, populate with removal lineage for projection manifests. Returns: Keys of removed transit nodes, in removal order. __getstate__(self) -> 'dict[str, Any]' __setstate__(self, state: 'dict[str, Any]') -> 'None' ## Enumerations Enumeration types CacheValidationPolicy(*values) XlError(*values) ## Exceptions Exception classes CycleError(message: 'str', cycle_path: 'list[NodeKey]', is_must_cycle: 'bool') Raised when a cycle prevents computing evaluation order. DynamicRefError Raised when dynamic reference analysis cannot proceed. When building a dependency graph, pass a `DynamicRefConfig` (e.g. via `DynamicRefConfig.from_constraints`) or set `use_cached_dynamic_refs=True` to resolve OFFSET/INDIRECT instead of raising. SeriesBindingsLoadError Raised when binding files cannot be parsed or merged. SeriesBindingsSchemaError Raised when a binding manifest fails JSON Schema validation. ## Named Tuples Named tuple definitions ValidationResult(is_valid: ForwardRef('bool'), in_graph_not_in_chain: ForwardRef('set[str]'), in_chain_not_in_graph: ForwardRef('set[str]'), messages: ForwardRef('list[str]')) ValidationResult(is_valid, in_graph_not_in_chain, in_chain_not_in_graph, messages) ## Typed Dicts TypedDict definitions ValidationReport ## Functions Utility functions bindings_canonical_sha256(bindings: 'Mapping[str, Any]') -> 'str' SHA-256 hex digest of the canonical binding manifest. build_graph_cache_meta(workbook_path: 'Path', targets: 'list[str]', *, extraction_params: 'dict[str, Any] | None' = None, schema_version: 'int' = 1) -> 'GraphCacheMeta' build_graph_cache_meta_portable(targets: 'list[str]', *, extraction_params: 'dict[str, Any] | None' = None, schema_version: 'int' = 1, excel_grapher_version: 'str | None' = None) -> 'GraphCacheMeta' Build expected cache metadata without access to the workbook file. Portable validation enforces only: - schema_version - excel_grapher_version - targets_sha256 - extraction_params create_dependency_graph(workbook: 'Path | str', targets: 'Iterable[str]', *, max_depth: 'int' = 50, expand_ranges: 'bool' = True, max_range_cells: 'int' = 5000, hooks: 'list[NodeHook] | None' = None, load_values: 'bool' = True, dynamic_refs: 'DynamicRefConfig | None' = None, use_cached_dynamic_refs: 'bool' = False, capture_dependency_provenance: 'bool' = False, blank_ranges: 'Iterable[str] | None' = None, type_analysis_cache: 'TypeAnalysisCache | None' = None) -> 'DependencyGraph' Build a dependency graph starting from target cells. `targets` accepts any mix of: - sheet-qualified single cells (`"Sheet1!A1"`, `"'My Sheet'!B2"`); - sheet-qualified rectangular ranges (`"Sheet1!B12:F12"`, `"Sheet1!A1:Sheet1!B2"`, `"'My Sheet'!A1:B2"`); and - defined names that resolve to a single cell or a rectangular range (`"MyInput"`, `"DataRange"`). Range and named-range targets are expanded to one BFS root per cell (subject to `max_range_cells`); the deduplicated union seeds traversal. Targets that are neither sheet-qualified nor a known defined name raise `ValueError`. Supports basic A1 references, sheet-qualified references, and dynamic references (OFFSET/INDIRECT). For OFFSET/INDIRECT: - **use_cached_dynamic_refs=True**: Resolve using cached workbook values (existing path). `dynamic_refs` is ignored. - **use_cached_dynamic_refs=False** (default), **dynamic_refs=None**: On any formula that contains OFFSET or INDIRECT requiring resolution, raise `DynamicRefError`. Callers can pass a `DynamicRefConfig` or set `use_cached_dynamic_refs=True` to avoid. - **use_cached_dynamic_refs=False**, **dynamic_refs** set: Resolve OFFSET/INDIRECT via the config's `cell_type_env` and `limits`; missing or invalid domains raise `DynamicRefError`. To build a config from a `dict[str, type]` constraints schema, use `DynamicRefConfig.from_constraints`. When `capture_dependency_provenance` is True, each edge stores merged `excel_grapher.grapher.dependency_provenance.EdgeProvenance` under the `\"provenance\"` key in `DependencyGraph.edge_attrs` (how the dependency arises: direct reference, static range, dynamic OFFSET/INDIRECT). `blank_ranges` is an optional iterable of sheet-qualified A1 rectangles (e.g. `\"Sheet1!B2:D10\"`) treated as structurally empty: no nodes are created for those cells (edges into them are kept), and dynamic-ref leaf constraints are not required for addresses inside these ranges. Pair with the same declarations on `excel_grapher.FormulaEvaluator` and `excel_grapher.exporter.codegen.CodeGenerator.generate` for **evaluator <-> export** parity (consistent behavior between evaluation and generated code). **Cost model**: constraint-based dynamic-ref expansion (`dynamic_refs` set, `use_cached_dynamic_refs=False`) runs `expand_leaf_env_to_argument_env` once per formula regardless of `capture_dependency_provenance`. A shared per-graph cache ensures provenance collection reuses the already-computed expansion instead of repeating it. Callers doing iterative constraint-tuning workflows can still set `capture_dependency_provenance=False` to avoid any provenance overhead (formula-string span collection, branch-union merging, etc.). format_cell_key(sheet: 'str', column: 'str', row: 'int') -> 'NormalizedAddress' Format a (sheet, column_letters, row) triple into a canonical address. get_calc_settings(workbook_path: 'Path') -> 'WorkbookCalcSettings' Extract calculation settings from xl/workbook.xml. Defaults follow Excel's typical defaults when attributes are missing. list_dynamic_ref_constraint_candidates(workbook: 'Path | str | fastpyxl.Workbook', targets: 'Iterable[str]', *, dynamic_refs: 'DynamicRefConfig | None' = None, max_depth: 'int' = 50, max_range_cells: 'int' = 5000, type_analysis_cache: 'TypeAnalysisCache | None' = None) -> 'list[str]' Return sorted leaf cells missing dynamic-ref constraint entries. These are leaf cell addresses that feed dynamic-ref arguments (OFFSET/INDIRECT/INDEX) but have no entry in `dynamic_refs.cell_type_env`. Unlike `create_dependency_graph`, this function does **not** raise `DynamicRefError` when constraints are missing. Instead it collects all missing leaf addresses in a single pass and returns them sorted. When `dynamic_refs` is `None` the function treats it as an empty constraint environment: all leaf cells that feed dynamic-ref arguments are returned as candidates. **Completeness caveat**: Cells reachable only through unresolvable dynamic refs will not be visited, so their constraint candidates won't appear in the output. A second call after adding the first batch of constraints will quickly find any remaining missing entries. load_series_bindings(path: 'Path | str', *, validate: 'bool' = True) -> 'WorkbookSeriesBindings' Load a binding sidecar file or directory of shards. When `path` is a directory, all `*.bindings.yaml` / `*.bindings.json` files are merged in sorted filename order before schema validation. needs_quoting(sheet: 'str') -> 'bool' Return True if a sheet name must be wrapped in single quotes in a formula. normalize_blank_range_specs(specs: 'Iterable[str] | None') -> 'tuple[BlankRangeRect, ...]' Normalize a sequence of sheet-qualified range strings. resolve_series_bindings(graph: 'DependencyGraph', bindings: 'WorkbookSeriesBindings', *, workbook: 'Path | str | None' = None, direction: 'BindingDirection' = 'input', export_addresses: 'Iterable[str] | None' = None) -> 'ResolutionReport' Resolve all series in a binding manifest for the given direction. save_graph_cache(path: 'Path', graph: 'DependencyGraph', meta: 'GraphCacheMeta') -> 'None' select_local_force_subgraph(payload: 'LightweightVizPayload', *, node_id: 'int') -> 'LocalForceSubgraph' select_path_induced_subgraph(graph: 'DependencyGraph', *, source_keys: 'Sequence[NodeKey]', target_keys: 'Sequence[NodeKey]', max_path_length: 'int | None' = None, max_paths: 'int | None' = None, include_endpoints: 'bool' = True) -> 'DependencyGraph' Return the induced subgraph over nodes lying on directed source->target paths. Edge direction follows `DependencyGraph` semantics: `A -> B` means `A` depends on `B`. to_graphviz(graph: 'DependencyGraph', *, label_fn: 'Callable[[NodeKey, Node | NodeView], str] | None' = None, highlight: 'set[NodeKey] | None' = None, rankdir: 'str' = 'TB', include_formula_on_nodes: 'bool' = True, max_formula_length: 'int | None' = 120) -> 'str' to_mermaid(graph: 'DependencyGraph', *, label_fn: 'Callable[[NodeKey, Node | NodeView], str] | None' = None, max_nodes: 'int' = 100, include_formula_on_nodes: 'bool' = True, max_formula_length: 'int | None' = 120) -> 'str' to_networkx(graph: 'DependencyGraph | GraphReadView', *, include_formula_on_nodes: 'bool' = True, max_formula_length: 'int | None' = 120) Convert a dependency graph to a NetworkX DiGraph. Accepts `DependencyGraph` or any graph-like object with node iteration, dependency lookup, and edge attributes (for example `ProjectionResult`). NetworkX is an optional dependency. If not installed, raises ImportError with a helpful message. to_web_viz_payload(nx_graph: 'Any', *, max_local_nodes: 'int | None' = None, max_local_edges: 'int | None' = None, include_guarded_edges: 'bool' = True, include_guarded_edges_for_partition: 'bool' = False, layout: 'WebVizLayoutSpec' = 'stratified_multipartite', layout_config: 'dict[str, Any] | None' = None, include_formula_on_nodes: 'bool' = True, max_formula_length: 'int | None' = 120, seed: 'int' = 0, weight_attr: 'str | None' = None, include_module_overlay: 'bool' = True) -> 'WebVizPayload' Build a web-visualization payload from a NetworkX DiGraph. Layout is selected by `layout` (registered web layout plugin id or a direct `WebVizLayoutPlugin` callable). The default `stratified_multipartite` uses SCC-condensation longest-path rank on the vertical axis and Louvain community ordering on the horizontal axis when `include_module_overlay` is true. Other built-in ids include `spring`, `forceatlas2`, `multipartite` (NetworkX `multipartite_layout`), `graphviz_dot`, and `graphviz_sfdp`. Set `include_module_overlay=False` to skip the partition overlay (single module color; overview still draws local graph edges in the viewer). trace_dynamic_refs(callback: 'DynamicRefTraceFn') -> 'Iterator[None]' Activate *callback* as the dynamic-ref tracer for the enclosed block. Nestable: an inner `trace_dynamic_refs` overrides the outer one; the outer tracer is restored when the inner block exits. try_load_graph_cache(path: 'Path', *, expected_meta: 'GraphCacheMeta', policy: 'CacheValidationPolicy' = ) -> 'DependencyGraph | None' validate_graph(graph: 'DependencyGraph', workbook_path: 'Path', *, scope: 'set[str] | None' = None) -> 'ValidationResult' validate_series_bindings(graph: 'DependencyGraph', bindings: 'WorkbookSeriesBindings', *, workbook: 'Path | str | None' = None) -> 'ValidationReport' Validate binding manifests against an extracted dependency graph. write_lightweight_viz_data(payload: 'LightweightVizPayload', path: 'Path | str') -> 'None' write_lightweight_viz_html(payload: 'LightweightVizPayload', path: 'Path | str', *, title: 'str' = 'Workbook dependency graph', data_mode: "Literal['inline', 'sidecar', 'auto']" = 'auto', data_path: 'Path | str | None' = None, inline_size_budget_mb: 'int' = 50, template_path: 'Path | str | None' = None) -> 'None' write_web_viz_html(payload: 'LightweightVizPayload', path: 'Path | str', *, title: 'str' = 'Workbook dependency graph', data_mode: "Literal['inline', 'sidecar', 'auto']" = 'auto', data_path: 'Path | str | None' = None, inline_size_budget_mb: 'int' = 50, template_path: 'Path | str | None' = None) -> 'None' Write a web visualization HTML bundle from a web-viz payload. ## Constants Module-level constants and data format_key(sheet: 'str', cell: 'str') -> 'NormalizedAddress' Format a sheet and A1 cell coordinate into a canonical address string. ---------------------------------------------------------------------- This is the User Guide documentation for the package. ---------------------------------------------------------------------- ## Core workflow ### Key design decisions - **Node identity**: nodes are keyed by sheet-qualified A1 strings like `Sheet1!A1` (`NodeKey`). - **Edge direction**: an edge `A -> B` means **A depends on B** (dependency-first evaluation). - **Leaf definition**: a leaf is any node with no cell dependencies (`Node.is_leaf=True`), including non-formula cells and literal-only formulas (e.g. `=1+1`). - **Values are optional**: `load_values=True` loads cached Excel results (second workbook load); otherwise formula nodes have `value=None`. - **Extensible metadata**: each `Node` has a `metadata: dict[str, Any]` that hooks can mutate. - **Range expansion**: ranges like `A1:A10` are expanded to individual cell dependencies (bounded by `max_range_cells`). - **Whole-column/row shorthand**: Excel forms like `Data!A:A` and `Data!5:5` parse as syntactic shorthands and resolve to each sheet's **used range** (workbook dimensions), not Excel's full grid. These shorthands expand to every row/column in that extent even when `max_range_cells` would otherwise keep only rectangular corner endpoints. - **Normalized formulas**: each formula node has a `normalized_formula` field with sheet-qualified refs, resolved named ranges, and stripped `$` markers — ready for transpilation. ### Quick start: building a graph ```python from pathlib import Path from excel_grapher.grapher import create_dependency_graph from excel_grapher.grapher import to_graphviz, to_mermaid, to_networkx # optional wb_path = Path("model.xlsx") targets = ["Sheet1!A10"] g = create_dependency_graph(wb_path, targets, load_values=False) print(len(g)) # number of visited nodes print(to_graphviz(g)) # GraphViz DOT ``` ### Target forms `targets` accepts any mix of: - sheet-qualified single cells: `"Sheet1!A1"`, `"'My Sheet'!B2"` - sheet-qualified ranges: `"Sheet1!B12:F12"`, `"Sheet1!A1:Sheet1!B2"`, `"'My Sheet'!A1:B2"` - defined names that resolve to a single cell or rectangular range: `"MyInput"`, `"DataRange"` Range and named-range targets expand to one root per cell (subject to `max_range_cells`) and the BFS proceeds from the deduplicated union of roots. Targets that are neither sheet-qualified nor a known defined name raise `ValueError`. ### Dynamic OFFSET/INDIRECT configuration Dynamic references (e.g. `OFFSET`, `INDIRECT`) can be handled in three ways via the `create_dependency_graph` API: ```python from excel_grapher.grapher import create_dependency_graph, DynamicRefConfig, DynamicRefLimits ## Signature (simplified): ## create_dependency_graph( ## workbook_path, ## targets, ## *, ## dynamic_refs: DynamicRefConfig | None = None, ## use_cached_dynamic_refs: bool = False, ## ... ## ) ``` - **`use_cached_dynamic_refs=True`** Use the existing cached-workbook path for `OFFSET`/`INDIRECT`. This preserves the legacy behavior and **ignores** `dynamic_refs`. - **`use_cached_dynamic_refs=False` (default) and `dynamic_refs is None`** When the builder encounters dynamic refs that require resolution, it raises **`DynamicRefError`**. This is the safe “no silent fallback” default. - **`use_cached_dynamic_refs=False` and `dynamic_refs is a DynamicRefConfig`** Resolve dynamic refs using static constraints (cell types and limits). Missing or invalid domains still raise `DynamicRefError`. You typically build a `DynamicRefConfig` from a `dict[str, type]` constraints schema (sheet-qualified addresses mapped to typing annotations): ```python from typing import Annotated, Literal from excel_grapher.grapher import DynamicRefConfig, create_dependency_graph from excel_grapher.core.cell_types import Between constraints_schema = { "Sheet1!B1": Literal["ROW_INDEX"], "Sheet1!C1": Annotated[float, Between(0, 10)], } constraints_data: dict[str, object] = {} config = DynamicRefConfig.from_constraints(constraints_schema, constraints_data) graph = create_dependency_graph( "model_with_dynamic_refs.xlsx", ["Sheet1!D10"], load_values=False, dynamic_refs=config, # use_cached_dynamic_refs=False is the default ) ``` Key points: - Constraint keys use **address-style** strings (e.g. `"Sheet1!B1"`). - `DynamicRefConfig` is immutable and carries both the `cell_type_env` and `DynamicRefLimits`. - From the top-level package, you can import `DynamicRefConfig`, `DynamicRefLimits`, and `DynamicRefError`. ### Working with cell data (for transpilation) The `DependencyGraph` provides direct O(1) access to cell data via `get_node()`, plus filter methods for iterating over formula vs. leaf cells. ```python from pathlib import Path from excel_grapher.grapher import create_dependency_graph, discover_formula_cells_in_rows from excel_grapher.grapher import DependencyGraph ## Discover formula cells in specific rows targets = discover_formula_cells_in_rows(Path("model.xlsx"), "Sheet1", [10, 11, 12]) ## Build the dependency graph graph: DependencyGraph = create_dependency_graph(Path("model.xlsx"), targets, load_values=True) ## Access cells by normalized address (O(1) lookup) node = graph.get_node("Sheet1!A10") print(node.formula) # Original formula print(node.normalized_formula) # Sheet-qualified for transpilation print(node.value) # Cached value from Excel print(node.is_target) # True if this node came from original targets ## Iterate over formula cells for key, node in graph.formula_nodes(): print(key, node.normalized_formula) ## Iterate over leaf (value) cells for key, node in graph.leaf_node_items(): print(key, node.value) ## Get sorted keys formula_keys = graph.formula_keys() leaf_keys = graph.leaf_keys() target_keys = graph.target_keys() ``` #### `DependencyGraph` filter methods | Method | Returns | Description | |---------------------|------------------------------------------|---------------------------------------| | `get_node(key)` | `NodeView \| None` | O(1) immutable lookup by cell address | | `formula_nodes()` | `Iterator[tuple[NodeKey, Node]]` | Cells with formulas | | `leaf_node_items()` | `Iterator[tuple[NodeKey, Node]]` | Leaf cells (no cell dependencies) | | `formula_keys()` | `list[NodeKey]` | Sorted keys for formula cells | | `leaf_keys()` | `list[NodeKey]` | Sorted keys for leaf cells | | `target_keys()` | `list[NodeKey]` | Sorted keys marked as original targets | #### `Node` fields | Field | Type | Description | |----------------------|--------------|-------------------------------------------| | `formula` | `str \| None` | Original formula (``None`` for value-only cells) | | `normalized_formula` | `str \| None` | Sheet-qualified formula for transpilation | | `value` | `Any` | Cached or hardcoded value | | `is_leaf` | `bool` | True if the node has no cell dependencies | | `is_target` | `bool` | True if the node was one of the original graph targets | | `sheet` | `str` | Sheet name | | `column` | `str` | Column letter | | `row` | `int` | Row number | #### `discover_formula_cells_in_rows()` Utility for scanning rows to find formula cells with numeric cached values: ```python def discover_formula_cells_in_rows( wb_path: Path, sheet_name: str, rows: list[int], ) -> list[str]: ... ``` Returns sheet-qualified cell addresses (e.g., `"'Sheet Name'!A1"`) for formula cells. --- ### GraphViz DOT ```python from excel_grapher.grapher import to_graphviz dot = to_graphviz(g, rankdir="LR") ``` ### Mermaid ```python from excel_grapher.grapher import to_mermaid mm = to_mermaid(g, max_nodes=100) ``` ### Path-induced subgraphs for focused visualization Use `select_path_induced_subgraph(...)` to isolate only nodes on directed dependency paths between source and target node sets, then pass the smaller graph to any exporter. ```python from excel_grapher.grapher import select_path_induced_subgraph, to_graphviz focused = select_path_induced_subgraph( g, source_keys=["Sheet1!F1"], target_keys=["Sheet1!A1"], max_path_length=10, # optional safety cutoff max_paths=1000, # optional safety cutoff ) dot = to_graphviz(focused, rankdir="LR") ``` The path search follows graph edge direction (`A -> B` means `A` depends on `B`), validates that all requested keys exist in the graph, and preserves edge guards/provenance in the returned induced subgraph. ### NetworkX (optional dependency) ```python from excel_grapher.grapher import to_networkx G = to_networkx(g) ``` ### Formula text on nodes For `to_graphviz`, `to_mermaid`, and `to_networkx`, formula cells get a **second line** in the node label (cell address, then the formula). This is on by default (`include_formula_on_nodes=True`). Set `include_formula_on_nodes=False` to use only the cell address. Long formulas are truncated for display: `max_formula_length` defaults to `120` characters; use `None` for no limit. Truncated text ends with `...`. ### Large graphs and module inference: NetworkX visualization For graphs that are too large for Graphviz or Mermaid, the current recommended workflow is: - `DependencyGraph` -> `to_networkx(...)` -> `to_web_viz_payload(...)` - `write_web_viz_html(...)` for rendering This builds a NetworkX graph, converts it to a visualization payload, generates a static HTML viewer that wraps the payload, and opens the generated HTML in a browser. The payload stores node coordinates, module/rank metadata, and edge columns; the browser viewer consumes those directly. ```python from pathlib import Path from excel_grapher.exporter import to_web_viz_payload from excel_grapher.grapher import create_dependency_graph, to_networkx, write_web_viz_html g = create_dependency_graph("model.xlsx", ["Sheet1!A1"], load_values=False) payload = to_web_viz_payload(to_networkx(g)) write_web_viz_html(payload, Path("model.html"), data_mode="auto") ``` ![Lightweight visualization overview](_files/lightweight_viewer.png) #### Customizing NetworkX visualization The payload builder (`to_web_viz_payload`) and the viewer (`write_web_viz_html`) are powered by plugins. A single default plugin is provided for each, but you are encouraged to write your own. Configure `to_web_viz_payload(...)` via: - `layout="..."`: layout plugin id (`stratified_multipartite` default; see `list_web_viz_layouts()`). - `layout_config={...}`: plugin-specific optional config. - `include_guarded_edges`: include/exclude guarded edges in core/local edge export. - `include_guarded_edges_for_partition`: include/exclude guarded edges in module partitioning. - `include_module_overlay`: include partition overlay metadata and module color semantics. #### Default layout: `stratified_multipartite` The default layout is `stratified_multipartite`, which uses SCC-condensation longest-path rank on the vertical axis and Louvain community ordering on the horizontal axis. ```python from excel_grapher.exporter import to_web_viz_payload, list_web_viz_layouts from excel_grapher.grapher import write_web_viz_html payload = to_web_viz_payload( G, layout="stratified_multipartite", # default layout_config=None, # plugin-specific options include_module_overlay=True, # include Louvain partition overlay ) write_web_viz_html(payload, "model-web.html", data_mode="auto") ``` This layout is partly intended to inform refactoring and modularization of generated code. Interpret the viewer with this mental model: - **Position**: `x`/`y` - SCC-rank vertical strata and Louvain-based horizontal ordering. - **Color**: node color maps to `module_id` (partition/community id). With no module overlay, all nodes are one module color. - **Edges**: overview prefers node-level `local_edges`; if unavailable, it falls back to module-centroid edges. - **Label `Module edges` / `Graph edges`**: reflects which edge set is currently drawn in overview. - **Rank** in tooltip is node rank metadata and may differ from visual layering for force-directed layouts. `excel_grapher.exporter.to_web_viz_payload(...)` includes the partition overlay by default and is the canonical payload entrypoint for this visualization workflow. Open the exported HTML directly in a browser. The interface supports panning, zooming, hover tooltips, and a local force-layout mode for inspecting a neighborhood around a selected node. ### Validation via `calcChain.xml` You can validate the graph against Excel’s `calcChain.xml`: ```python from pathlib import Path from excel_grapher.grapher import create_dependency_graph, validate_graph g = create_dependency_graph("model.xlsx", ["Sheet1!A10"], load_values=False) res = validate_graph(g, Path("model.xlsx"), scope={"Sheet1"}) print(res.is_valid, res.messages) ``` If `xl/calcChain.xml` is missing (common for generated files), validation returns `is_valid=True` with an informational message. ### Evaluating formulas The evaluator implements Excel’s semantics in Python and runs over a `DependencyGraph`. ### Conceptually - `FormulaEvaluator` is a wrapper around `DependencyGraph` that: - Translates Excel formulas to Python at runtime. - Provides Python equivalents for Excel functions, operators, and error types. - Handles circular references in a way compatible with Excel’s defaults (warn + return `0`, etc.). - Caches results to ensure each cell is computed at most once in a given evaluation. This gives **fast, accurate, repeatable** execution for any given workbook, but keeps the logic in an Excel-shaped representation. It’s the easiest path when you want to: - Re-extract and re-run a computation whenever the workbook changes. - Keep a tight coupling to Excel while still running logic in Python. ### Minimal evaluator example ```python from excel_grapher.grapher import create_dependency_graph from excel_grapher.evaluator import FormulaEvaluator targets = ["Sheet1!B10"] graph = create_dependency_graph( "model.xlsx", targets, load_values=True, max_depth=10, ) with FormulaEvaluator(graph) as ev: evaluator_results = ev.evaluate(targets) print(evaluator_results) ## {'Sheet1!B10': ...} ``` ## Export & quality ### End-to-end demo This example builds a **synthetic two-cell workbook** and runs it through the full pipeline. - `S!A1` is a leaf value (`10`). - `S!B1` is a formula (`=A1*2`) that references `S!A1`. ### Setup: create the workbook ```python from __future__ import annotations import sys from pathlib import Path import fastpyxl def _find_repo_root(start: Path) -> Path: for p in [start, *start.parents]: if (p / "pyproject.toml").exists(): return p raise RuntimeError("Could not find repo root (missing pyproject.toml)") def create_synthetic_workbook(path: Path, *, sheet_name: str = "S") -> None: path.parent.mkdir(parents=True, exist_ok=True) wb = fastpyxl.Workbook() ws = wb.active ws.title = sheet_name ws["A1"].value = 10 ws["B1"].value = "=A1*2" wb.save(path) ROOT = _find_repo_root(Path.cwd()) sys.path.insert(0, str(ROOT)) workbook_path = ROOT / "demo" / "_artifacts" / "two_cell_demo.xlsx" create_synthetic_workbook(workbook_path, sheet_name="S") ``` ### Build the `DependencyGraph` (dict representation) ```python import json from dataclasses import asdict from excel_grapher.grapher import create_dependency_graph, DependencyGraph from excel_grapher.evaluator import FormulaEvaluator from excel_grapher.exporter import CodeGenerator targets = ["S!B1"] graph: DependencyGraph = create_dependency_graph( workbook_path, targets, load_values=True, max_depth=10, ) def serialize_graph(graph: DependencyGraph) -> dict: return { "nodes": {k: asdict(v) for k, v in graph._nodes.items()}, # Adjacency list: node -> dependencies (edges point from node to its deps) "edges": {k: sorted(v) for k, v in graph._edges.items()}, } print(json.dumps(serialize_graph(graph), indent=2, sort_keys=True)) ``` Example output: ```json { "edges": { "S!A1": [], "S!B1": [ "S!A1" ] }, "nodes": { "S!A1": { "column": "A", "formula": null, "is_leaf": true, "metadata": {}, "normalized_formula": null, "row": 1, "sheet": "S", "value": 10 }, "S!B1": { "column": "B", "formula": "=A1*2", "is_leaf": false, "metadata": {}, "normalized_formula": "=S!A1*2", "row": 1, "sheet": "S", "value": null } } } ``` ### Evaluator results ```python with FormulaEvaluator(graph) as ev: evaluator_results = ev.evaluate(targets) print(evaluator_results) ## {'S!B1': 20.0} ``` ### Caching an extracted graph (optional) If graph extraction is expensive and you expect to re-use the same workbook + targets + extraction settings, you can cache the `DependencyGraph` to disk as JSON. Strict caching (requires access to the workbook file to validate fingerprints): ```python from pathlib import Path from excel_grapher import ( CacheValidationPolicy, build_graph_cache_meta, create_dependency_graph, save_graph_cache, try_load_graph_cache, ) workbook_path = Path("workbook.xlsx") targets = ["S!B1"] extraction_params = {"load_values": True, "max_depth": 50} expected = build_graph_cache_meta(workbook_path, targets, extraction_params=extraction_params) graph = try_load_graph_cache(Path("graph-cache.json"), expected_meta=expected) if graph is None: graph = create_dependency_graph(workbook_path, targets, **extraction_params) save_graph_cache(Path("graph-cache.json"), graph, expected) ``` Portable caching (for `FormulaEvaluator` on machines without the workbook file): ```python from excel_grapher import ( CacheValidationPolicy, build_graph_cache_meta_portable, try_load_graph_cache, ) targets = ["S!B1"] expected = build_graph_cache_meta_portable(targets, extraction_params={"load_values": True, "max_depth": 50}) graph = try_load_graph_cache( Path("graph-cache.json"), expected_meta=expected, policy=CacheValidationPolicy.PORTABLE, ) if graph is None: raise FileNotFoundError("No valid cached graph found for the requested targets/settings.") ``` **Tradeoffs for the evaluator approach:** - **Advantages** - **Native interface for extraction**: easy to re-extract and re-run if the workbook changes. - **Template flexibility**: users can alter workbook structure; re-extraction will follow the new formula graph. - **Disadvantages** - **Runtime translation**: Excel → Python translation happens at runtime for each evaluation. - **Coupled to Excel**: still conceptually “driven by Excel” rather than a fully normalized Python model. ### Series bindings Declarative **series bindings** describe how spreadsheet input cells map to named dimensions (SDMX-style concepts) and how generated Python **setters** and **compute** functions accept `Records` — `list[dict]` with a required measure field (`OBS_VALUE` by default) plus key columns. They replace the retired label-detection heuristics (`row_labels` / `column_labels` on graph nodes). The machine source of truth is a **sidecar manifest** next to the workbook, not inferred geometry. Validation uses the JSON Schema shipped with the package at `excel_grapher/series_bindings/series_binding.schema.json`. **Example workbook:** [series_bindings.xlsx](https://github.com/Teal-Insights/excel-grapher/blob/main/examples/micro_workbooks/series_bindings.xlsx) with [series_bindings.bindings.yaml](https://github.com/Teal-Insights/excel-grapher/blob/main/examples/micro_workbooks/series_bindings.bindings.yaml) **Hands-on walkthrough:** [Series bindings example](https://github.com/Teal-Insights/excel-grapher/blob/main/examples/micro_workbooks/series_bindings.md) ([Quarto source](https://github.com/Teal-Insights/excel-grapher/blob/main/examples/micro_workbooks/series_bindings.qmd)) ## JSON Schema The schema below is read from the installed package (same file used by `validate_bindings_document()`): ```{python} #| echo: false #| warning: false from importlib.resources import files from IPython.display import Markdown, display schema_text = ( files("excel_grapher.series_bindings") .joinpath("series_binding.schema.json") .read_text(encoding="utf-8") ) display( Markdown( "
JSON Schema\n\n" f"```json\n{schema_text}\n```\n
" ) ) ``` ## Authoring conventions | Topic | Convention | |--------|------------| | **Format** | YAML by default (`.bindings.yaml`); JSON (`.bindings.json`) also accepted | | **Location** | One sidecar per workbook, colocated with the `.xlsx` | | **Granularity (MVP)** | One `series[]` entry per logical indicator → one `set_*` function (`layout: series`) | | **Naming** | Stable `id` (snake_case); `setter.name` must match `set_[a-z][a-z0-9_]*` | | **Non-executable prose** | `sdmx_notes`, `notes` — documentation only | ### Default file layout ```text lic_inputs.xlsx lic_inputs.bindings.yaml # schema_version, workbook, series[] ``` All setters for that workbook live in a single `series[]` array. ### Optional per-sheet shards For large workbooks, split by sheet and merge at load time: ```text lic_inputs.xlsx lic_inputs.bindings/ Inputs.bindings.yaml Assumptions.bindings.yaml ``` Rules: - Same `schema_version` and `workbook` in every shard (error otherwise). - Union `series[]`; **reject duplicate `series[].id`** across shards. - `load_series_bindings(path)` accepts a **file or directory** of `*.bindings.{yaml,yml,json}`. **Non-goals:** bindings embedded inside the xlsx; one file per series; repo-wide manifest without a `workbook` field. ## Input and output direction blocks Each `series[]` entry shares structure (`data_range`, `layout`, `structure`, `key`) and declares optional direction blocks: - **`input.setter`** — generated setter requires key dimensions plus `OBS_VALUE` on each incoming record. - **`output.compute`** — generated compute returns all declared dimensions plus computed `OBS_VALUE` per graph cell in `data_range`. At least one of `input` or `output` is required per series (legacy top-level `setter` is normalized to `input.setter`; legacy `layout: row_series` is normalized to `series`). ## `data_range` addressing Expansion uses the same path as graph **targets** (`expand_targets_to_roots`), including: - Local range: `Inputs!F5:J5` - **Both-end sheet-qualified** (project standard): `Inputs!F5:Inputs!J5`, `'My Sheet'!A1:'My Sheet'!B2` - **Defined names**: `PrimaryRow` (requires `workbook=` or a graph built from that workbook so named-range maps are available) ```python from excel_grapher.series_bindings import expand_data_range, expand_data_range_for_graph expand_data_range("Inputs!F5:Inputs!J5") expand_data_range("PrimaryRow", workbook=path_to_xlsx) expand_data_range_for_graph(graph, "PrimaryRow", workbook=path_to_xlsx) ``` ## LIC manifest table → sidecar workflow Analysts often maintain a **manifest table** (Excel or markdown) listing inputs: country, indicator, units, sheet/range, and notes. Treat that table as human QA; the **sidecar YAML** is what tooling validates and codegen consumes. Suggested workflow: 1. **Inventory** inputs in the manifest (one row per logical series / indicator row). 2. **Draft** `series_bindings.bindings.yaml` — one `series[]` item per manifest row that should become a setter. 3. **Map columns** from the manifest into binding fields: | Manifest column (typical) | Binding field | |---------------------------|---------------| | Sheet name | `sheet` (required today; may become inferred from `data_range`) | | Cell range (e.g. `F5:J5`) | `data_range` | | Country / REF_AREA | `series_context.REF_AREA` or `bind.kind: cell` | | Indicator label | `bind.kind: row_label` + `series_context.INDICATOR` | | Year / period axis | `bind.kind: column_header` + `key: [TIME_PERIOD]`; use `read: int` for projection offsets (1–5) or `read: datetime` for calendar headers | | Active / flag column | `bind.kind: row_label` or `column_header` + `read: bool`; or `bind.kind: constant` with YAML `true` / `false` | | Unit | `structure.attributes[]` with `value:` | | Setter name | `setter.name` | 4. **Validate** against the dependency graph (see [Python API](#python-api)). 5. **Export** with `CodeGenerator(..., series_bindings=..., bindings_workbook=...)` to emit `set_*` / `compute_*` functions (see [Code export](export.qmd)). Offset projection years (1..5) vs calendar years (1999..2000) are distinguished only by what you bind in `column_header` — declare the intended semantics in `sdmx_notes`. Use `read: int` for offset-style headers and `read: datetime` (or `read: auto` when Excel stores native date cells) for calendar `TIME_PERIOD` keys. **Example:** [calendar_flags.yaml](https://github.com/Teal-Insights/excel-grapher/blob/main/tests/fixtures/series_bindings/calendar_flags.yaml) — bool flag column (`IS_ACTIVE`) plus datetime column headers (`TIME_PERIOD`). The workbook is built in integration tests via `write_calendar_flags_workbook()`. ### Export authoring checklist 1. Extract a dependency graph whose targets include the cells you need (targets seed extraction; output bindings may also cover intermediate formula cells present in the graph). 2. Declare `data_range`, `layout`, `structure.measure`, `structure.dimensions`, and `key` (input matching uses `key` only). 3. Add dimension binds (`column_header`, `row_label`, `cell`, `constant`, …). Use `bind.kind: constant` for fixed dimensions such as `FREQUENCY: A`. 4. Add `output.compute.name` (e.g. `compute_borvelia_primary_balance`) to emit a records-shaped `compute_*` function. 5. Optionally add `input.setter` in the same series or a mergeable shard for symmetric input APIs. 6. Pass `series_bindings` and `bindings_workbook` to `CodeGenerator.generate()`; call the generated `compute_*` function to obtain `Records` with `OBS_VALUE` and dimensions. ## Records contract Generated setters accept: ```python records: list[dict[str, object]] ``` Each record must include: - **Measure field** — observation written to the matched leaf cell, named by `structure.measure.concept` (`OBS_VALUE` by default). - **Key fields** — every concept listed in `key` (e.g. `TIME_PERIOD: 3` for offset-style int keys). **Key types must match resolved Python types.** Resolution coerces workbook and manifest values according to each bind’s `read` mode (and `concept_scheme` dtypes for constants and `series_context` entries). Generated setters index records by the same types — callers must not pass strings when resolution produced `datetime` or `bool` objects: | Binding | Resolved key type | Correct record value | Incorrect (will not match) | |---------|-------------------|----------------------|----------------------------| | `read: int` on `TIME_PERIOD` | `int` | `3` | `"3"` | | `read: datetime` on `TIME_PERIOD` | `datetime.datetime` | `datetime(2024, 1, 1)` | `"2024-01-01"` | | `read: auto` on `TIME_PERIOD` (Excel date serial) | `int` or `float` | serial as stored (e.g. `45309.0`) | `datetime(2024, 1, 1)` — use `read: datetime` | | `read: bool` on `IS_ACTIVE` | `bool` | `True` | `1` or `"true"` | Inspect `resolve_series_binding()` output or generated setter index literals to confirm the types for a given manifest. Output `compute_*` functions return the same types in each record’s key fields. Optional: - **`address`** or **`cell_address`** — when `setter.allow_address` is true or keys are ambiguous (`requires_address`). - Other fields allowed when `setter.strict` is false; strict mode rejects unknown keys. `series_context` values are attached to emitted records for documentation; they are not used for matching unless also listed in `key`. When a `series_context` concept has a `dtype` in `concept_scheme`, resolution coerces ISO date strings and bool-like strings the same way as bind constants. ## Bind kinds | `bind.kind` | Scope | Status | Purpose | |-------------|--------|--------|---------| | `data_cell` | leaf | **supported** | Read numeric input from the leaf itself | | `cell` | series | **supported** | Fixed address (e.g. country in `A2`) | | `column_header` | cell | **supported** | Header row for the data column | | `row_label` | series/cell | **supported** | Label text on the data row | | `constant` | series | **supported** | Fixed scalar (e.g. scalar layout key) | Normalization: `strip`, `strip_trailing_unit` (removes trailing `(…)` unit clauses from indicator text). ## Layouts `layout: series` names a **one-dimensional** `data_range` with at least one **cell-scoped** dimension in `key`. Whether data runs across rows or columns is determined by **bind kinds** (`column_header`, `row_label`, …), not by the layout name. Borvelia’s wide-row example is one orientation; a single-column range with `row_label` for the varying dimension is equally valid. | `layout` | Status | Description | |----------|--------|-------------| | `series` | **supported** | 1D `data_range` + bind geometry (row- or column-oriented) | | `scalar` | **supported** | Single editable cell | | `matrix` | **schema only** | Multi-row blocks — not implemented yet | ## Boolean and datetime types (1.4.0) Schema **1.4.0** adds `bool` and `datetime` to `bind.read`, `measure.dtype`, and `concept_scheme` concept dtypes. Use them for flag dimensions, calendar `TIME_PERIOD` axes, and typed manifest constants. ### `read: bool` Bind Excel **TRUE/FALSE** cells and bool-typed keys: - **`data_cell`** — scalar or series measure cells stored as boolean (or coerced with explicit `read: bool` from numeric 0/1). - **`column_header` / `row_label`** — flag columns or row labels used as key dimensions (e.g. an `IS_ACTIVE` column distinguishing scenario rows). - **`constant`** — fixed bool keys on `layout: scalar` or series-scoped dimensions. YAML manifest constants accept native booleans or strings such as `true`, `false`, `yes`, `no`, `1`, and `0`. When the concept dtype is `bool` (via `concept_scheme` or inferred from a native YAML boolean), resolution coerces string constants and matching `series_context` values to Python `bool`. Numeric coercion with `read: bool` accepts only **0** or **1** (int or float); other numbers raise a resolution error. ```yaml concept_scheme: concepts: - id: IS_ACTIVE dtype: bool structure: dimensions: - concept: IS_ACTIVE role: key scope: series bind: kind: constant value: true # or "FALSE" — coerced to bool - concept: IS_ACTIVE role: key scope: cell bind: kind: row_label label_column: A read: bool # Excel TRUE/FALSE in column A ``` With `read: auto` on a bool Excel cell, resolution preserves the native `bool`. Use `read: bool` when the cell may be numeric (0/1) but the key should be boolean. ### `read: datetime` Bind calendar **`TIME_PERIOD`** column headers and date constants: - **`column_header` + `read: datetime`** — calendar month/quarter/year headers formatted as Excel dates; serial numbers are converted to naive `datetime` values. - **`constant`** — ISO 8601 strings in YAML (`"2024-01-01"`, `"2024-03-15T00:00:00"`); requires `dtype: datetime` on the concept (via `concept_scheme` or dimension metadata). The same coercion applies to `series_context` entries for datetime-typed concepts. - **`read: auto`** — when Excel already exposes a native `datetime` cell, resolution keeps it without serial conversion. ```yaml concept_scheme: concepts: - id: TIME_PERIOD dtype: datetime structure: dimensions: - concept: TIME_PERIOD role: key scope: cell bind: kind: column_header header_row: 1 read: datetime # calendar headers; not projection offsets - concept: TIME_PERIOD role: key scope: series bind: kind: constant value: "2024-01-01" # ISO date string in the manifest ``` **`read: auto` vs explicit `read: datetime`:** `auto` passes through native datetimes from the workbook reader and is appropriate when headers are stored as Excel date cells. Explicit `datetime` coerces Excel serial numbers and ISO strings and is required when you need deterministic date parsing regardless of how the reader classified the cell. Timezone-aware datetimes are rejected; only naive datetimes are supported. **`read: auto` footgun:** when the workbook reader returns a calendar date as a **number** (Excel serial), `read: auto` keeps it as `int` or `float` — it does not convert serials to `datetime`. That often surfaces as setter key mismatches: resolution indexed `45309.0` but callers pass `datetime(2024, 1, 1)`. Prefer `read: datetime` on `column_header` binds for calendar `TIME_PERIOD` axes unless you have confirmed the reader yields native `datetime` cells for those headers. Contrast with Borvelia’s offset-style headers (`read: int`, values 1–5): calendar and offset period axes differ only in the declared `read` mode and analyst notes — not in layout name. Generated code imports `datetime` and emits `datetime.datetime(...)` literals only when resolutions include datetime scalars. ## Schema versions | Version | Contents | |---------|----------| | **1.0.0–1.2.0** | `row_series`, `scalar`; supported bind kinds above. Use for production workbooks. | | **1.1.0** | Draft extensions (e.g. `matrix` in schema). Not fully implemented in tooling. | | **1.2.0** | Optional `input` / `output` direction blocks and output `compute_*` functions. | | **1.3.0** | `row_series` renamed to `series` (no resolver/codegen change). Legacy `row_series` accepted at load time. | | **1.4.0** | `bool` and `datetime` read modes and dtypes; ISO date strings in manifest constants. | ## Validation and codegen closure `validate_series_bindings()` and `derive_*_series` intersect bindings with the extracted graph (input: **leaves**; output: **any graph node**). **Codegen** intersects with the export closure from the current `CodeGenerator` targets (formula cells, inputs, and dependencies actually emitted). Cells outside that closure are skipped; `validation.warn_on_partial_overlap` (default `true`) emits a warning when that happens. ## Python API {#python-api} ```python from pathlib import Path from excel_grapher.grapher import create_dependency_graph from excel_grapher.series_bindings import ( bindings_canonical_sha256, derive_input_series, expand_data_range, load_series_bindings, validate_series_bindings, ) from excel_grapher.exporter import CodeGenerator workbook = Path("lic_inputs.xlsx") bindings = load_series_bindings(workbook.with_suffix(".bindings.yaml")) targets = [ addr for s in bindings["series"] for addr in expand_data_range(s["data_range"], workbook=workbook) ] graph = create_dependency_graph(workbook, targets, load_values=True) report = validate_series_bindings(graph, bindings, workbook=workbook) assert report["ok"] bindings_canonical_sha256(bindings) input_series = derive_input_series(graph, bindings, workbook=workbook) with CodeGenerator(graph) as gen: code = gen.generate( targets, series_bindings=bindings, bindings_workbook=workbook, ) ``` After export: ```python ctx = make_context() set_borvelia_primary_balance(ctx, [{"TIME_PERIOD": 3, "OBS_VALUE": -0.5}]) results = compute_all(ctx=ctx) ``` For modular package exports, generated series setters are emitted from the package entrypoint and re-exported from the package root, alongside `make_context` and `compute_all`. ## Command-line validation Full CLI reference: [Command-line interface](cli.qmd). Quick examples: ```bash excel-grapher bindings validate lic_inputs.xlsx excel-grapher bindings validate lic_inputs.xlsx --json excel-grapher bindings validate lic_inputs.xlsx --smoke-test excel-grapher bindings validate lic_inputs.xlsx --smoke-test --emit-dir ./generated ``` By default the command resolves `lic_inputs.bindings.yaml` or a `lic_inputs.bindings/` shard directory colocated with the workbook. Use `--bindings` to override. Validation-only is the default; `--smoke-test` generates modular export code and exercises every declared `set_*` / `compute_*` function; `--emit-dir` writes generated module files to disk. ## Design note Series bindings are framed around **input series** derived from binding manifests, not independently discovered input groups. One binding id corresponds to one generated setter (when `input.setter` is declared), one optional output `compute_*` function (when `output.compute` is declared), and matching input-series views over the participating graph cells in `data_range`. Design history: [GitHub issue #192](https://github.com/Teal-Insights/excel-grapher/issues/192). ### Exporting standalone Python The exporter turns a `DependencyGraph` into a standalone Python module: ```python from excel_grapher.exporter import CodeGenerator code = CodeGenerator(graph).generate(targets) print("\n".join(code.splitlines()[:120])) ``` When graph nodes are target-marked (`Node.is_target=True`), `generate()` and `generate_modules()` can infer export targets directly from the graph: ```python with CodeGenerator(graph) as gen: code = gen.generate() # defaults to graph.target_keys() ``` If neither explicit targets nor target-marked nodes are available, code generation raises a `ValueError`. For a multi-file package, `generate_modules()` returns a `dict[str, str]` keyed by module filename (`__init__.py`, `api.py`, `data.py`, `runtime.py`, `internals.py`). Write those files into a package directory of your choice, then import the package as usual. ## Series bindings Optional sidecar manifests (`.bindings.yaml`) declare structured input/output APIs on exported code. See the dedicated [Series bindings](series-bindings.qmd) guide for the JSON Schema, authoring conventions, validation rules, and Python API. Minimal codegen hook: ```python from pathlib import Path from excel_grapher.exporter import CodeGenerator from excel_grapher.series_bindings import load_series_bindings workbook_path = Path("lic_inputs.xlsx") bindings = load_series_bindings(workbook_path.with_suffix(".bindings.yaml")) code = CodeGenerator(graph).generate( targets, series_bindings=bindings, bindings_workbook=workbook_path, ) ``` ## Series docstring callbacks When exporting series bindings, you can attach structured docstrings to generated `set_*` and series output `compute_*` functions by passing a callback directly or by registering a reusable callback name for project-wide use: ```python from excel_grapher.exporter import ( CodeGenerator, FieldDoc, GoogleSeriesDocstringRenderer, SeriesFunctionDoc, register_series_docstring_callback, ) def my_series_docstring(ctx): # ctx.contract contains deterministic binding facts (fields, examples, layout, etc.) return SeriesFunctionDoc( summary=f"Set {ctx.contract.series_id}.", purpose="Updates workbook inputs from Records.", record_matching="Records match by key fields.", field_descriptions={ field: FieldDoc(description=f"Describe {field}.") for field in ctx.contract.required_fields }, ) register_series_docstring_callback("my_docs", my_series_docstring) def compact_google_renderer(contract, doc, *, series=None): # Simple callables are valid custom renderers. return GoogleSeriesDocstringRenderer().render(contract, doc, series=series) code = CodeGenerator(graph).generate( targets, series_bindings=bindings, bindings_workbook=workbook_path, series_docstring_callback="my_docs", # or my_series_docstring docstring_renderer=compact_google_renderer, # or "plain" | "rst" | "google" | "numpy" ) ``` Callbacks return `SeriesFunctionDoc` prose; the exporter renders deterministic sections such as accepted fields, source binding metadata, and example calls. Return `None` to omit a docstring. Callback contract highlights: - The callback is invoked once per generated series API function (`set_*` and series output `compute_*`). - Use `ctx.function_kind` (`"setter"` or `"compute"`) and `ctx.function_name` to branch wording per function. - `ctx.contract` contains deterministic binding facts for that specific function (required fields, field dtypes, examples, layout, and data range). - Docstring callbacks may be direct callables or registered names (`series_docstring_callback="..."`). - Docstring renderers can be built-in names (`"plain"`, `"rst"`, `"google"`, `"numpy"`), renderer objects implementing `SeriesDocstringRenderer`, or simple callables `(contract, doc, *, series=None) -> str`. - If callback rendering is requested, codegen requires full series context (`series_bindings` + `bindings_workbook`). ## Graph projection for export Identity transit nodes (pure mirrors such as `Outputs!B12 = Engine!C6`) can be collapsed in **projected** export artifacts while the canonical workbook graph stays unchanged. Pass a `ProjectionResult` to `CodeGenerator` so public targets and series bindings keep their workbook-facing addresses: ```python from excel_grapher.exporter import CodeGenerator, IdentityTransitCompression projection = IdentityTransitCompression().project(graph) modules = CodeGenerator(projection).generate_modules( targets, series_bindings=series_bindings, bindings_workbook=workbook_path, ) ``` `ProjectionResult` behaves like the projected graph for dependency reads, and exposes `original_graph` plus a serializable `manifest`. The manifest separates **address projection** (`map_to_projected`, used by codegen to keep removed public addresses resolving) from **collapse lineage** (`retained_to_collapsed_sources`, `removed_node_snapshots` with original formulas and metadata, `collapsed_groups`, `formula_rewrites`) for audit and refactoring. Visualization can use the same facade: ```python from excel_grapher.grapher import to_networkx from excel_grapher.exporter import to_web_viz_payload payload = to_web_viz_payload(to_networkx(projection)) ``` Projection is an extension point: implement the `ProjectionStep` protocol to build your own projected graph (see the contributing guide), and compose steps with `apply_projection`. In-place `DependencyGraph.compress_identity_transits()` remains available for destructive graph simplification; prefer projections for export and viz workflows. ## Exported code sketch A (truncated) sketch of the exported code: ```python """Standalone runtime for generated Excel formula code.""" from __future__ import annotations from enum import Enum class XlError(str, Enum): """Excel error values.""" VALUE = "#VALUE!" REF = "#REF!" DIV = "#DIV/0!" NA = "#N/A" NAME = "#NAME?" NUM = "#NUM!" NULL = "#NULL!" def to_number(value) -> float | XlError: ... def xl_mul(left, right) -> float | XlError: ... from functools import lru_cache # --- Cell functions --- @lru_cache(maxsize=None) def cell_s_a1(): """Leaf cell: S!A1""" return 10 @lru_cache(maxsize=None) def cell_s_b1(): """Formula: =A1*2""" return xl_mul(cell_s_a1(), 2.0) def compute_all(): """Compute all target cells and return results.""" return { "S!B1": cell_s_b1(), } ``` ### Exported code results ```python namespace: dict = {} exec(code, namespace) generated_results = namespace["compute_all"]() print(generated_results) # {'S!B1': 20.0} ``` **Tradeoffs for the exported-code approach:** - **Advantages** - **Standalone artifact**: output is plain Python; no need to distribute `excel_grapher` or the evaluator with it. - **Partial obfuscation**: does not expose the extraction engine directly. - **Minimal runtime surface**: embeds only the Excel-equivalent `xl_*` helpers actually needed by the exported graph. - **Repeatable execution**: freezes workbook logic at a point in time; downstream runs are deterministic and Excel-free. - **Disadvantages** - **Still Excel-shaped**: the structure is still cell-centric and Excel-like; interpretability gains are incremental. - **Regeneration required**: changes to the workbook require re-extracting and re-exporting. ### Parity testing **Behavioral parity** is defined across three layers: **Excel** (reference), **`FormulaEvaluator`**, and **standalone exported code**. Shared semantics live in `excel_grapher/exporter/export_runtime/` so the evaluator and generated code stay aligned (**evaluator ↔ export** checks use `tests/integration/utils/parity_harness.py`). **Evaluator ↔ Excel** checks use values saved in the workbook and, when automation is available, **live recalculation** via xlwings that require Excel should **run when automation works** and **`pytest.skip`** otherwise (see `tests/integration/evaluator/test_golden_master.py`). ## Development ## Development environment setup Clone the repository: ```bash git clone https://github.com/Teal-Insights/excel-grapher.git cd excel-grapher ``` Install `uv` if you don't have it already: ```bash curl -LsSf https://astral.sh/uv/install.sh | sh ``` Install the dependencies: ```bash uv sync ``` Install pre-commit hooks: ```bash uv run pre-commit install ``` To render the documentation, install Quarto if you don't have it already: ```bash curl -LsSf https://quarto.org/install.sh | sh ``` ## Extension-point conventions A key goal of `excel-grapher` is to make it easy to extend the library with new functionality. We achieve this by exposing a rich set of extension points that allow users to customize library behaviors, especially during graph extraction, visualization, and code generation. Prefer explicit, typed extension points over ad hoc keyword flags when users need to customize behavior. The core API should make simple Python usage direct, while still supporting named plugins when a choice needs to be stored in configuration, selected from a CLI, or reused across a project. ### Current patterns in the codebase | Extension point | Customization forms | Registry helpers | Notes | | --- | --- | --- | --- | | Graph build hooks (`NodeHook`) | Direct callables only | None | Passed to `create_dependency_graph(..., hooks=...)` or `DependencyGraph.register_hook`. Local instrumentation only. | | Series docstring callbacks | `SeriesBindingDocstringCallbackSpec` | `register_*`, `list_*`, `resolve_*`, `run_*`, `unregister_*` | Direct callback or registered name for codegen. | | Series docstring renderers | `SeriesDocstringRendererSpec` | `resolve_*` only | Built-in names plus renderer objects or callables. | | Web visualization layouts | `WebVizLayoutSpec` | `register_*`, `list_*`, `resolve_*`, `run_*`, `unregister_*` | Layout ID string or direct `WebVizLayoutPlugin` callable. | ### Direct customization first For local Python usage, accept direct objects or callables when they are easy to type and configure: ```python code = CodeGenerator(graph).generate( targets, series_bindings=bindings, bindings_workbook=workbook_path, series_docstring_callback=my_callback, ) payload = to_web_viz_payload(graph, layout=my_layout_plugin) ``` Use a `Protocol` for non-trivial callables so contributors know the expected context and return type. ### Named plugins for reusable choices Use registries when an extension should be selectable by a stable string name. Registry-backed APIs should expose: - `register_*` - `list_*` - `resolve_*` for lookup without execution - `run_*` only when the helper executes the plugin immediately - `unregister_*` for test and notebook cleanup (optional but recommended) Duplicate registration should fail by default. Support `replace=True` as an explicit opt-in when replacement is useful in tests, notebooks, or project bootstrap code: ```python register_series_docstring_callback("project_docs", my_callback) code = CodeGenerator(graph).generate( targets, series_bindings=bindings, bindings_workbook=workbook_path, series_docstring_callback="project_docs", ) # In tests or notebooks, re-register or clean up: register_series_docstring_callback("project_docs", updated_callback, replace=True) unregister_series_docstring_callback("project_docs") ``` ### Built-ins and specs When an API accepts more than one customization form, define a `*Spec` type alias. Built-ins should usually be available both as importable Python objects/classes and as stable string IDs if they are useful in config-driven workflows. Examples in the current API: - `SeriesDocstringRendererSpec`: built-in renderer name, renderer object, or callable. - `SeriesBindingDocstringCallbackSpec`: registered callback name or direct callback. - `WebVizLayoutSpec`: registered layout ID or direct `WebVizLayoutPlugin`. ### Scope guidelines Use direct callables only for local instrumentation hooks that are not reusable plugin choices, such as `NodeHook` graph build hooks. Use named registries for behavior that users may want to list, document, select from config, or share across workflows. ### Graph projection for export Projections build artifact-specific views of the canonical graph without mutating it. A projection step implements the `ProjectionStep` protocol (`project(graph) -> ProjectionResult`) and returns a `ProjectionResult` facade that reads like the projected graph and carries a durable `ProjectionManifest`. The facade satisfies the read-only `grapher.GraphReadView` protocol, the same surface `DependencyGraph` exposes, so read-only consumers such as `to_networkx` and `CodeGenerator` accept a graph or a projection interchangeably. `IdentityTransitCompression` is the built-in reference step: ```python projection = IdentityTransitCompression().project(graph) CodeGenerator(projection).generate_modules(...) to_web_viz_payload(to_networkx(projection)) ``` #### The manifest contract `ProjectionManifest` is a protocol; codegen depends only on its small address-projection surface (`kind`, `map_to_projected`, `to_dict`). `BaseProjectionManifest` is a ready-to-use implementation that also carries collapse lineage. It deliberately separates two concerns: - **Forwarding map** (`forwarding_map`): removed addresses that are value-equivalent to a retained computation. Codegen queries this through `map_to_projected` only for public targets and series binding ranges, so internal-only collapses should leave it empty. - **Collapse lineage** (`retained_to_collapsed_sources`, `removed_node_snapshots`, `collapsed_groups`, `formula_rewrites`): which originals folded into each retained node, in dependency order, with original formulas and metadata. This is for audit and downstream refactoring and does *not* imply value-equivalence. #### Writing a custom projection Build the projected graph from a copy and record lineage on the manifest: ```python from excel_grapher.exporter import ( BaseProjectionManifest, ProjectedNodeSnapshot, ProjectionResult, register_projection_manifest, ) class SubgraphCollapse: def project(self, graph): projected = graph.copy() # never mutate the canonical graph # ... select a single-exit subgraph, then on `projected`: # set_node_formula(root, inlined_formula, normalized_formula) # substitute bodies # add_edge(root, external_dep, ...) # rewire to surviving deps # remove_node(internal) # delete collapsed internals # set_node_metadata(root, {"collapsed_from": [...]}) # tag the condensed node manifest = BaseProjectionManifest( kind="subgraph_collapse", forwarding_map={}, # internal nodes are not forwarded retained_to_collapsed_sources={root: tuple(internals)}, removed_node_snapshots={addr: ProjectedNodeSnapshot(...) for addr in internals}, formula_rewrites=(), collapsed_groups=(), ) return ProjectionResult(graph, projected, manifest) # Enable serialization round-trips for the custom kind: register_projection_manifest("subgraph_collapse", BaseProjectionManifest.from_dict) ``` Public graph-mutation primitives for projection authors: `DependencyGraph.copy`, `set_node_formula`, `remove_node`, `set_node_metadata`, `add_edge`. Compose steps with `apply_projection(graph, [step_a, step_b])`; multiple steps fold into a `CompositeProjectionManifest` that chains the forwarding map and preserves each component manifest (heterogeneous kinds supported). A parity-tested formula-inlining/subgraph-collapse engine is a planned follow-up; until then, authors own the substitution logic in their projection step. A config-driven registry for selecting projections (mirroring web viz layouts) may follow when needed. ### Alignment status Changes aligned in this release: - Series docstring callbacks accept direct callables or registered names via `SeriesBindingDocstringCallbackSpec`. - Web viz layouts accept direct plugins via `WebVizLayoutSpec`; `register_web_viz_layout` supports `replace=True`. - Registry cleanup helpers (`unregister_*`) for docstring callbacks and web viz layouts. Deferred (no change needed yet): - Graph build hooks remain direct-only; a registry would add no value for one-off instrumentation. - Series docstring renderers stay lookup-only (no global registration); built-ins are fixed and custom renderers are passed inline. ## Roadmap - Continue expanding **three-way parity** coverage: evaluator ↔ export runtime, evaluator ↔ Excel (cache and live automation where available), especially for representation-sensitive areas such as `OFFSET`, `INDIRECT`, `LOOKUP`, `MATCH`, and `INDEX`. - Refine the dynamic-reference configuration API and constraints tooling (e.g., validation helpers) as more real-world models and templates are integrated. - [Live workflow graph](/workflow/) ## Export & quality ### Command-line interface The `excel-grapher` command-line tool validates series-binding sidecars against workbooks, optionally smoke-tests generated setters and computes, and can write export modules to disk. It complements the Python APIs documented in [Series bindings](series-bindings.qmd). Run it from the project environment: ```bash uv run excel-grapher --help uv run excel-grapher bindings --help uv run excel-grapher bindings validate --help ``` When the package is installed, `excel-grapher` is also available as a console script. ## Command overview | Command | Purpose | |---------|---------| | `bindings validate` | Load a sidecar, build the dependency graph, and validate bindings against the workbook | More subcommands may be added over time; `excel-grapher --help` lists what is available in your installed version. ## `bindings validate` Validate a workbook and its binding sidecar before export or CI. ### Synopsis ```bash excel-grapher bindings validate WORKBOOK [--bindings PATH] [--json] [-v] [--smoke-test] [--emit-dir DIR] [--package-name NAME] ``` ### Arguments | Argument | Description | |----------|-------------| | `WORKBOOK` | Path to the `.xlsx` workbook (required). | | `--bindings PATH` | Binding sidecar **file** or **shard directory**. When omitted, the CLI looks for a colocated sidecar (see below). | | `--json` | Print the full validation `report` object as JSON (includes every issue). | | `-v`, `--verbose` | Print **warnings** in human-readable output. **Errors** are always printed when validation fails. | | `--smoke-test` | After a successful validation, generate binding modules and run setter/compute smoke checks. | | `--emit-dir DIR` | Write generated module files under `DIR` (see `--package-name`). Implied when combined with `--smoke-test` and an emit directory. | | `--package-name NAME` | Package directory name for smoke tests and emitted files (default: `bindings_module`). | Validation-only is the default: no code is generated unless you pass `--smoke-test` or `--emit-dir`. ### Resolving the sidecar When `--bindings` is omitted, `bindings validate` tries, in order: 1. `{workbook_stem}.bindings.yaml` next to the workbook 2. `{workbook_stem}.bindings/` (directory of `*.bindings.yaml` shards) Pass `--bindings` explicitly when the sidecar lives elsewhere—for example a shared fixture: ```bash uv run excel-grapher bindings validate examples/micro_workbooks/ffv2.xlsx \ --bindings tests/fixtures/series_bindings/ffv2.yaml ``` ### Example: ffv2 game log The [ffv2](https://github.com/Teal-Insights/excel-grapher/blob/main/examples/micro_workbooks/ffv2.xlsx) micro-workbook uses datetime column headers as `TIME_PERIOD` keys. With a valid sidecar: ```bash uv run excel-grapher bindings validate examples/micro_workbooks/ffv2.xlsx \ --bindings tests/fixtures/series_bindings/ffv2.yaml ``` Successful validation prints a short summary on stdout: ```text ok=True errors=0 warnings=0 canonical_sha256=… setters=['set_puka_longest_reception', 'set_puka_receptions', …] computes=['compute_puka_avg_yards_per_reception', …] ``` Add `-v` to include warnings (for example `dtype_read_mismatch` when a bind `read` mode disagrees with a concept `dtype`): ```bash uv run excel-grapher bindings validate examples/micro_workbooks/ffv2.xlsx \ --bindings tests/fixtures/series_bindings/ffv2.yaml -v ``` Use `--json` for machine-readable output (the same structure returned by `validate_series_bindings()`): ```bash uv run excel-grapher bindings validate examples/micro_workbooks/ffv2.xlsx \ --bindings tests/fixtures/series_bindings/ffv2.yaml --json ``` Each issue in the JSON report has `level`, `code`, `message`, and optional `series_id` and `address` fields. ### Smoke test and code generation After validation succeeds, `--smoke-test` generates modular binding code, imports each declared `set_*` and `compute_*` function, and runs lightweight round-trip checks: ```bash uv run excel-grapher bindings validate lic_inputs.xlsx --smoke-test ``` To persist generated files: ```bash uv run excel-grapher bindings validate lic_inputs.xlsx \ --smoke-test --emit-dir ./generated --package-name lic_bindings ``` Without `--smoke-test`, `--emit-dir` alone writes modules after validation: ```bash uv run excel-grapher bindings validate lic_inputs.xlsx --emit-dir ./generated ``` When smoke checks pass, the CLI prints: ```text All setter and compute functions passed smoke checks. ``` ### Error output Failures fall into two layers: **Sidecar schema errors** — the YAML/JSON manifest does not satisfy the JSON Schema (missing required fields, invalid setter names, and so on). These are reported on **stderr** before workbook validation runs: ```text Binding sidecar schema error: series[8] "puka_week_1_fantasy_score": missing required field `key` — Add a key list naming the dimension concepts that identify each record. ``` Locations use the series index and `id` from the sidecar so you can jump straight to the offending entry. **Workbook validation errors** — the sidecar parses, but coordinate resolution or binding rules fail against the live workbook. These appear as human-readable lines on **stdout** (or inside `issues[]` with `--json`): ```text error [bind_resolution_failed] puka_targets:Sheet1!A4: could not convert string to float: 'Tgts' ``` When the same root cause affects many cells (for example a `read: float` bind over a row that includes a text label), identical messages are **aggregated** into one issue with a cell count and range rather than one line per cell. Warnings such as `dtype_read_mismatch` do not fail validation by themselves; use `-v` to see them in text mode. ### Exit codes | Code | Meaning | |------|---------| | `0` | Validation succeeded (and smoke tests passed, if requested). | | `1` | Workbook or sidecar not found, schema error, validation failure, or smoke-test failure. | | `2` | Unknown subcommand or invalid CLI usage. | ### Related Python APIs The CLI wraps the same workflow as: ```python from pathlib import Path from excel_grapher.series_bindings import validate_bindings_workbook, run_binding_checks workbook = Path("lic_inputs.xlsx") bindings_path = workbook.with_suffix(".bindings.yaml") result = validate_bindings_workbook(workbook, bindings_path) assert result["report"]["ok"] run_binding_checks( workbook, bindings_path, module_dir=Path("./generated/lic_bindings"), package_name="lic_bindings", smoke_test=True, ) ``` See [Series bindings](series-bindings.qmd) for manifest authoring, bind kinds, and export integration.