Skip to content

Policy, Locale, Pivot, and Render APIs

Version scope

This API reference describes WolfXL 2.1.0. Every symbol on this page is Added in 2.1.0.

These eight areas share one design rule: nothing executes, refreshes, or evaluates unless the caller explicitly opts in, and every refusal is typed.

Two invariants hold across the whole page:

  • Active content never executes without an explicit caller-supplied evaluator callback. WolfXL never supplies one.
  • Default DataModel evaluation is disabled. ModelEvaluationPolicy() is default-deny and refuses before a measure is parsed or inspected.

Every example below runs offline against the installed wheel. None of them needs Microsoft Excel or network access.

Lifecycle and deprecation policy

What is public: the names this page imports from wolfxl, wolfxl.package, wolfxl.security, wolfxl.external, and wolfxl.datamodel.engine. Anything with a leading underscore, plus wolfxl._rust, is internal and may change in any release.

Within a major series, this surface only grows. A minor release may add a symbol, an enum member, a report field, or an optional keyword argument; it never removes one, renames one, or reorders positional parameters. Treat every typed enumeration as open: match the members you handle and route the rest to a default branch instead of assuming the set is final.

Two behavioral guarantees are part of the contract, not implementation detail. A default that refuses to execute, refresh, or evaluate is never flipped on by a minor release, and a redacted field never starts carrying package payloads, external targets, or credential material.

Structured payloads that a caller parses carry their own schema_version, which increments when an existing field changes meaning. Assert the versions you support and ignore unrecognized fields.

A deprecation is marked here as Deprecated in <version>, recorded in the release notes, and keeps working for the rest of the major series. Removal happens only in a major release. See Versioning and Upgrades for the pinning and upgrade flow.

Active content: inspect, authorize, execute

Added in 2.1.0

Import from wolfxl.security.active_content (also re-exported from wolfxl.security).

Inspection is metadata-only. ActiveContentInventory is built from a PackageInventory and reports typed channels through ActiveContentFinding. ActiveContentFinding.part_name is str | None: it is None for an external relationship originating at the package root and otherwise names the source part. Findings also carry via_external_relationship, and deliberately omit external targets, relationship ids, payloads, connection strings, and provider values.

ActiveContentKind members: VBA, XLM_MACROSHEET, OLE_EMBEDDING, ACTIVEX, ADD_IN, CUSTOM_UI, POWER_QUERY_DATA_MASHUP, DATA_MODEL, EXTERNAL_WORKBOOK_LINK, QUERY_TABLE, CONNECTION, LINKED_RICH_VALUE_PROVIDER, EXTERNAL_RELATIONSHIP.

A customXml/ item is reported as a POWER_QUERY_DATA_MASHUP candidate, because DataMashup is encoded inside custom XML and cannot be distinguished without reading that payload. The conservative classification keeps the redaction boundary from becoming an under-detection boundary.

from wolfxl.package import PackageInventory
from wolfxl.security.active_content import (
    ActiveContentExecutionUnavailable,
    ActiveContentKind,
    ActiveContentPolicy,
    evaluate_active_content,
    inventory_active_content,
)

parts = {
    "[Content_Types].xml": (
        b'<Types xmlns="http://schemas.openxmlformats.org/package/2006/'
        b'content-types"><Default Extension="xml" ContentType="application/xml"/>'
        b"</Types>"
    ),
    "xl/vbaProject.bin": b"\x00\x01",
    "customXml/item1.xml": b"<x/>",
}

inventory = inventory_active_content(PackageInventory(parts))
print(inventory.has_active_content)                       # True
print([kind.value for kind in inventory.kinds])
# ['power_query_data_mashup', 'vba']
print(inventory.has_kind(ActiveContentKind.VBA))          # True

# The default policy refuses before the evaluator argument is even observed.
try:
    evaluate_active_content(inventory)
except ActiveContentExecutionUnavailable as refusal:
    print(refusal.diagnostic.code.value)                  # disabled_by_policy
    print(refusal.diagnostic.channel_count)               # 2

Authorization defaults and bounds

ActiveContentPolicy defaults: allow_evaluator=False, max_recursion_depth=8, max_wall_clock_seconds=30.0, max_memory_bytes=67108864 (64 MiB), cancellation=None. Hard ceilings rejected with ValueError at construction: depth above 64, wall clock above 300.0 seconds, memory above 536870912 bytes (512 MiB).

