API reference¶
The public Python API of dlt-ops — the names exported from the top-level dlt_ops package (its __all__). Everything else is importable but internal, with no stability promise; the public-vs-internal convention and the stability contract live in versioning. Import any name directly, for example from dlt_ops import with_checkpoints, DestinationAdapter, register.
The schema-drift names (reconcile_all, reconcile_source, detect_removal, ReconcileResult, DriftFinding, AlertSink) are re-exported lazily so that import dlt_ops never pulls the reconciler's backend dependencies at import time; they are documented below under their dlt_ops.reconciler module but resolve equally as from dlt_ops import reconcile_all.
Plugin registration¶
Register a plugin against one of the entry-point axes; see plugins.
dlt_ops.register
¶
Register the decorated object as plugin <axis>/<name>.
Runtime twin of the dlt_ops.<axis> entry-point groups — both feed
the same process-wide registry, so get/names behave identically
regardless of how a plugin arrived::
@dlt_ops.register("destination", "duckdb")
class DuckDBAdapter: ...
Discovery and validation¶
Scan a project for sources and run the rule framework; see discovery and validation.
dlt_ops.discover_sources
¶
Discover dlt sources and attach their imported callables.
Supports multiple sources per directory. Each source is keyed by its config_section (source name), not the directory name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
project_root
|
Path
|
Path to the project root (holds .dlt/config.toml and one subdirectory per pipeline) |
required |
Returns:
| Type | Description |
|---|---|
dict[str, SourceInfo]
|
Dict mapping source name (config_section) to a fully-introspected |
dict[str, SourceInfo]
|
SourceInfo. Sources whose module could not be imported are excluded |
dict[str, SourceInfo]
|
(with a warning); Phase 1 still lists them via |
Raises:
| Type | Description |
|---|---|
ProjectConfigParseError
|
.dlt/config.toml exists but is broken TOML. |
dlt_ops.validate_sources
¶
validate_sources(
project_root: Path,
*,
validators: list[Validator] | None = None,
strict: bool = False,
) -> list[ValidationError]
Run the resolved rule set against discovered sources.
Discovery runs both phases: Phase 1 (AST) lists everything — including a
placeholder per source module that does not parse — and Phase 2
(sandboxed import) attaches callables and records import failures /
Rule 15 findings. Validators that instantiate sources see only the
import-OK subset (ctx.sources); the import-health validators see the
full Phase-2 output (ctx.introspected).
Rule assembly + resolution happen once per run: registry defaults
overlaid by [dlt_ops.rules] decide which rules execute, and
rule_exemptions findings are filtered per (source, rule) pair.
Config problems — unknown rule IDs, non-bool knob values, unjustified
exemptions — surface as errors in the returned list, as do rule
providers that contributed no rules (:func:rule_provider_errors): a run
that quietly checks less than it claims must not report success.
Import-error surfacing (a module that cannot import cannot run — it fails
to parse, raises, or is withheld for violating Rule 15) is
infrastructure, not a rule: always on, no knob.
Every finding is returned in both modes — warnings included, so callers
can render them — and is_warning carries the finding's severity for
this run: strict promotes warnings to errors before returning, so a
caller decides pass/fail with one question in either mode, "is any finding
not a warning?".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
project_root
|
Path
|
Path to the project root |
required |
validators
|
list[Validator] | None
|
Escape hatch — run exactly these callables instead of the resolved rule set. Rule knobs and exemptions key on rule IDs and therefore do not apply to a custom list. |
None
|
strict
|
bool
|
If True, promote warnings to errors ( |
False
|
Returns:
| Type | Description |
|---|---|
list[ValidationError]
|
Every finding, errors and warnings alike. |
dlt_ops.SourceInfo
¶
Discovered dlt source with metadata.
Two-phase population:
- Phase 1 (
discovery.phase1.discover) fills the static fields from a pure AST scan and never imports project code.resourcesis then a static approximation:@dlt.resourcedeclarations in the source's own module plus the pipeline'sresource/*.pysiblings (dynamic resource factories only resolve in Phase 2). - Phase 2 (
discovery.phase2.introspect) enriches: attaches the importedsource_fn, replacesresourceswith the authoritative instantiated list, and records import failures / import-safety findings.
dlt_ops.SourceConfig
¶
Config from config.toml for a source.
All custom keys are under [sources.X.dlt_ops]:
- schedule: Schedule enum value
- destination: per-source destination override; falls back to
[dlt_ops].default_destination (see dlt_ops.config)
- dataset: per-source dataset override; falls back to
[dlt_ops].default_dataset
- airflow_var: Airflow Variable name, surfaced by pipeline list
/ pipeline resources. Parsed for display only — the Airflow secret
backend reads its own trigger keys straight off the raw
[sources.X.dlt_ops] table, so core never acts on this value.
- schema_contract_evolve_reason: opt-in for the evolve schema
contract literal. Non-empty string = the source may declare
{"tables": "evolve", "columns": "evolve", "data_type": "freeze"}
on its @dlt.resource calls. Absence / empty string = default freeze.
- injected_columns: infrastructure keys stamped per-row inside a
resource rather than coming from the upstream payload. Consumed by
the reconciler so it doesn't flag them as unknown drift.
loaded_at is always ignored — no need to list it.
is_schema_contract_evolve
property
¶
True iff a non-empty justification is present.
Empty string is treated as absence so opt-in requires actual
justification, not just presence of the key. Non-string TOML values
(int/bool/list) are treated as absence rather than raising — the
scanner passes the value through untouched so a hand-authored
schema_contract_evolve_reason = 42 reaches here as an int.
dlt_ops.Schedule
¶
Bases: str, Enum
Valid schedule tags for dlt pipelines.
from_string
classmethod
¶
Parse schedule from string, with helpful error message.
dlt_ops.RuleSpec
¶
One registered validation rule: stable identity + validator + provenance.
rule_id is the rule's public identity: the [dlt_ops.rules]
knob and rule_exemptions tables key on it, so it never renames within
a major version. plugin names the provider that registered the rule
("core" for the package's own rules, the plugin's entry-point name
otherwise) — shown as the rule's origin in validate
--show-resolved-rules. default_on=False ships a rule opt-in.
description
property
¶
One-line rule summary: the validator docstring's first line.
dlt_ops.Validator
¶
Bases: Protocol
Protocol for validator functions.
dlt_ops.ValidationContext
¶
Context passed to all validators.
sources holds only import-OK sources (source_fn attached) so rules
that instantiate sources keep working. introspected is the full Phase-2
output — including sources whose module failed to import or violated
Rule 15 — for the import-health validators.
resolved_rules is the per-run rule resolution (registry defaults
overlaid by [dlt_ops.rules]) and exemptions the parsed
[sources.<X>.dlt_ops.rule_exemptions] view
({source: {rule_id: reason}}). Both are populated once per
validate_sources run so validators consult a single resolution
instead of re-reading raw config.
dlt_ops.ValidationError
¶
Validation error for a source.
Checkpoints¶
Persist pagination progress to the destination and resume mid-run; see checkpoints.
dlt_ops.with_checkpoints
¶
with_checkpoints(
cursor_field: str,
frequency: int = 10,
offset_seconds: int = 1,
checkpoint_table: str = DEFAULT_CHECKPOINT_TABLE,
cleanup_days: int = 7,
value_parser: Callable[[str], Any] | None = None,
) -> Callable
Decorator to add transparent checkpointing to dlt resources.
Saves progress to destination during pagination and resumes from last checkpoint on failure. Works with any incremental cursor field.
Concurrent Run Isolation: Automatically isolates checkpoints for different intervals (e.g., hourly vs backfill) using the incremental's initial_value. Runs with different initial_value get separate checkpoint namespaces. Scheduler-agnostic (works locally and in Airflow).
Decorator order: @with_checkpoints must sit UNDER @dlt.resource
(closest to the generator function). Applied on top of @dlt.resource it
would replace the DltResource with a plain generator function, silently
dropping the resource's name, write disposition, and hints — that order
raises TypeError at decoration time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cursor_field
|
str
|
Name of incremental field (e.g., "last_updated_utc") |
required |
frequency
|
int
|
Checkpoint every N pages (default: 10) |
10
|
offset_seconds
|
int
|
Safety overlap on resume in seconds (default: 1) |
1
|
checkpoint_table
|
str
|
Name of checkpoint table (default: _dlt_custom_checkpoints) |
DEFAULT_CHECKPOINT_TABLE
|
cleanup_days
|
int
|
Days to keep completed checkpoints (default: 7) |
7
|
value_parser
|
Callable[[str], Any] | None
|
Optional function to parse checkpoint string back to original type. If not provided, uses default parser for datetime/timestamp fields. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If frequency <= 0 or cleanup_days < 0 |
TypeError
|
If applied on top of |
Usage
@dlt.resource(...) @with_checkpoints(cursor_field="last_updated_utc") def my_resource(last_updated_utc=dlt.sources.incremental(...)): for page in paginate(...): yield page
Example with custom parser
@dlt.resource(...) @with_checkpoints( cursor_field="cursor_id", value_parser=lambda s: int(s) ) def my_resource(cursor_id=dlt.sources.incremental(...)): for page in paginate(...): yield page
dlt_ops.list_checkpoints
¶
list_checkpoints(
pipeline_name: str | None = None,
checkpoint_table: str = DEFAULT_CHECKPOINT_TABLE,
pipeline=None,
) -> list[dict[str, Any]]
List all checkpoints for debugging.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pipeline_name
|
str | None
|
Pipeline name to filter (required if pipeline not provided) |
None
|
checkpoint_table
|
str
|
Name of checkpoint table (default: _dlt_custom_checkpoints) |
DEFAULT_CHECKPOINT_TABLE
|
pipeline
|
Optional pipeline object (avoids dlt.attach() call) |
None
|
Returns:
| Type | Description |
|---|---|
list[dict[str, Any]]
|
List of checkpoint records as dicts keyed by column name, newest first |
dlt_ops.cleanup_checkpoints
¶
cleanup_checkpoints(
pipeline_name: str | None = None,
resource_name: str | None = None,
checkpoint_table: str = DEFAULT_CHECKPOINT_TABLE,
pipeline=None,
*,
include_active: bool = False,
)
Delete completed checkpoint rows for a pipeline or resource.
Retention housekeeping by default: only rows a successful run already
marked completed are deleted. active rows are live resume state — the
row a crashed or still-running extract resumes from — so deleting one
restarts that resource at its window start and silently re-extracts
everything the previous run already loaded. Active rows found in scope are
kept and reported at WARNING.
Pass include_active=True for the destructive form: every checkpoint row
in scope regardless of status. That is the surgical escape hatch —
abandoning a poisoned resume point, or clearing state for a pipeline
dropped outside dlt-ops — and the next run of an affected resource
restarts its window from the beginning.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
pipeline_name
|
str | None
|
Pipeline name to clean (required if pipeline not provided) |
None
|
resource_name
|
str | None
|
Specific resource to clean (None = all resources in pipeline) |
None
|
checkpoint_table
|
str
|
Name of checkpoint table (default: _dlt_custom_checkpoints) |
DEFAULT_CHECKPOINT_TABLE
|
pipeline
|
Optional pipeline object (avoids dlt.attach() call) |
None
|
|
include_active
|
bool
|
Also delete |
False
|
Examples:
Prune completed checkpoints for a pipeline¶
cleanup_checkpoints(pipeline_name="my_pipeline")
Prune completed checkpoints for one resource¶
cleanup_checkpoints(pipeline_name="my_pipeline", resource_name="companies_bulk")
Wipe every row, resume state included¶
cleanup_checkpoints(pipeline_name="my_pipeline", include_active=True)
Destination adapters¶
The contract a destination implements to reach full tier; see destinations and capability tiers and the adapter guide.
dlt_ops.DestinationAdapter
¶
Bases: Protocol
Everything checkpoint/cleanup/reconciler code may know about a destination.
Implementations register as dlt_ops.destination entry points (or
via dlt_ops.register("destination", name)) and are resolved by
name through the plugin registry.
name
instance-attribute
¶
Registry key: the destination's dlt engine name — "bigquery", "duckdb", ...
Not the SQL dialect. Which dialect an adapter transpiles into is its own business and stays out of this port; engines can share a dialect, and an engine name is not always one any transpiler knows.
placeholder_style
instance-attribute
¶
Native positional placeholder token of the destination's dlt sql_client.
Deliberately an open string: the set of placeholder styles a DB-API driver
can use is not the package's to close, so an adapter may declare any token
its client binds against. Informational for diagnostics; conversion from
canonical ? happens inside execute_sql / execute_query, never
in caller code.
supports_if_exists
instance-attribute
¶
CREATE TABLE IF NOT EXISTS / DROP TABLE IF EXISTS are valid DDL.
Consumers: checkpoint table DDL and drop_table_if_exists,
which falls back to probe-then-drop when False.
supports_create_schema_if_not_exists
instance-attribute
¶
The adapter may create the schema/dataset via ensure_schema.
Consumer: CheckpointManager setup, which needs the schema to exist before
the checkpoint DDL runs. False when schema creation is not the adapter's
to make — it can carry placement or access-control decisions owned by dlt
or the surrounding infrastructure — and ensure_schema is then a no-op.
timestamp_now_sql
instance-attribute
¶
Canonical-dialect fragment for "now" that survives this adapter's transpile.
Consumers: checkpoint _mark_completed, cleanup retention.
timestamp_sub_days_sql
instance-attribute
¶
days -> canonical-dialect fragment for "now minus N days".
Same consumers as timestamp_now_sql; kept per-adapter because interval
arithmetic is the idiom sqlglot most often mistranslates.
render_identifier
¶
Validate ident against this destination's identifier grammar and quote it.
Returns the canonically-quoted form for embedding in canonical SQL —
native quoting is applied by the transpile step inside execute_sql.
Defaults to :func:render_canonical_identifier; an adapter whose
destination accepts less may tighten the grammar, never loosen it.
Raises ValueError for names outside the grammar.
render_table_ref
¶
dataset.table reference in canonical form; both parts validated.
execute_sql
¶
Transpile canonical SQL, bind *params, execute via client.
Params bind natively by default. An adapter whose driver cannot type a
given bound value — a rejected bound None is the usual case — may
inline it as a literal instead: the None becomes a NULL literal
in the SQL, never injected text (the value still goes through the
sqlglot AST, not string interpolation).
execute_query
¶
Like execute_sql but returns a Cursor over the result rows.
ensure_schema
¶
Create the schema/dataset when the destination supports and needs it.
No-op when supports_create_schema_if_not_exists is False, so
callers may invoke it unconditionally.
fetch_columns
¶
Columns of dataset.table via one canonical information_schema.columns SELECT.
Returns None when the table (or its dataset) is absent. Consumer:
the reconciler's drift detection.
Capability-derived adapters¶
These names live on the dlt_ops.destinations subpackage rather than the top level — import them as from dlt_ops.destinations import register_derived_adapter. They build an adapter from the facts dlt already publishes about a destination, for engines nobody hand-wrote one for. Derivation shapes the SQL for the declared dialect; it does not verify that anyone has run it there, so registering is opt-in and logs a warning. Derived is not the same as tested is the framing to read before relying on one.
dlt_ops.destinations.derived.register_derived_adapter
¶
Register a capability-derived adapter for engine on the destination axis.
The runtime twin of shipping an adapter under the dlt_ops.destination
entry-point group: after this call has_adapter answers True for
engine and the adapter-gated features run against it. Registering the
same engine twice is harmless.
Raises:
| Type | Description |
|---|---|
UnderivableDestinationError
|
see :func: |
dlt_ops.destinations.derived.derived_adapter
¶
Build (without registering) a capability-derived adapter for engine.
Raises:
| Type | Description |
|---|---|
UnderivableDestinationError
|
dlt publishes no basis for one — an unresolvable destination, no declared dialect, or a dialect whose placeholder syntax sqlglot cannot render as a token. |
dlt_ops.destinations._capabilities.derivable_destinations
¶
Engine names dlt publishes enough capabilities to derive an adapter for.
Diagnostic surface: what a user could opt into, not what is supported. Derivation says the SQL will be shaped for the right dialect; it says nothing about whether anyone has run it there.
dlt_ops.destinations.is_capability_derived
¶
Whether adapter was derived from dlt's capabilities rather than written and tested.
Takes a resolved adapter rather than a name so it never triggers a load of its own. Reporting surfaces use it to qualify full tier honestly: derived is usable, but it says the SQL was shaped for the declared dialect, not that anyone ran it there.
dlt_ops.destinations._capabilities.UnderivableDestinationError
¶
Bases: LookupError
Capabilities for this destination give no basis for an adapter.
Raised instead of falling back to a plausible-looking dialect: guessing one would produce SQL that parses and silently means something else.
dlt_ops.destinations.CI_VERIFIED_DESTINATIONS
module-attribute
¶
Destinations whose adapter is exercised against a live instance in CI.
The honest floor under every "supported" claim. A destination absent here may
still work — bigquery has a live lane that is credential-gated and
therefore non-blocking, and a capability-derived adapter may be perfectly
correct — but nothing has proven it on every commit. Kept separate from the
registry on purpose: registration answers "can this run", this answers "has
anyone checked", and conflating them is how a package starts overclaiming.
Assertions¶
Pre-load data-quality gates and the error they raise; see assertions.
dlt_ops.AssertionType
¶
Bases: Protocol
One assertion type: a named accumulator with a static config check.
Implementations register under the dlt_ops.assertion entry-point
group (or the runtime twin dlt_ops.register("assertion", name));
entry points conventionally register the class, instantiated with no
arguments.
name
instance-attribute
¶
Registry name == TOML key. Frozen once released (config compatibility).
row_scoped
instance-attribute
¶
True → observe emits per-row verdicts (quarantine-compatible).
False → batch-scope only; on_failure = "quarantine" is a config error.
check_config
¶
Statically checkable config errors (Tier 1). Empty list = valid.
This is the enforced-opinions declaration mechanism: everything a type
CAN check without data (param types/domains, column references against
ctx.declared_columns) it MUST check here. Never touches data,
network, or clients.
start
¶
Fresh accumulator state for one (resource, run) batch.
observe
¶
Per-row verdict: None = pass; message = THIS ROW fails.
Batch-scope types accumulate and return None.
finalize
¶
Batch verdict after the last row: None = pass; message = batch fails.
dlt_ops.AssertionContext
¶
Static facts check_config may validate against. No data, no clients.
declared_columns
instance-attribute
¶
Column names from the resource's columns= Pydantic model; None when
the model is unresolvable (e.g. pydantic_columns_required exempted).
Types MUST skip column-existence checks when this is None.
dlt_ops.AssertionFailedError
¶
Bases: Exception
A configured assertion failed with on_failure = "fail".
Raised by the engine (row verdicts inside the extract gate, batch verdicts
at finalize); the run aborts, nothing loads, and the runner drops the
pending extracted package so the rejected batch cannot auto-load on the
next run.
Secret backends¶
The contract a secret backend implements; see plugins.
dlt_ops.SecretBackend
¶
Bases: Protocol
Everything the runtime may know about a secret backend.
Raising :class:SecretNotFoundError from get on a missing secret is
part of the contract. The optional secret_requests selection hook is
documented in the module docstring — it is not a required member.
Alert sinks¶
The contract a drift-alert sink implements; see reconciler.
dlt_ops.reconciler.AlertSink
¶
Bases: Protocol
Destination for reconciler events: drift findings + internal errors.
The contract of the alert_sink plugin axis (entry-point group
dlt_ops.alert_sink): emit_drift(finding) /
emit_error(exc, *, source_name, resource_name, context) /
flush(timeout). Sinks are selected per project via
[dlt_ops] alert_sinks = ["logging", ...] (default: the core
logging sink) and constructed with their
[dlt_ops.alert_sink.<name>] options as keyword arguments; all
configured sinks receive every event. --dry-run suppresses all
emission.
flush(timeout) is called by every public reconciler entry point on
the way out — sinks with a background transport queue must drain it
there (deterministic, bounded by timeout) because a short-lived CLI
or orchestrator task may exit immediately after.
Schema-drift reconciliation¶
Diff declared models against the live destination schema; see reconciler.
dlt_ops.reconciler.reconcile_all
¶
reconcile_all(
*,
dry_run: bool = False,
fetcher: "SchemaFetcher | None" = None,
runner: "QueryRunner | None" = None,
dataset: str | None = None,
project_root: "Any | None" = None,
project_config: ProjectConfig | None = None,
sink: "AlertSink | None" = None,
) -> list[ReconcileResult]
Iterate every discovered source and reconcile each.
Each source resolves — and reconciles against — its own destination +
dataset from the config chain, so a multi-destination project sweeps
per destination with no cross-destination auth. A failure on one source
never blocks the others — each source produces its own
ReconcileResult (potentially with error=<message>). One sink
flush at the end covers the whole sweep, not one per source.
dlt_ops.reconciler.reconcile_source
¶
reconcile_source(
source_name: str,
*,
dry_run: bool = False,
fetcher: "SchemaFetcher | None" = None,
runner: "QueryRunner | None" = None,
dataset: str | None = None,
sources: dict[str, SourceInfo] | None = None,
project_root: "Any | None" = None,
project_config: ProjectConfig | None = None,
sink: "AlertSink | None" = None,
) -> ReconcileResult
Run additive-drift detection against one discovered source.
dry_run=True suppresses all sink emission (both drift and
reconciler-error paths); the returned ReconcileResult still carries the
findings tuple so local verification can inspect them.
Injection points (fetcher, runner, dataset, sources,
project_root, project_config, sink) exist for tests — prod
callers pass nothing and everything resolves from the project config
chain + discover_sources + the DestinationAdapter-backed defaults.
project_root lets the CLI's project-root option reach discovery
without an env-var round-trip.
dlt_ops.reconciler.detect_removal
¶
detect_removal(
source_name: str,
*,
dry_run: bool = False,
runner: "QueryRunner | None" = None,
dataset: str | None = None,
sources: dict[str, SourceInfo] | None = None,
project_root: "Any | None" = None,
project_config: ProjectConfig | None = None,
sink: "AlertSink | None" = None,
baseline_threshold: float = DEFAULT_BASELINE_THRESHOLD,
recent_threshold: float = DEFAULT_RECENT_THRESHOLD,
recent_window_hours: int = DEFAULT_RECENT_WINDOW_HOURS,
baseline_window_days: int = DEFAULT_BASELINE_WINDOW_DAYS,
) -> ReconcileResult
Run removal-drift detection against one discovered source.
Contract matches additive.reconcile_source — dry_run=True
suppresses all sink emission; source-level failures land in
result.error; per-resource failures are trapped and reported through
the sink's error path but don't stop the sweep. When no
load_timestamp_column is configured, detection is skipped and the
result carries a warning instead.
Callers can widen or tighten the windows and thresholds without
patching this module — every knob is a keyword-only parameter with the
canonical default exported as DEFAULT_*. Prod callers pass
runner=None and the reconciler opens the source's own resolved
destination through the DestinationAdapter boundary.
dlt_ops.reconciler.ReconcileResult
¶
Outcome of one reconcile_source / detect_removal call.
findings is the tuple of per-resource findings surfaced during the
run. duration_ms is wall-clock time including destination round-trips
and alert emission (or 0 when the runner skipped everything on an
early failure). error is populated only when the top-level
source-scan itself failed — per-resource failures are wrapped and
reported through the alert sink's error path, and do not surface here so
an orchestrated sweep can succeed with partial coverage. warnings
carries non-fatal degradations (e.g. removal detection skipped because no
load-timestamp column is configured).
dlt_ops.reconciler.DriftFinding
¶
One drifted resource surfaced by the reconciler.
kind disambiguates additive (the destination has a column the model
doesn't) from removal (a model column's non-null coverage collapsed to
~0). columns is the full drifted set — additive detection emits one
finding per drifted RESOURCE with all its drifted columns, not one
finding per column, so downstream alert issues collapse cleanly.
sample_values is a mapping keyed by column name → up to 5 recent
values from the resource's destination table. Empty for removal findings
(there are no recent non-null values to sample by definition).
inferred_types is the destination-reported data_type string per
column, positionally aligned to columns.
reproduce_sql is a copy-pasteable canonical-dialect SELECT computed at
detection time (where the resolved dataset and load-timestamp column are
known) so alert sinks stay pure serializers with no SQL knowledge.
Pydantic model helpers¶
Derive column facts from your declared Pydantic models; see add a source.
dlt_ops.drop_unknown_nulls
¶
Strip None-valued keys not in the model's known-fields set.
Prevents the seen-null-first freeze trap: dlt would otherwise register an incomplete column on the first null observation; when the first non-null arrives, dlt tries to complete-with-type and data_type: "freeze" rejects it. Stripping unknown nulls means the column is born typed on its first real value.
dlt_ops.extract_model_column_names
¶
Return the set of keys a raw payload can carry for a known model field.
Includes attribute names + aliases + validation_aliases so callers do not
need to know Pydantic-v2 alias forms. Shared by drop_unknown_nulls (below),
reconciler/additive.py (known-set for live_columns - known diff), and
reconciler/removal.py (columns-to-check-for-coverage-drop).