"""Strict records describing workbook plans produced from registry snapshots.
Every record is a frozen pydantic v2 model with `extra="forbid"` so that
schema drift surfaces as validation failures at the moment the engine
assembles the plan rather than as silent payload divergence at the
renderer boundary. The records are intentionally narrow: the engine produces
them, the Google apply adapter and offline XLSX materializer consume them, and
the pull/parity adapters compare incoming workbook cell values back against the
same plan.
A1 addressing
-------------
Workbook cell addresses are expressed by the `SheetCellAddress` record
which carries the human-readable A1 string plus the structured tab
name, row index, and column index. Row and column indices are 1-based
to match Sheets/openpyxl convention. The `a1` string is recomputed from the
tab + row + column at construction time, so callers never hand-roll
A1 strings — they always go through this record.
See Also:
:class:`aeat.domain.calculations.registry.RegistrySnapshot`
Registry snapshot compiled into these records by the engine.
:class:`SheetEvidenceFacet`
Evidence facet carried by the plan and rendered by both workbook
export paths.
"""
from __future__ import annotations
import re
from collections.abc import Mapping
from datetime import date, datetime
from decimal import Decimal
from enum import StrEnum
from typing import Literal
from pydantic import BaseModel, Field, field_serializer, model_validator
from ....core import STRICT_FROZEN_CONFIG as _STRICT_FROZEN
from ....core import Period
from ....core.time import now as _utc_now
from ....core.time import validate_utc_aware
from ....domain.calculations.registry import (
BindingId,
CasillaId,
FormulaId,
LegalRefId,
ModeloId,
ParameterId,
RelationId,
RevisionId,
SourceRefId,
)
from ....domain.calculations.registry import DecimalValue as _RegistryDecimalValue
from ._errors import CalcSheetsRecordError
from ._theme import WORKBOOK_FONT_FAMILY, StyleRole
# `DecimalValue` is the registry's annotated `Decimal` with a
# BeforeValidator that coerces int / str inputs through `Decimal(...)`.
# That coercion is appropriate inside registry TOML loading but
# breaks `Decimal | str | bool | None` unions where the validator
# would eagerly try to parse a label string as a Decimal. The
# engine therefore uses plain `Decimal` for fields that mix value
# types, and the imported `_RegistryDecimalValue` only where the
# field is unambiguously a Decimal.
DecimalValue = _RegistryDecimalValue
[docs]
class TabName(StrEnum):
"""The tabs the engine emits in every workbook.
The set is fixed so that the pull adapter and the parity oracle
have a stable layout to anchor against. New layers (for example a
future "Pagos" tab for ingreso/devolución timings) are added by
extending this enumeration; freeform tab names are rejected.
"""
ENTRADAS = "Entradas"
CALCULOS = "Cálculos"
PROVENANCE = "Procedencia"
TARIFFS = "Tarifas"
DETALLE = "Detalle"
EVIDENCIA = "Evidencia"
GUIDE = "Guía"
_A1_COLUMN = re.compile(r"^[A-Z]{1,3}$")
def _column_index_to_letters(column: int) -> str:
"""Translate a 1-based column index to an A1 column letter string.
Converts a positive integer column index (1-based, matching Google
Sheets' convention) to the corresponding letter sequence used in A1
notation: 1 → ``"A"``, 26 → ``"Z"``, 27 → ``"AA"``, 702 → ``"ZZ"``.
Args:
column: 1-based column index; must be ≥ 1.
Returns:
Upper-case letter string, e.g. ``"A"``, ``"B"``, ``"AA"``.
Raises:
``CalcSheetsRecordError``: if ``column`` is less than 1.
"""
if column < 1:
raise CalcSheetsRecordError(
"column index must be 1-based and positive",
translated_message="application.storage.calc_sheets.records.errors.invalid_column_index",
)
letters: list[str] = []
cursor = column
while cursor:
cursor, remainder = divmod(cursor - 1, 26)
letters.append(chr(ord("A") + remainder))
return "".join(reversed(letters))
def _column_letters_to_index(letters: str) -> int:
"""Translate an A1 column letter string to a 1-based column index.
Inverse of ``_column_index_to_letters``. Accepts one to three upper-case
ASCII letters: ``"A"`` → 1, ``"Z"`` → 26, ``"AA"`` → 27.
Args:
letters: Upper-case column identifier matching ``^[A-Z]{1,3}$``.
Returns:
1-based integer column index.
Raises:
``CalcSheetsRecordError``: if ``letters`` does not match the expected
pattern.
"""
if not _A1_COLUMN.match(letters):
raise CalcSheetsRecordError(
"invalid Sheets column letters",
context={"letters_length": len(letters)},
translated_message="application.storage.calc_sheets.records.errors.invalid_column_letters",
)
cursor = 0
for char in letters:
cursor = cursor * 26 + (ord(char) - ord("A") + 1)
return cursor
[docs]
class SheetCellAddress(BaseModel):
"""A single Sheets cell address, expressed as tab + row + column."""
model_config = _STRICT_FROZEN
tab: TabName
row: int = Field(ge=1)
column: int = Field(ge=1)
a1: str = Field(min_length=2)
@model_validator(mode="after")
def _a1_matches_row_column(self) -> SheetCellAddress:
letters = _column_index_to_letters(self.column)
expected = f"{letters}{self.row}"
if self.a1 != expected:
raise CalcSheetsRecordError(
"sheet cell A1 address does not match row and column",
context={"row": self.row, "column": self.column},
translated_message="application.storage.calc_sheets.records.errors.address_mismatch",
)
return self
[docs]
@classmethod
def at(cls, tab: TabName, row: int, column: int) -> SheetCellAddress:
"""Construct a ``SheetCellAddress`` from tab, row, and column indices.
Derives the ``a1`` string automatically so callers never hand-roll A1
notation.
Args:
tab: ``TabName`` enum member identifying the workbook tab.
row: 1-based row index.
column: 1-based column index.
Returns:
:class:`SheetCellAddress`: A validated ``SheetCellAddress`` instance.
"""
letters = _column_index_to_letters(column)
return cls(tab=tab, row=row, column=column, a1=f"{letters}{row}")
[docs]
def qualified(self) -> str:
"""Return the cross-tab A1 reference (`'Tab Name'!A1`)."""
# Sheets A1 syntax single-quotes the sheet name and escapes
# inner apostrophes by doubling them. The tab enum values do
# not contain apostrophes today, but the escape is applied
# unconditionally so a future tab rename cannot break syntax.
safe = self.tab.value.replace("'", "''")
return f"'{safe}'!{self.a1}"
[docs]
class SheetValueCell(BaseModel):
"""A literal value the engine writes verbatim to a cell.
Used for operator-input cells (with `value=None` indicating "blank,
awaiting operator entry"), for parameter mirror cells, and for the
structural section/header labels.
"""
model_config = _STRICT_FROZEN
address: SheetCellAddress
value: Decimal | str | bool | None = None
note: str | None = None
casilla_id: CasillaId | None = None
parameter: ParameterId | None = None
role: Literal["operator_input", "parameter_value", "label", "metadata"]
[docs]
class SheetCellConstraint(BaseModel):
"""A declarative value constraint surfaced to one Sheets cell.
Mirrors the registry's `CasillaConstraints` record into a Sheets
`setDataValidation` rule. The apply adapter renders this as a
`condition` block on the target cell so an operator who types an
out-of-range value sees Sheets's own validation banner reject it
in the workbook UI.
The constraint also propagates to the cell's `note` so the
operator sees the legal grounding ("LIRPF art. 56 — non-negative")
even before they attempt invalid input.
"""
model_config = _STRICT_FROZEN
address: SheetCellAddress
sign: Literal["any", "non_negative", "non_positive"] = "any"
min_value: Decimal | None = None
max_value: Decimal | None = None
legal_refs: tuple[LegalRefId, ...] = Field(min_length=1)
casilla_id: CasillaId
[docs]
class SheetRowSetColumn(BaseModel):
"""One column of a `SheetRowSet`, mapping a binding id to a header cell."""
model_config = _STRICT_FROZEN
binding: BindingId
header_address: SheetCellAddress
header_label: str = Field(min_length=1)
legal_refs: tuple[LegalRefId, ...] = Field(min_length=1)
[docs]
class SheetRowSet(BaseModel):
"""A repeating-row data block in the `Detalle` tab.
Mirrors the registry's row-producer binding pattern (bindings
declared with `aggregation = { op = "rows", grouping = "..." }`).
Each row in the workbook represents one operator-supplied detail
record (e.g., one perceptor on modelo 190, one VIES counterparty
on modelo 349, one foreign asset on modelo 720). Columns are
declared per row-producing binding sharing the same `grouping`.
The engine emits a header row carrying `header_label` for each
column. Operators add row data freely below; the apply adapter
leaves the data area unprotected. The pull adapter reads the
operator-supplied rows back into structured `RowSetEdit` records
keyed by binding id.
`legal_refs` and `source_refs` ground the row-set against AEAT's
diseño-de-registro authority that mandates the per-record
fields.
"""
model_config = _STRICT_FROZEN
grouping: str = Field(min_length=1)
tab: TabName
header_row: int = Field(ge=1)
first_data_row: int = Field(ge=2)
columns: tuple[SheetRowSetColumn, ...] = Field(min_length=1)
legal_refs: tuple[LegalRefId, ...] = Field(min_length=1)
source_refs: tuple[SourceRefId, ...] = Field(min_length=1)
[docs]
class SheetProtectedRange(BaseModel):
"""A contiguous range the apply adapter marks read-only.
`Cálculos`, `Procedencia`, and `Tarifas` are always fully
protected. The pull adapter inspects these to decide which cells
operators are allowed to mutate.
"""
model_config = _STRICT_FROZEN
tab: TabName
start_row: int = Field(ge=1)
end_row: int = Field(ge=1)
start_column: int = Field(ge=1)
end_column: int = Field(ge=1)
description: str
@model_validator(mode="after")
def _range_well_formed(self) -> SheetProtectedRange:
if self.end_row < self.start_row:
raise CalcSheetsRecordError(
"range end row must be on or after start row",
context={"range_kind": "protected"},
translated_message="application.storage.calc_sheets.records.errors.range_malformed",
)
if self.end_column < self.start_column:
raise CalcSheetsRecordError(
"range end column must be on or after start column",
context={"range_kind": "protected"},
translated_message="application.storage.calc_sheets.records.errors.range_malformed",
)
return self
[docs]
class SheetAnchor(BaseModel):
"""An explicit labelled start / final anchor on the calculation flow.
``start`` marks the opening of the operator-input region (Entradas); ``final``
marks the filing result (resultado / cuota) on Cálculos. Rendered as a
labelled cell in both transports so the inputs→resultado flow is
unambiguously oriented, mirroring the published workbook layout.
"""
model_config = _STRICT_FROZEN
address: SheetCellAddress
kind: Literal["start", "final"]
label: str = Field(min_length=1)
[docs]
class SheetStyledRange(BaseModel):
"""A contiguous range tagged with a presentation role.
The engine emits one styled range per role-region (the header band, each
section banner, the operator-input column, the computed column, the result
cell, wrapped body columns). Both transports resolve ``role`` to a concrete
fill / font / alignment through the shared ``_theme`` palette, so the offline
xls and online Sheets render the same look from the same declaration. Later
ranges win on overlap, so a narrow accent range (e.g. ``result``) may be
emitted after the broad column range it refines.
"""
model_config = _STRICT_FROZEN
tab: TabName
start_row: int = Field(ge=1)
end_row: int = Field(ge=1)
start_column: int = Field(ge=1)
end_column: int = Field(ge=1)
role: StyleRole
wrap: bool = False
@model_validator(mode="after")
def _range_well_formed(self) -> SheetStyledRange:
if self.end_row < self.start_row:
raise CalcSheetsRecordError(
"range end row must be on or after start row",
context={"range_kind": "styled"},
translated_message="application.storage.calc_sheets.records.errors.range_malformed",
)
if self.end_column < self.start_column:
raise CalcSheetsRecordError(
"range end column must be on or after start column",
context={"range_kind": "styled"},
translated_message="application.storage.calc_sheets.records.errors.range_malformed",
)
return self
[docs]
class SheetColumnWidth(BaseModel):
"""A per-tab column width in approximate character units.
The offline renderer applies it as an openpyxl ``column_dimensions`` width;
the online renderer converts it to a pixel size (``~7 px`` per character)
for ``updateDimensionProperties``. Sized so concept labels and legal-ref
columns read without clipping.
"""
model_config = _STRICT_FROZEN
tab: TabName
column: int = Field(ge=1)
width: int = Field(ge=1, le=255)
[docs]
class SheetFrozenView(BaseModel):
"""Per-tab frozen header rows / leading columns.
Freezes the column-title row (and, where useful, the leading label columns)
so the header stays visible while an operator scrolls a long modelo.
"""
model_config = _STRICT_FROZEN
tab: TabName
frozen_rows: int = Field(ge=0, default=0)
frozen_columns: int = Field(ge=0, default=0)
@model_validator(mode="after")
def _at_least_one(self) -> SheetFrozenView:
if self.frozen_rows == 0 and self.frozen_columns == 0:
raise CalcSheetsRecordError(
"a frozen view must freeze at least one row or column",
translated_message="application.storage.calc_sheets.records.errors.frozen_view_empty",
)
return self
[docs]
class SheetAutoFilter(BaseModel):
"""A per-tab basic-filter range over the header row plus its data rows.
Lets the operator sort / filter the tab by section or concept. Offline maps
it to ``worksheet.auto_filter.ref``; online to ``setBasicFilter``.
"""
model_config = _STRICT_FROZEN
tab: TabName
start_row: int = Field(ge=1)
end_row: int = Field(ge=1)
start_column: int = Field(ge=1)
end_column: int = Field(ge=1)
@model_validator(mode="after")
def _range_well_formed(self) -> SheetAutoFilter:
if self.end_row < self.start_row:
raise CalcSheetsRecordError(
"range end row must be on or after start row",
context={"range_kind": "auto_filter"},
translated_message="application.storage.calc_sheets.records.errors.range_malformed",
)
if self.end_column < self.start_column:
raise CalcSheetsRecordError(
"range end column must be on or after start column",
context={"range_kind": "auto_filter"},
translated_message="application.storage.calc_sheets.records.errors.range_malformed",
)
return self
[docs]
class SheetProvenanceRow(BaseModel):
"""One row of the `Procedencia` audit tab.
Each computed casilla emits a row recording the formula id, the
rounding rule, the legal references, and the workbook cell that
holds the value. Operators reading the workbook can audit every
figure back to a registry source.
"""
model_config = _STRICT_FROZEN
casilla_id: CasillaId
display_number: str
casilla_label: str
formula_id: FormulaId | None = None
rounding_rule: Literal["money", "integer", "none"]
legal_refs: tuple[LegalRefId, ...] = Field(min_length=1)
source_refs: tuple[SourceRefId, ...] = Field(min_length=1)
target_address: SheetCellAddress
[docs]
class SheetEvidenceContributorRow(BaseModel):
"""One ledger contributor rendered into the workbook evidence surface."""
model_config = _STRICT_FROZEN
casilla_id: CasillaId
transaction_id: str = Field(min_length=1)
amount: Decimal
currency: str = Field(min_length=1)
taxable_base: Decimal | None = None
iva_rate: Decimal | None = None
iva_amount: Decimal | None = None
counterparty: str | None = None
attachment_ids: tuple[str, ...] = ()
document_link_ids: tuple[str, ...] = ()
legal_refs: tuple[LegalRefId, ...] = Field(min_length=1)
source_refs: tuple[SourceRefId, ...] = Field(min_length=1)
[docs]
class SheetEvidenceManualEntry(BaseModel):
"""One non-ledger fact basis entry rendered into the evidence surface."""
model_config = _STRICT_FROZEN
casilla_id: CasillaId
value: str = Field(min_length=1)
kind: str = Field(min_length=1)
note: str = ""
legal_refs: tuple[LegalRefId, ...] = Field(min_length=1)
source_refs: tuple[SourceRefId, ...] = Field(min_length=1)
[docs]
class SheetEvidenceFacet(BaseModel):
"""Evidence rows attached to a workbook export plan.
Contributor rows carry ledger-derived transaction facts by casilla; manual
entries carry non-ledger fact basis values. The offline serializer writes
this facet to both the Evidencia worksheet and the adjacent JSON sidecar.
"""
model_config = _STRICT_FROZEN
snapshot_fingerprint: str | None = Field(default=None, min_length=64, max_length=64)
contributor_rows: tuple[SheetEvidenceContributorRow, ...] = ()
manual_entries: tuple[SheetEvidenceManualEntry, ...] = ()
[docs]
class SheetTariffTableRow(BaseModel):
"""One row of a parameter bracket table mirrored to the `Tarifas` tab."""
model_config = _STRICT_FROZEN
lower_bound: DecimalValue
upper_bound: DecimalValue | None = None
fixed_addition: DecimalValue
marginal_rate: DecimalValue
valid_from: date
valid_to: date | None = None
[docs]
class SheetTariffTable(BaseModel):
"""A parameter mirrored into the workbook as a lookup table.
Two flavours are supported: scalar dated values and bracket tables
(`bracket_table` parameters). The engine emits one of the two
depending on the parameter's `data_type`.
"""
model_config = _STRICT_FROZEN
parameter: ParameterId
label: str
data_type: Literal["decimal", "money", "integer", "ratio", "bracket_table"]
anchor: SheetCellAddress
scalar_value: DecimalValue | None = None
bracket_rows: tuple[SheetTariffTableRow, ...] = ()
@model_validator(mode="after")
def _shape_well_formed(self) -> SheetTariffTable:
if self.data_type == "bracket_table":
if not self.bracket_rows:
raise CalcSheetsRecordError(
"tariff table shape is invalid",
context={"data_type": self.data_type, "reason": "missing_bracket_rows"},
translated_message="application.storage.calc_sheets.records.errors.tariff_shape_invalid",
)
if self.scalar_value is not None:
raise CalcSheetsRecordError(
"tariff table shape is invalid",
context={"data_type": self.data_type, "reason": "unexpected_scalar_value"},
translated_message="application.storage.calc_sheets.records.errors.tariff_shape_invalid",
)
else:
if self.scalar_value is None:
raise CalcSheetsRecordError(
"tariff table shape is invalid",
context={"data_type": self.data_type, "reason": "missing_scalar_value"},
translated_message="application.storage.calc_sheets.records.errors.tariff_shape_invalid",
)
if self.bracket_rows:
raise CalcSheetsRecordError(
"tariff table shape is invalid",
context={"data_type": self.data_type, "reason": "unexpected_bracket_rows"},
translated_message="application.storage.calc_sheets.records.errors.tariff_shape_invalid",
)
return self
[docs]
class ParameterCell(BaseModel):
"""Pointer from a parameter id to its anchor cell in the `Tarifas` tab.
The translator consults this mapping when an expression references
a parameter, so that the emitted Sheets formula reads from the
mirrored value instead of inlining a literal.
"""
model_config = _STRICT_FROZEN
parameter: ParameterId
anchor: SheetCellAddress
[docs]
class RelationValue(BaseModel):
"""One pre-resolved cross-revision relation value.
Relations aggregate values from a different modelo's filings
(typically rolling quarterly filings up into an annual summary).
The local side resolves them through
`resolve_relation_values_from_observations` and supplies the
result here; the engine mirrors it as a scalar cell in `Tarifas`
so the workbook's formulas can consume it.
`provenance` carries the source tier the value came from:
`local_filing` (operator's own prior filing in
`SecureObjectRepository`), `aeat_live` (remote AEAT Sede
justificante parse), `operator_manual` (operator entered the
value into Sheets directly). The engine stamps the provenance
on the workbook so the pull adapter can detect stale prefills.
The `source_*` and ref fields mirror the registry relation so the
scalar workbook value remains joinable back to its official source
modelo, source casilla, and legal grounding.
"""
model_config = _STRICT_FROZEN
relation: RelationId
value: Decimal | None = None
provenance: Literal["local_filing", "aeat_live", "operator_manual"] = "operator_manual"
source_modelo: ModeloId | None = None
source_filing_year: int | None = Field(default=None, ge=2000, le=2099)
source_periods: tuple[str, ...] = ()
source_casilla_ids: tuple[CasillaId, ...] = ()
legal_refs: tuple[LegalRefId, ...] = Field(min_length=1)
source_refs: tuple[SourceRefId, ...] = Field(min_length=1)
resolved_at: datetime | None = None
note: str | None = None
[docs]
class RelationValues(BaseModel):
"""Caller-supplied relation aggregations for the `Tarifas` tab."""
model_config = _STRICT_FROZEN
values: tuple[RelationValue, ...] = ()
[docs]
def by_relation(self) -> Mapping[RelationId, RelationValue]:
"""Return a ``RelationId`` → ``RelationValue`` lookup mapping.
Returns:
Mapping[RelationId, :class:`RelationValue`]: A ``Mapping`` keyed by relation id
for fast lookup when the engine resolves cross-revision formula references.
"""
return {item.relation: item for item in self.values}
[docs]
class SheetGuideContent(BaseModel):
"""Plain-text content for the `Guía` tab.
The guide tab is the human-readable preamble: how to use the
workbook, what each tab contains, and where to find the
bidirectional sync command. Strings are caller-supplied (the CLI
surface resolves them through `tr()` so locale parity holds).
"""
model_config = _STRICT_FROZEN
title: str = Field(min_length=1)
paragraphs: tuple[str, ...] = Field(min_length=1)
[docs]
class SheetExportPlan(BaseModel):
"""Complete description of the workbook every renderer will write.
The plan is the shared contract between the registry-backed engine, Google
Sheets apply adapter, offline XLSX materializer, pull adapter, and parity
harness. It includes calculation cells, protected ranges, display facets,
registry metadata, relation provenance, row sets, and workbook evidence.
"""
model_config = _STRICT_FROZEN
metadata: SheetExportMetadata
value_cells: tuple[SheetValueCell, ...] = ()
formula_cells: tuple[SheetFormulaCell, ...] = ()
tariffs: tuple[SheetTariffTable, ...] = ()
provenance: tuple[SheetProvenanceRow, ...] = ()
protected_ranges: tuple[SheetProtectedRange, ...] = ()
number_formats: tuple[SheetNumberFormat, ...] = ()
section_headers: tuple[SheetSectionHeader, ...] = ()
anchors: tuple[SheetAnchor, ...] = ()
cell_constraints: tuple[SheetCellConstraint, ...] = ()
row_sets: tuple[SheetRowSet, ...] = ()
relation_provenance: RelationValues | None = None
evidence: SheetEvidenceFacet = Field(default_factory=SheetEvidenceFacet)
font_family: str = Field(default=WORKBOOK_FONT_FAMILY, min_length=1)
styled_ranges: tuple[SheetStyledRange, ...] = ()
column_widths: tuple[SheetColumnWidth, ...] = ()
frozen_views: tuple[SheetFrozenView, ...] = ()
auto_filters: tuple[SheetAutoFilter, ...] = ()
guide: SheetGuideContent
@model_validator(mode="after")
def _writable_cells_are_unique(self) -> SheetExportPlan:
seen: set[tuple[TabName, int, int]] = set()
duplicate_count = 0
for address in self.all_addresses():
key = (address.tab, address.row, address.column)
if key in seen:
duplicate_count += 1
continue
seen.add(key)
if duplicate_count:
raise CalcSheetsRecordError(
"sheet export plan writes more than one payload to the same cell address",
context={"duplicate_count": duplicate_count},
translated_message="application.storage.calc_sheets.records.errors.duplicate_write_address",
)
return self
[docs]
def all_addresses(self) -> tuple[SheetCellAddress, ...]:
"""Return every cell address referenced by value or formula cells.
Useful for collision detection and for building the full write list
before sending requests to the Sheets API.
Returns:
tuple[:class:`SheetCellAddress`, ...]: objects from ``value_cells``
followed by those from ``formula_cells``, in declaration order.
"""
seen: list[SheetCellAddress] = []
for cell in self.value_cells:
seen.append(cell.address)
for cell in self.formula_cells:
seen.append(cell.address)
return tuple(seen)
__all__ = [
"OperatorInput",
"OperatorInputs",
"ParameterCell",
"RelationValue",
"RelationValues",
"SheetAnchor",
"SheetAutoFilter",
"SheetCellAddress",
"SheetCellConstraint",
"SheetColumnWidth",
"SheetEvidenceContributorRow",
"SheetEvidenceFacet",
"SheetEvidenceManualEntry",
"SheetExportMetadata",
"SheetExportPlan",
"SheetFormulaCell",
"SheetFrozenView",
"SheetGuideContent",
"SheetNumberFormat",
"SheetProtectedRange",
"SheetProvenanceRow",
"SheetRowSet",
"SheetRowSetColumn",
"SheetSectionHeader",
"SheetStyledRange",
"SheetTariffTable",
"SheetTariffTableRow",
"SheetValueCell",
"TabName",
"_column_index_to_letters",
"_column_letters_to_index",
"_utc_now",
]