Bounds are handed to the evaluator through ActiveContentEvaluationRequest (max_recursion_depth, max_wall_clock_seconds, max_memory_bytes, deadline_monotonic, and cancelled()). A synchronous Python callback cannot be preempted or memory-capped by this library, so a caller needing hard isolation must implement it inside the evaluator, for example in a constrained worker process.

policy = ActiveContentPolicy(
    allow_evaluator=True,
    max_recursion_depth=4,
    max_wall_clock_seconds=1.0,
    max_memory_bytes=1 << 20,
)

outcome = evaluate_active_content(
    inventory,
    lambda request: request.max_recursion_depth,
    policy=policy,
)
print(outcome.status.value)                 # completed
print(outcome.diagnostic.code.value)        # completed

failed = evaluate_active_content(
    inventory,
    lambda request: (_ for _ in ()).throw(RuntimeError("boom")),
    policy=policy,
)
print(failed.status.value)                  # failed
print(failed.diagnostic.code.value)         # evaluator_failed

The evaluator's return value is discarded on purpose: it may carry untrusted package data, credentials, or provider output, so it must not cross this security boundary. ActiveContentEvaluationOutcome exposes only status, diagnostic, and elapsed_seconds.

ActiveContentDiagnosticCode: COMPLETED, DISABLED_BY_POLICY, EVALUATOR_REQUIRED, CANCELLED, WALL_CLOCK_LIMIT_EXCEEDED, EVALUATOR_FAILED.

ActiveContentExecutionStatus: COMPLETED, CANCELLED, LIMIT_EXCEEDED, FAILED.

Raise-versus-return boundary:

Situation Result
allow_evaluator=False raises ActiveContentExecutionUnavailable, code disabled_by_policy
policy allows, evaluator=None raises ActiveContentExecutionUnavailable, code evaluator_required
cancellation already signalled raises ActiveContentExecutionUnavailable, code cancelled
evaluator not callable raises TypeError
evaluator raised returns outcome, status failed
evaluator overran the wall clock returns outcome, status limit_exceeded
cancellation signalled during the call returns outcome, status cancelled

ActiveContentPolicy is unrelated to wolfxl.external.RefreshPolicy: a refresh allowlist resolves external workbook channels, whereas this policy decides whether a supplied evaluator may run at all.

OOXML package admission

Added in 2.1.0

Import from wolfxl.package. PackageInventory accepts a mapping of part name to bytes, a bytes payload, a path, or an open zipfile.ZipFile, and produces an immutable, deterministic, payload-redacted inventory.

from wolfxl.package import PackageError, PackageInventory, allocate_custom_xml_slot

inventory = PackageInventory(parts)
print(inventory.part_names)
# ('[Content_Types].xml', 'customXml/item1.xml', 'xl/vbaProject.bin')
print([(part.name, part.content_type, part.size) for part in inventory.parts][2])
# ('xl/vbaProject.bin', None, 2)

slot = allocate_custom_xml_slot(inventory)
print(slot.parts)
# ('customXml/item2.xml', 'customXml/itemProps2.xml', 'customXml/_rels/item2.xml.rels')

try:
    PackageInventory({"../evil.xml": b""})
except PackageError as error:
    print(error)                            # unsafe OOXML package part path

PackagePart exposes name, content_type, size, and sha256; never the payload. PackageRelationship sets target_part only for internal relationships, because an external target string can carry credentials or other sensitive locator material; is_external reports the mode. PackageInventory also groups custom_xml_parts, embedded_parts, and external_parts.

allocate_custom_xml_slot returns the first triple whose item, item-properties, and relationships paths are all free. reserved_parts lets a caller hold back paths not yet written, so a planned DataMashup triple stays unavailable to a concurrent generic allocation.

Admission limits

Defaults, each overridable by environment variable:

Limit Default Environment variable
Member count 200000 WOLFXL_MAX_ZIP_ENTRIES
Single part size 536870912 (512 MiB) WOLFXL_MAX_ZIP_ENTRY_BYTES
Total uncompressed size 4294967296 (4 GiB) WOLFXL_MAX_ZIP_TOTAL_BYTES
Per-part compression ratio 1000 WOLFXL_MAX_ZIP_COMPRESSION_RATIO

Redacted error strings

