Skip to content

Policy, Locale, Pivot, and Render APIs

Edition: Commercial

Every symbol on this page is part of the Commercial edition (authenticated package index, latest published release 2.3.0) and is Added in 2.1.0. Community (MIT, public PyPI, 2.0.9) does not include these APIs. See Install for the difference between the editions.

These nine 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_pro.security, wolfxl_pro.external, and wolfxl_pro.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_pro.security.active_content (also re-exported from wolfxl_pro.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_pro.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_pro.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.

Sanitization: explicit removal with a residual report

Added in 2.1.0

Import inspect_sanitization and sanitize_workbook from wolfxl.operations (also served by wolfxl-ops sanitize-inspect and wolfxl-ops sanitize).

Sanitization is an allowlist, never a default. inspect_sanitization(request, *, source) reports what a source contains and what may be requested for it, and writes nothing. sanitize_workbook(request, *, source, destination) removes exactly the categories the caller's own request names and publishes one artifact into a destination directory that must not exist. The artifact is named and typed from the sanitized package itself: sanitized.xlsx, sanitized.xlsm, sanitized.xltx, or sanitized.xltm, whichever the published bytes' workbook main-part content type declares, so a package whose macros were retained is never published as a macro-free .xlsx. The source path and the destination directory are arguments, so no request names an output path and no request carries an authority bit.

from wolfxl.operations import inspect_sanitization, sanitize_workbook

inventory = inspect_sanitization(
    {"schema_version": 1, "source_sha256": digest},
    source="source.xlsm",
)
print(inventory["removable_kinds"])
# ['comment', 'embedded_active_content', 'external_link', 'hidden_sheet', 'macro', 'personal_metadata']
print(inventory["outer_opc_signature"], inventory["inner_vba_signature"])
# False False

result = sanitize_workbook(
    {"schema_version": 1, "source_sha256": digest, "removals": ["macro", "hidden_sheet"]},
    source="source.xlsm",
    destination="sanitized",
)
print(result["removals"])
# ['macro', 'hidden_sheet']
print(result["outputs"][0]["relative_path"], result["outputs"][0]["sha256"] == published_sha256)
# sanitized.xlsx True

Request fields

Field sanitize-inspect sanitize
schema_version required, must be 1 required, must be 1
source_sha256 required, whole-source digest required, whole-source digest
removals refused by name required, non-empty list of SanitizationKind values

removals is the caller's own authorization, so it is never defaulted and never inferred: an absent, empty, repeated, non-string, or unrecognized entry refuses with invalid_request, and the accepted selection is reported order-normalized in removals. SanitizationKind members are macro, external_link, comment, hidden_sheet, personal_metadata, and embedded_active_content; a requested category the package does not contain removes nothing and is not an error.

Result fields

Every call returns one canonical JSON object with schema_version, command, status, and:

  • source_sha256: the digest the request was verified against.
  • removable_kinds, outer_opc_signature, inner_vba_signature, report (sanitize-inspect). The inventory is built from exactly the admitted bytes and removes nothing, so its report is the retention view of the package.
  • removals, outputs, report (sanitize). outputs is the established publication inventory — one entry per artifact with relative_path, sha256, media_type, and size_bytes. The one artifact's relative_path suffix and media_type are derived from the sanitized package's workbook main-part content type, never from the source file name or its extension.

The report is the existing count-only redacted report. Recognized categories carry the disposition the caller's allowlist selected, and every residual category that was not removed carries its own retained entry: media payloads, printer-settings and revision metadata, opaque non-XML payloads, cell data and hidden rows or columns inside retained worksheets, any signature scope, and orphan relationship parts. Entries are counts of package channels only; no part name, relationship target, host path, media type value, or cell value ever appears, and no entry is a statement that the output is clean.

Refusals

Condition Category Result
Parse or digest field is invalid invalid_request Nothing is read or written
Source digest does not match the request source_changed No output
Source path changed during the transform source_changed No output
Destination exists, or resolves to the source output_collision Existing destination preserved
Source is unreadable, malformed, or over the admission limits cannot_scan No output
A requested removal would invalidate an outer OOXML signature policy_rejected No output
The sanitized package's workbook type is outside the admitted vocabulary unsupported_feature No output
The workspace is no longer the directory this call created apply_failed Replacement preserved, no output

The source is read once, bounded by the package admission limits above, and the admitted bytes are what the native inventory or the private snapshot handed to the transform contains, so a concurrent writer cannot change what is inspected or removed. The original path is hashed again immediately before the no-replace rename, so a source whose bytes differ at that check refuses with source_changed. That check observes the path at that instant; it does not promise the pathname stays unchanged up to the rename, and this operation makes no such claim.

The verified bytes are staged in a private workspace whose device and inode are recorded at creation, and cleanup removes that workspace only while the name still holds the directory this call created, without following a symbolic link. A workspace that was renamed away or rebound, or replaced by a file or a symbolic link, is preserved and the operation refuses before publication. The artifact's name and media type are read from the staged sanitized package, so they describe the published bytes; a workbook type outside the admitted vocabulary refuses with no output rather than publishing under a guessed name.

Both commands require paths the caller controls: the source path and the destination's parent directory must not have a concurrent same-UID writer. The workspace-identity and final-source checks detect a change made by such a writer and refuse; they are not designed to defeat one, and no blanket guarantee is offered for a namespace another process can mutate.

Relationship to the lower-level transform

sanitize_ooxml(source, target) in wolfxl_pro.security.sanitize keeps its own contract: it writes to a target path and replaces that path when it exists, for callers that manage their own publication. sanitize_workbook never points it at a caller path. It gives the level below a private workspace and publishes the verified output itself, which is what makes the destination directory no-replace, the source re-checked at publication, and the operation's refusals byte-safe. Use the operation when a filesystem destination is part of the task, and the lower-level function when you own the target and its replacement.

Not implemented in the sanitization surface: removing outer OOXML signature material (strip it through the existing save path with set_package_signature_policy("remove") before requesting removals), reading or verifying any signature, certificate, signer, or trust chain, and removing hidden rows, hidden columns, retained media, opaque payloads, printer-settings or revision metadata, or cell data inside a retained worksheet. Those are reported as retained counts instead of being claimed.

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 and formatting

Added in 2.1.0

Import from wolfxl.formula.locale. normalize_formula converts localized input into invariant OOXML/English syntax, while format_formula presents an invariant formula with a caller-supplied profile. Both are pure and never touch 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.

import wolfxl

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

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

fr_fr = LocaleProfile(
    decimal_separator=",",
    function_argument_separator=";",
    array_column_separator="\\",
    array_row_separator=";",
    localized_booleans={"VRAI": "TRUE", "FAUX": "FALSE"},
    localized_errors={"#VALEUR!": "#VALUE!"},
    function_aliases={"SOMME": "SUM"},
)
canonical = normalize_formula("=SOMME(A1:A3;1,5)", fr_fr)
print(canonical)                         # =SUM(A1:A3,1.5)
print(format_formula(canonical, fr_fr))  # =SOMME(A1:A3;1,5)

workbook = wolfxl.Workbook()
cell = workbook.active["A1"]
cell.set_formula_local("=SOMME(A1:A3;1,5)", fr_fr)
assert cell.value == "=SUM(A1:A3,1.5)"
assert cell.get_formula_local(fr_fr) == "=SOMME(A1:A3;1,5)"
workbook.close()

OOXML conversion rules: - normalize_formula changes the decimal separator to . only between digits. format_formula changes canonical decimal points back to the profile's decimal separator. - normalize_formula changes the function argument separator to ,; format_formula changes canonical function separators to the profile's separator. - Array column and row separators are resolved per delimiter context, so a profile that reuses ; for function arguments and array rows stays unambiguous. Invariant grouping and top-level range unions remain ,. - Boolean and error aliases are matched case-insensitively. Normalization replaces them with TRUE, FALSE, or the invariant Excel error code; formatting uses a configured localized spelling only when that canonical target has exactly one alias. Unknown and ambiguous aliases remain invariant. - 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. - normalize_formula re-tokenizes its canonical result. format_formula validates its canonical input before formatting. - Cell.set_formula_local(formula, profile) and Cell.get_formula_local(profile) make localization explicit. Normal Cell.value assignment and reads remain invariant, and no profile is auto-detected or persisted in OOXML.

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 formula input not starting with =, a separator used in the wrong context, a mismatched or unclosed delimiter, an unterminated string, an unterminated bracketed reference, or any residual tokenizer failure. Cell.get_formula_local also raises it when the cell does not hold a scalar formula.

Not supported: localized number formats, localized defined names, localized cell reference styles such as R1C1, or implicit locale detection.

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_pro.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_pro.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.