Every admission failure raises PackageError, a ValueError subclass. The messages are fixed and value-redacted, so a rejection never echoes package content:

  • invalid OOXML package
  • OOXML package exceeds member limit
  • OOXML package exceeds total size limit
  • OOXML package part exceeds size limit: <part name>
  • OOXML package part exceeds compression ratio limit
  • OOXML package contains duplicate part names
  • OOXML package contains case-colliding part names
  • OOXML package contains duplicate normalized part names
  • unable to read OOXML package part
  • OOXML package part size changed while reading
  • unsafe OOXML package part path
  • invalid content types, invalid content type default, invalid content type override, duplicate content type default, duplicate content type override
  • invalid relationships, invalid relationship part path, relationship source part is missing, relationship target part is missing
  • unsafe relationship target, unsafe external relationship target

Part names are compared after percent-escape decoding, NFC normalization, and case folding, so duplicate, case-colliding, and normalization-colliding names are rejected rather than silently merged. Absolute, backslashed, traversing, and control-character paths are rejected. An external relationship target with a dangerous URI scheme is rejected as unsafe external relationship target. Native reader admission tolerates a missing internal sharedStrings or externalLink target only for the exact standard relationship type URI and only when the relationship is owned by the workbook part. This narrow compatibility rule admits stale optional relationships emitted by real spreadsheet tools; every other missing internal target remains rejected.

Not supported: writing, repairing, or decrypting a package through this API. It is read-only inspection.

Delimited export: formula-injection policy

Added in 2.1.0

Import ConversionOptions, FormulaInjectionPolicy, FormulaInjectionAction, FormulaInjectionReportEntry, FormulaInjectionRejectedError, and the convert* entry points from wolfxl.

FormulaInjectionPolicy.ESCAPE is the default. The policy applies to CSV, TSV, and TXT exports; any other policy paired with another destination format raises ValueError (formula_injection_policy is supported only for CSV, TSV, and TXT).

Policy Rendered bytes Report entry Diagnostic
ESCAPE (default) candidate gets a leading ' action escaped none
WARN candidate retained verbatim action warned feature_rewritten warning, feature formula_injection
REJECT no artifact published none raises FormulaInjectionRejectedError
ALLOW candidate retained verbatim action allowed none

A candidate is rendered text whose first character after leading ASCII spaces is a formula marker, or any C0 control in that lead-in (importers can discard controls before evaluating). A complete signed numeric literal such as -12.5 stays numeric text and is not a candidate. Delimited exports render U+0000 as the literal text \x00 after policy evaluation so the output remains readable by supported Python 3.10 CSV tooling.

import io

from wolfxl import (
    ConversionOptions,
    FormulaInjectionPolicy,
    FormulaInjectionRejectedError,
    Workbook,
    convert_to_bytes,
)

workbook = Workbook()
sheet = workbook.active
sheet["A1"] = "=1+1"
sheet["A2"] = "safe"
buffer = io.BytesIO()
workbook.save(buffer)
source = buffer.getvalue()

escaped = convert_to_bytes(source, options=ConversionOptions(destination_format="csv"))
print(escaped.data)                                        # b"'=1+1\r\nsafe\r\n"
print([entry.to_dict() for entry in escaped.report.formula_injection_entries])
# [{'policy': 'escape', 'action': 'escaped', 'candidates': 1}]

warned = convert_to_bytes(
    source,
    options=ConversionOptions(
        destination_format="csv",
        formula_injection_policy=FormulaInjectionPolicy.WARN,
    ),
)
print(warned.data)                                         # b'=1+1\r\nsafe\r\n'

try:
    convert_to_bytes(
        source,
        options=ConversionOptions(
            destination_format="csv",
            formula_injection_policy=FormulaInjectionPolicy.REJECT,
        ),
    )
except FormulaInjectionRejectedError as rejection:
    print(rejection.to_dict())
    # {'policy': 'reject', 'action': 'rejected', 'candidates': 1}

allowed = convert_to_bytes(
    source,
    options=ConversionOptions(
        destination_format="tsv",
        formula_injection_policy=FormulaInjectionPolicy.ALLOW,
    ),
)
print([entry.to_dict() for entry in allowed.report.formula_injection_entries])
# [{'policy': 'allow', 'action': 'allowed', 'candidates': 1}]

Evidence is aggregate and value-redacted: FormulaInjectionReportEntry and FormulaInjectionRejectedError both expose only policy, action, and a candidates count, through attributes and to_dict(). The candidate cell text never appears. A report entry is emitted only when at least one candidate was seen; REJECT publishes no artifact at all, so it emits no entry.

ConversionOptions also gates cell_range, values, encoding, and bom to CSV and TSV, rejects utf-16 and utf-32 without an explicit byte order, and allows bom=True only for UTF encodings.

Multi-area references

Added in 2.1.0

Import from wolfxl.formula.reference.

ReferenceArea is one normalized rectangular A1 area; MultiAreaReference is an ordered tuple of one or more areas. parse_reference_expression parses the parenthesized Excel reference grammar: a comma inside a grouping parenthesis is the union operator, whitespace between references is the intersection operator, and a comma that separates function arguments stays a separator.

from wolfxl.formula.reference import (
    MultiAreaReference,
    NullIntersectionError,
    parse_reference_expression,
)

union = parse_reference_expression("=(A1:B3,D1:D5)")
print(len(union.areas), union.render())          # 2 A1:B3,D1:D5

intersection = parse_reference_expression("=(A1:C5 B2:D9)")
print(intersection.render())                     # B2:C5

left = MultiAreaReference.area("Sheet1!A1:B2")
right = MultiAreaReference.area("Sheet1!B2:C3")
print(left.union(right).render())                # Sheet1!A1:B2,Sheet1!B2:C3
print(left.intersection(right).render())         # Sheet1!B2

try:
    MultiAreaReference.area("A1:B2").intersection(MultiAreaReference.area("D1:E2"))
except NullIntersectionError as error:
    print(NullIntersectionError.error, error)    # #NULL! references do not overlap

Calculation semantics:

  • Union preserves source order and keeps repeated and overlapping areas. Excel functions consume each supplied area independently, including duplicates, so SUM((A1:A3,A1:A3)) double-counts by design.
  • Intersection pairs every left area with every right area and keeps each non-empty overlap, so an intersection may itself be multi-area.
  • Two areas on different sheets never intersect.
  • An empty intersection raises NullIntersectionError, whose class attribute error is the Excel spelling #NULL!.
  • MultiAreaReference values are frozen and reusable; a reference with zero areas raises ReferenceError.

ReferenceError is the ValueError base for a malformed expression: an invalid cell or range, a missing closing parenthesis, a non-reference operand, or a trailing token. NullIntersectionError subclasses it.

Not supported: 3-D references spanning a sheet range, whole-row or whole-column shorthand, and structured table references.

Locale-aware formula input

Added in 2.1.0

Import from wolfxl.formula.locale. normalize_formula converts localized input into invariant OOXML/English syntax before it is assigned to a workbook. It has no side effects and never touches a workbook.

LocaleProfile defaults are the invariant profile: decimal_separator=".", function_argument_separator=",", array_column_separator=",", array_row_separator=";", and empty localized_booleans, localized_errors, and function_aliases mappings. LocaleProfile.invariant() returns it.

from wolfxl.formula.locale import (
    FormulaNormalizationError,
    InvalidLocaleProfile,
    LocaleProfile,
    normalize_formula,
)

en_us = LocaleProfile.invariant()
print(normalize_formula("=SUM(A1:A3,1.5)", en_us))     # =SUM(A1:A3,1.5)

de_de = LocaleProfile(
    decimal_separator=",",
    function_argument_separator=";",
    array_column_separator="\\",
    array_row_separator=";",
    localized_booleans={"WAHR": "TRUE", "FALSCH": "FALSE"},
    localized_errors={"#WERT!": "#VALUE!"},
    function_aliases={"SUMME": "SUM", "WENN": "IF"},
)
print(normalize_formula("=SUMME(A1:A3;1,5)", de_de))    # =SUM(A1:A3,1.5)
print(normalize_formula("=WENN(WAHR;{1\\2;3\\4};#WERT!)", de_de))
# =IF(TRUE,{1,2;3,4},#VALUE!)

OOXML normalization rules:

  • The decimal separator becomes . only between digits; elsewhere a , decimal separator is valid solely as a union operator inside a grouping parenthesis.
  • The function argument separator becomes ,.
  • The array column separator becomes , and the array row separator becomes ;, resolved per delimiter context, so a profile that reuses ; for function arguments and array rows stays unambiguous.
  • Boolean and error aliases are matched case-insensitively and replaced with TRUE, FALSE, or the invariant Excel error code.
  • A function alias is applied only to an identifier immediately followed by (. Sheet-qualified identifiers before !, quoted strings, quoted sheet names, and bracketed structured references are preserved byte-for-byte. Unqualified identifiers matching configured boolean or error aliases are normalized even when a caller intended them as defined names; localized defined names are outside this API's contract.
  • The normalized result is re-tokenized before it is returned, so a profile cannot emit syntax the tokenizer rejects.

Validation and errors:

  • InvalidLocaleProfile (a FormulaNormalizationError, itself a ValueError) is raised at profile construction for a decimal separator other than . or ,, a grammar separator outside , ; \ |, a grammar separator equal to the decimal separator, identical array column and row separators, a boolean target other than TRUE/FALSE, or an error target outside #NULL!, #DIV/0!, #VALUE!, #REF!, #NAME?, #NUM!, #N/A, #GETTING_DATA.
  • FormulaNormalizationError is raised for input not starting with =, a separator used in the wrong context, a mismatched or unclosed delimiter, an unterminated string, an unterminated bracketed reference, and any residual tokenizer failure.

Not supported: localized number formats, localized defined names, and localized cell reference styles such as R1C1.

Native pivot filters

Added in 2.1.0

Import from wolfxl.pivot.filters (also re-exported from wolfxl.pivot). Every specification is a frozen dataclass; the public collection is an immutable tuple.

PivotFilterSpec is the union of LabelFilter, ValueFilter, TopNFilter, and DateFilter. Each specification names exactly one field, and the specifications are applied in the order given, each narrowing the record set the previous one selected.

from datetime import date

from wolfxl.pivot.filters import (
    DateFilter,
    DatePeriod,
    DatePeriodKind,
    DateRange,
    LabelFilter,
    LabelOperator,
    NumericOperator,
    TopDirection,
    TopMode,
    TopNFilter,
    ValueFilter,
)

label = LabelFilter(field="region", operator=LabelOperator.BEGINS_WITH, value="no")
print(label.to_rust_dict())
# {'kind': 'label', 'field': 'region', 'operator': 'begins_with',
#  'value': 'no', 'case_sensitive': False}

value = ValueFilter(
    field="region",
    operator=NumericOperator.BETWEEN,
    value=10.0,
    upper_value=20.0,
    measure="amount",
)
top = TopNFilter(field="region", value=3, mode=TopMode.ITEMS, direction=TopDirection.TOP)
window = DateFilter(
    field="closed",
    selection=DateRange(start=date(2026, 1, 1), end=date(2026, 3, 31)),
)
quarter = DateFilter(
    field="closed",
    selection=DatePeriod(kind=DatePeriodKind.QUARTER, year=2026, quarter=1),
)

# Public specs are immutable.
try:
    label.value = "other"
except Exception as error:
    print(type(error).__name__)                # FrozenInstanceError

Wire the specifications into a table with PivotTable(..., filters=(label, top)). PivotTable.filters returns the immutable tuple; assigning it validates that every element is a PivotFilterSpec and raises RuntimeError once the layout has been materialized. Passing a non-spec element raises TypeError. A filter naming a field absent from the cache raises ValueError (PivotTable filter does not match a cache field), and a measure name that matches no data field, or more than one, raises ValueError.

Operators and defaults

LabelOperator: EQUALS, NOT_EQUALS, CONTAINS, NOT_CONTAINS, BEGINS_WITH, NOT_BEGINS_WITH, ENDS_WITH, NOT_ENDS_WITH, BLANK, NOT_BLANK. BLANK and NOT_BLANK take no operand and reject one with ValueError; every other operator requires a string value and raises TypeError without it. case_sensitive defaults to False and folding is locale-independent.

NumericOperator: EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, BETWEEN, NOT_BETWEEN. measure defaults to 0, the first data field, and accepts a non-negative index or a data-field name. upper_value is required for the two range operators, rejected for the others, and must not be below value. include_lower and include_upper both default to True. Non-finite numbers raise ValueError.

TopNFilter defaults to direction=TopDirection.TOP and mode=TopMode.ITEMS with measure=0. ITEMS selects value ranked items and requires an integral value; PERCENT selects ceil(value / 100 * item_count) ranked items and requires 0 <= value <= 100; SUM selects ranked items through the first cumulative aggregate that reaches value. value must be non-negative. Ties at the cutoff are retained.

DateRange requires plain datetime.date endpoints with start <= end and defaults both include_start and include_end to True. DatePeriod is matched without consulting the wall clock: DatePeriodKind.YEAR takes only year (1 through 9999), QUARTER requires quarter in 1 through 4, MONTH requires month in 1 through 12, and DAY requires month and day forming a valid calendar date. Any other combination raises ValueError.

Not supported: filtering a cache field backed by an external hierarchy, and native filter recompute in a build without the pivot recompute extension. In both cases the table raises rather than silently returning unfiltered records.

Loaded-workbook render behavior

Added in 2.1.0

Import from wolfxl.render. RenderService routes every request through the public Workbook.render_* methods, so direct and service callers share one validation boundary.

import io

from wolfxl import Workbook, load_workbook
from wolfxl.render import RenderOptions, RenderService

workbook = Workbook()
sheet = workbook.active
sheet["A1"] = "hello"
sheet.freeze_panes = "B2"
sheet.print_title_rows = "1:1"
sheet.row_dimensions[2].hidden = True
buffer = io.BytesIO()
workbook.save(buffer)

loaded = load_workbook(io.BytesIO(buffer.getvalue()))
service = RenderService(workbook=loaded)

report = service.render(RenderOptions(format="png", sheet="Sheet"))
print(type(report.result).__name__)                     # bytes
print(report.diagnostics.selector)                      # sheet
print(report.diagnostics.rendered_sheet_count)          # 1
print(report.diagnostics.sheet_titles)                  # ('Sheet',)
print(report.diagnostics.unsupported_features)          # ()

paginated = service.render(
    RenderOptions(format="pdf", sheet="Sheet", paginated=True, pages=[1])
)
print(paginated.diagnostics.page_count)                 # 1

Supported loaded render behavior:

  • Loaded conditional formatting is applied to the rendered cells: cellIs, expression, containsText and its negations, containsBlanks and notContainsBlanks, containsErrors and notContainsErrors, duplicateValues and uniqueValues, top10 by rank or percent, aboveAverage, two- and three-stop colorScale, dataBar, and iconSet.
  • Conditional expression rules and formula-backed cellIs, color-scale, data-bar, and icon-set thresholds use the native calculation engine over the loaded worksheet snapshot. This includes relative and absolute A1 anchoring, rectangular ranges, and the engine's registered function library.
  • x14 extension metadata for dataBar and iconSet rules is read, including x14:cfIcon custom-icon references. A custom icon resolves only when it names a built-in icon set and a valid icon index within it.
  • Hidden rows and hidden columns collapse to zero height and zero width, so they contribute no pixels and shift their neighbors.
  • Freeze and split panes contribute pane offsets to the worksheet canvas layout, and the canvas extent grows so a pane pointing past the last populated cell still lays out the intervening default rows and columns.
  • A right-to-left sheet view mirrors the worksheet canvas horizontally: cell rectangles, strokes, images, glyph-run origins, and chart anchors are all reflected across the canvas width.
  • Paginated output repeats print-title rows and columns on each page without duplicating them on the page where they naturally appear. Titles that do not start at row 1 or column A begin repeating only after their natural page. Paginated routes ignore freeze and split viewports.

Loud unsupported boundaries. These raise instead of silently omitting content:

  • A loaded conditional-format rule the renderer cannot represent raises ValueError with the message unsupported loaded conditional format: <code>. Stable codes include conditional_format_time_period, conditional_format_custom_icon_payload, conditional_format_data_bar_border, conditional_format_data_bar_color_role, conditional_format_unknown_rule, and conditional_format_<rule type> for any other rule type.
  • A colorScale with a stop count outside two or three, a dataBar or iconSet over non-numeric cells, a rule referencing a missing differential style, a non-solid or RGB-less differential fill, and an invalid target sqref all raise RuntimeError carrying the native unsupported: or invalid input: detail.
  • In a right-to-left sheet, rotated cell text raises: mirroring cell glyph runs never allows rotation. Loaded drawing-shape text keeps its verified rotation, but a loaded right-to-left shape using line rotation, form-control rotation, a grouped child path gradient, a grouped gradient combined with text, a grouped child gradient with non-uniform scaling or non-cardinal rotation, or a flip the renderer cannot represent raises ValueError naming the shape and the unsupported detail.
  • A manual page break inside a print-title band, a print-title rows value that is not a whole-row range, and a print-title columns value that is not a whole-column range are all refused.
  • A loaded workbook in any format other than xlsx raises NotImplementedError.
  • A wheel built without the render Cargo feature raises RuntimeError naming the required build flag.

RenderDiagnostics.unsupported_features is reserved for sheet-level visual content that the renderer intentionally omits, deduplicated and sorted across every rendered sheet. Font codes are font_family_fallback, font_substitution_applied, font_family_missing, font_style_fallback, font_file_unreadable, and font_system_discovery. A chart render reports no sheet-level codes. A build that cannot inspect a worksheet reports a backend_unavailable diagnostic rather than an empty tuple that would read as "nothing was omitted".

Bounded DataModel evaluation

Added in 2.1.0

Import from wolfxl.datamodel.engine. Evaluation is offline, in-memory, and disabled by default.

ModelEvaluationPolicy() is default-deny: enabled=False with every bound at zero. A disabled policy carrying a non-zero bound raises ValueError, and an enabled policy missing any positive bound raises ValueError (enabled model evaluation requires positive finite bounds). ModelEvaluationPolicy.bounded is the only convenience constructor that enables evaluation, and it requires max_recursion_depth, timeout_seconds, and max_memory_bytes together, with an optional CancellationToken. The policy is unrelated to refresh or network permissions.

from wolfxl.datamodel.engine import (
    ColumnReference,
    DataModel,
    Measure,
    ModelEvaluationPolicy,
    ModelEvaluationUnavailableError,
    Table,
)

sales = Table.from_rows(
    "Sales",
    ("region", "amount"),
    [{"region": "north", "amount": 10.0}, {"region": "south", "amount": 5.0}],
)
model = DataModel(
    tables=(sales,),
    measures=(
        Measure(name="Total", expression="SUM('Sales'[amount])"),
        Measure(name="North", expression="CALCULATE([Total], 'Sales'[region] = \"north\")"),
    ),
)

print(model.measure_dependencies("North"))          # ('Total',)

# Default policy refuses before parsing or inspecting the measure.
try:
    model.evaluate_measure("Total")
except ModelEvaluationUnavailableError as refusal:
    print(refusal)                                  # embedded model evaluation is disabled

policy = ModelEvaluationPolicy.bounded(
    max_recursion_depth=8,
    timeout_seconds=1.0,
    max_memory_bytes=1 << 20,
)
print(model.evaluate_measure("Total", policy))       # 15.0
print(model.evaluate_measure("North", policy))       # 10.0
print(model.cube_value("Total", {ColumnReference("Sales", "region"): "south"}, policy))
# 5.0

DataModel.evaluate_measure(name, policy=ModelEvaluationPolicy()) is the public measure evaluator; the equivalent Rust entry point is DataModel::evaluate_measure in wolfxl-model, whose internal eval_measure recursion is not part of the public surface. DataModel.cube_value(measure, filters, policy) performs a deterministic CUBE-style lookup with column-qualified equality filters. DataModel.measure_dependencies(name) resolves transitive references in sorted name order without evaluating anything. It uses the same fixed internal analysis limits as public DAX parsing: a recursion depth of 128, five seconds, and 1 MiB of tracked memory. These limits are independent of ModelEvaluationPolicy and return ModelEvaluationLimitError rather than exhausting the host.

Model metadata is immutable: Table, Relationship, Measure, ColumnReference, and DataModel are frozen. A Relationship propagates a filter one way, from from_column to to_column.

The DAX subset is intentionally small: numbers, measure references, column references, SUM, arithmetic, and CALCULATE with a single equality filter. tokenize_dax and parse_dax expose that grammar without evaluating it; both use the fixed internal analysis limits above.

Redacted error types, all subclasses of ModelEvaluationError (a RuntimeError). None of their messages contain a formula, a row value, a package byte, or a credential:

Error Meaning
ModelEvaluationUnavailableError evaluation refused before parsing or inspection
ModelEvaluationLimitError a finite recursion, time, or memory bound was reached
ModelEvaluationCycleError a measure dependency cycle was detected
ModelEvaluationCancelledError a caller-owned cancellation token stopped evaluation
ModelExpressionError the restricted DAX subset cannot evaluate the expression
ModelMetadataError model metadata is inconsistent

Not supported: reading an embedded Power Pivot model out of a workbook package, refreshing a model, non-equality CALCULATE filters, and aggregate functions beyond SUM.