Ingest messy tabular-ish feeds from any source and map them to your Pydantic model. An LLM writes a versioned spec once; every subsequent parse is deterministic, with zero LLM calls.
Project description
fidelis
Every partner sends the same data in a different shape. fidelis maps any of them onto one Pydantic model — an LLM writes the mapping once, then never runs again.
A CSV from one partner, an Excel sheet from another, a JSON webhook from a third: the same data, with different column names, units, and quirks — and a field that's missing here and renamed there. Instead of a hand-written parser per source, you point fidelis at each one. The first time it meets a new shape, an LLM writes a versioned mapping spec — human-readable YAML you review and commit. Every run after that is pure, deterministic Python: zero LLM calls, every row validated, schema drift caught, and nothing dropped silently.
A file is just one adapter. Identity is bound to what the fields mean (their signature), not to a filename — so one spec covers a CSV today and the same vendor's JSON tomorrow.
Why fidelis
| 🧠 LLM once, then never | An LLM writes the spec the first time it sees a source shape. Every run after is pure, deterministic Python — no network, no surprises. |
🔌 Any source, one parse() |
CSV, Excel, JSON, or a list[dict] payload — matched by what fields mean, not by filename. One call, same result. |
| 📝 The spec is the contract | A versioned, reviewable YAML — mapping, transforms, enrichment, and dedup all in one place. You diff it in PRs, not reverse-engineer a black box. |
| 🛟 Never lose a row | Every input row becomes a validated record or a RowError with its row, field, and reason — then quarantine, fix, and re-ingest. |
| 🔎 Drift-aware | A vendor adds or renames a column and fidelis catches it under an explicit policy — caught in CI, not silent corruption in prod. |
| 🪶 Light & provider-agnostic | pydantic + pyyaml at the core; Anthropic / OpenAI / local LLMs are optional extras. |
Contents
- Install
- The headline: one
parse(), any source, same result - Worked examples (
examples/showcase/) - How spec generation & caching work
- The YAML spec format
- Schema drift handling
- Quarantine round-trip
- Transforms
- Enrichment (post-mapping hooks)
- Shaping output (multi-record, multi-field, whole-column)
- Run context, coverage & LLM hints
- Provider strings
- Testing offline with
FakeProvider - API surface
- License
Install
pip install fidelis # core (pydantic + pyyaml)
# or pull in what you need:
pip install 'fidelis[all]' # + Excel, HTTP providers, pandas & polars
pip install 'fidelis[excel]' # + openpyxl for .xlsx sources
pip install 'fidelis[anthropic]' # + httpx for the Anthropic provider
Requires Python ≥ 3.11.
Working from a clone? Install in editable mode instead:
pip install -e '.[dev]'.
Don't have a model yet?
infer_model_source writes a draft Pydantic class from a sample — field names
and types inferred from the data (deterministic, no LLM). Review it, then use it
as your target:
from fidelis import infer_model_source
print(infer_model_source("feed.csv", class_name="User"))
Inference: textual booleans → bool, whole numbers → int, decimals →
float, ISO/dd.mm.yyyy dates → date, else str; any empty/missing value in
the sample makes the field Optional[...] = None.
The headline: one parse(), any source, same result
The same parser.parse() consumes a CSV file and a list[dict]
payload. Because both carry the same fields by meaning, they resolve to the
same cached spec and produce identical valid_rows.
from datetime import date
from pydantic import BaseModel
from fidelis import Parser
class User(BaseModel):
email: str
full_name: str
signup_date: date
parser = Parser(
target_model=User,
spec_store="specs/", # where specs live (a path, or a custom SpecStore)
llm="anthropic:claude-opus-4-8", # only used the first time a shape is seen
on_unknown_column="error", # default drift policy
)
# 1) A CSV file on disk → spec generated once and written to specs/.
from_file = parser.parse("incoming/acme_users.csv")
# 2) A list[dict] payload with the SAME fields → SAME cached spec, 0 LLM calls.
from_records = parser.parse([
{"E-mail": " a@b.com ", "Name": " Alice ", "Date": "01.02.2026"},
{"E-mail": "bob@x.com", "Name": "Bob", "Date": "15.03.2026"},
])
assert from_file.valid_rows == from_records.valid_rows # identical Users
# Inspect the result of a parse:
from_file.valid_rows # list[User] — validated records
from_file.errors # list[RowError] — what didn't fit, and why (never silent)
from_file.spec_used # the Spec that was applied
from_file.spec_generated # was the spec generated by the LLM in THIS run?
from_file.drift_report # schema drift, if any
from_file.needs_review # bool — any mappings still flagged needs_review?
print(from_file.summary())
# valid=2 errors=0 coverage=1.00 needs_review=True drift=False generated=True
Runnable, offline versions live in
examples/— no API key, no network. For a guided tour,examples/showcase/walks one feature per folder (a CSV, arun.py, and the spec), simplest first:python examples/showcase/01_basic/run.py # rename + transforms + default python examples/showcase/03_expand/run.py # one row → many records python examples/showcase/06_advanced/run.py # the whole pipeline at once
Other sources & the kind hint
parse() infers the source kind from the type / file extension. Pass kind=
to be explicit (e.g. a JSON string, or CSV text with no .csv extension):
parser.parse("incoming/users.xlsx") # Excel
parser.parse(json_payload, kind="json") # JSON array
parser.parse("E-mail;Name;Date\n...", kind="csv") # raw CSV text
Remote & compressed sources are handled transparently — pass an HTTP(S) URL
or a .gz path and fidelis fetches/decompresses before dispatch:
parser.parse("https://example.com/daily/feed.csv") # fetched over HTTP(S)
parser.parse("exports/users.json.gz") # gunzipped, then parsed
parser.parse("https://example.com/feed.csv.gz") # both
The adapter kind is inferred from the URL path / inner extension. (Excel is a binary container read from a path, so Excel-over-URL/gzip is not supported — download it first.)
Dedup / upsert keys
Declare the row key in the spec and fidelis collapses duplicate rows within
a feed, keeping the first (default) or last occurrence. Dropped rows are
never lost silently — they come back as result.duplicates, each pairing the
kept row with the one it displaced.
# in the spec:
dedup:
key: [email] # one or more model fields; composite key supported
keep: first # first | last (last = upsert-within-feed)
result = parser.parse(feed)
print(len(result.valid_rows), "kept,", len(result.duplicates), "dropped")
for d in result.duplicates:
print(d.key, "→ dropped", d.dropped)
The key is matched after validation/coercion, so "A@B.com" and "a@b.com"
collide once strip_lower has run. validate_spec flags a key field that isn't
on the model.
Global default. Parser(User, dedup_key="email", dedup_keep="last") still
works for specs that don't declare their own dedup (pass a list or
comma-separated string for a composite key). A spec's dedup takes precedence
over the Parser default.
Typed output
ParseResult hands the validated rows straight into your stack:
result.to_dicts() # list[dict] (mode="json" for JSON-native scalars)
result.to_pandas() # pandas.DataFrame — pip install 'fidelis[pandas]'
result.to_polars() # polars.DataFrame — pip install 'fidelis[polars]'
result.errors_to_dicts() # rejected rows for a dead-letter file / report
How spec generation & caching work — LLM once, then deterministic
Identity is bound to the field signature, not to a filename. The signature is a short hash of the normalized set of source field names (trim, lower, collapsed whitespace/underscores). Field order and case do not matter, so one spec covers a CSV today and a JSON payload tomorrow if they mean the same thing.
data → fingerprint of field names
├─ spec found → deterministic mapping (0 LLM calls)
└─ no spec → LLM generates a draft → saved to the spec store
→ you review it (especially needs_review mappings)
→ every subsequent run is LLM-free
What goes to the LLM is intentionally tiny and one-shot: only the inventory of
field names, a small sample of rows (≤20 by default), and the JSON
schema of your target model — never the whole dataset, and never per-row.
Low-confidence mappings (below confidence_threshold, default 0.8) are
auto-flagged needs_review so you know exactly what to check by hand.
Helper methods for review flows / CI:
spec = parser.generate_spec(source) # generate a draft Spec, don't parse
problems = parser.validate_spec(spec) # [] means the spec is well-formed
validate_spec checks that every target (and targets) exists on the model,
isn't mapped twice, every required model field is covered, transforms are known,
confidences are in [0, 1], and that everything the spec references by name —
enrich / batch_enrich / expand expanders / column_steps / dedup keys,
plus transforms inside rules — is registered and valid.
Where specs live (spec_store)
By default specs are YAML files next to your project (spec_store="specs/").
In production you may want them in S3, a database, or a config service —
implement SpecStore and pass it in. The same spec_store argument takes either
a path or a store:
Parser(User, spec_store="specs/") # files (default)
Parser(User, spec_store=S3SpecStore(...)) # your backend
A spec's identity is its field signature (the hash of the source field
names), which maps cleanly onto a storage key — get(signature) is one object
GET / one SELECT by primary key, no scanning. Only drift detection looks across
specs (via all()); a backend where listing everything is impractical can
override find_drift_candidate or skip it.
from fidelis import SpecStore, Spec
class S3SpecStore(SpecStore):
def __init__(self, bucket, prefix=""):
self.s3, self.bucket, self.prefix = boto3.client("s3"), bucket, prefix
def get(self, signature): # one GET, keyed by signature
try:
obj = self.s3.get_object(Bucket=self.bucket,
Key=f"{self.prefix}spec_{signature}.yaml")
except self.s3.exceptions.NoSuchKey:
return None
return Spec.from_yaml(obj["Body"].read().decode())
def save(self, spec):
self.s3.put_object(Bucket=self.bucket,
Key=f"{self.prefix}spec_{spec.signature}.yaml",
Body=spec.dump_yaml().encode())
# implement all() too if you want schema-drift detection
FileSpecStore and an in-memory MemorySpecStore (handy for tests) ship built-in.
The YAML spec format
A spec is a human-readable, version-controlled contract for ingesting one
source format. A real, hand-written example lives at
specs/partner_acme_users.yaml and maps onto
the User model above:
version: 1
generated_by: claude-opus-4-8 # or "human" after a manual edit
generated_at: "2026-06-28"
signature: 6a93a9 # hash of the normalized source field names
# Stage 1 — structural dialect. Only for text/file sources (CSV/TSV);
# skipped entirely for already-structured inputs (list[dict] / JSON).
parsing:
delimiter: ";"
encoding: utf-8
quote_char: '"'
# Stage 2 — universal field mapping (source column -> target model field).
mappings:
- target: email
source: "E-mail Address"
transform: strip_lower
confidence: 0.98
status: ok
- target: full_name
source: "Customer Full Name"
transform: strip
confidence: 0.91
status: ok
- target: signup_date
source: "Registration Date"
transform: "parse_date:%d.%m.%Y"
confidence: 0.62
status: needs_review # LLM unsure — a human verifies, then sets ok
- target: source_system # a CONSTANT — no source column
value: "acme"
- target: country # source if present, else this DEFAULT
source: "Country"
value: "US"
# Stage 3 — record-level steps, referenced by registered name (you add these by
# hand; the LLM never writes them). See "Enrichment", "Row expansion", "Dedup",
# and "Shaping output" for rules / unpivot / column_steps / skip_when.
expand: # one row -> many records
- field: airport_code # split this column...
delimiter: "|" # ...on this separator (declarative, no code)
enrich: # per-row (runs on each fanned-out row)
- fill_domain
batch_enrich: # whole-batch, register_batch_enrichment
- attach_scores
dedup: # collapse duplicate rows after validation
key: [email]
keep: first # first | last
# Per-source drift policy (overrides the Parser's global setting).
on_unknown_column: error # ignore | error | regenerate
Field notes:
signatureis the primary key binding the spec to data; it is whatparse()looks up. Editing source field names changes it. The target model is not in the spec — you bind it in code (Parser(target_model=...)), so a spec maps source fields onto field names and stays model-agnostic.statusisokorneeds_review; aneeds_reviewmapping is surfaced viaParseResult.needs_review.transformis a built-in name orname:arg(see below).valuesets a target without (or as a fallback to) a source: with nosourceit's a constant; with asourceit's the default used when the source cell is empty/missing. A mapping needs at least one ofsource/value.expand/enrich/batch_enrich/dedupdescribe the record-level steps in the spec, by registered name — so the YAML is the whole contract and varies per source. The functions are registered in code; the spec just references them, exactly liketransform. (The matchingParser(...)arguments still work as a global default for specs that don't declare a step.)parsingis only meaningful for text sources and is ignored for structured inputs.
Schema drift handling (on_unknown_column)
When a familiar source gains or loses a column, its field signature changes and
no spec matches exactly. fidelis recognizes this as drift of a known
source (by field-set similarity) rather than a brand-new format, and applies the
policy from the spec's on_unknown_column (falling back to the Parser
default). In every case ParseResult.drift_report describes exactly what
changed.
| Policy | Behavior |
|---|---|
ignore |
Silently ignore unknown columns and keep mapping the known ones. |
error |
(default) Raise DriftError with a precise message; lose nothing silently. |
regenerate |
Ask the LLM to fill in only the missing part of the mapping, then continue. |
parser = Parser(target_model=User, llm="anthropic:claude-opus-4-8",
on_unknown_column="regenerate")
result = parser.parse(source_with_a_new_column)
print(result.drift_report.describe())
# Schema drift (regenerate): new fields: Phone
Note on detection limits. Since specs are keyed by a hash of the source field names, a drifted source is recognized as "the same source, changed" only when its field set still overlaps a known spec's by ≥ 50% (Jaccard). If a source mutates so heavily that most columns are renamed at once, it is indistinguishable from a brand-new format and a fresh spec is generated instead of raising drift. Heavy renames therefore need a fresh review of the new spec.
Note on the
errorpolicy. BecauseerrorraisesDriftError, noParseResultis returned — the drift details live onexc.reportinstead ofresult.drift_report. Useignoreorregenerateif you need the drift reported inside the result object.
Quarantine round-trip
No bad row is ever dropped silently — every failure is a RowError carrying the
original source record. The quarantine helpers turn those into a file a
human can fix, then read it back into clean records to re-ingest:
result = parser.parse("feed.csv")
result.write_quarantine("bad_rows.csv") # original data + _error_reason column
# …a human opens bad_rows.csv and fixes the offending cells…
from fidelis import read_quarantine
fixed = read_quarantine("bad_rows.csv") # diagnostic columns stripped
again = parser.parse(fixed) # same field signature → same spec
Each exported row is the source record plus three _-prefixed diagnostic
columns (_row_index, _error_field, _error_reason) that are stripped on the
way back in — so the re-ingested rows keep the original field signature and reuse
the same spec (no spurious drift). .json paths round-trip as JSON, anything
else as CSV.
Transforms
A transform normalizes a raw cell before validation. Reference it in the spec by
name, or name:arg. Built-ins:
| Transform | Example | Effect |
|---|---|---|
strip |
strip |
Trim surrounding whitespace. |
strip_lower |
strip_lower |
Trim and lowercase (great for emails). |
to_int |
to_int |
Parse to int (tolerant of "12.0", spaces). |
to_float |
to_float |
Parse to float (accepts , decimal separator). |
to_bool |
to_bool |
yes/1/true/y → True, no/0/false/n → False. |
parse_date |
parse_date:%d.%m.%Y |
strptime with the given format (ISO if omitted). Pipe-separate several formats to try in order: parse_date:%Y-%m-%d|%d/%m/%Y. |
clip |
clip:0:100 |
Clamp a number into a range (clip:0: / clip::100 for one-sided). |
Empty values pass through untouched — requiredness is enforced by Pydantic validation, not by transforms. Register your own from code:
from fidelis import register_transform, available_transforms
# Plain call…
register_transform("upper", lambda value, arg: str(value).upper())
# …or as a decorator (like register_enrichment / register_expander):
@register_transform("shout")
def shout(value, arg):
return str(value).upper() + "!"
print(available_transforms()) # [..., 'shout', 'to_int', 'upper', ...]
Then use transform: upper in any spec.
Enrichment (post-mapping hooks)
A transform sees a single cell. An enrichment sees the whole mapped record and the original source row — so it can derive a field with no source column, combine several columns (mapped or raw), mask a value, or look something up in an external source. Enrichers run after mapping and before validation, so any field they add is validated like the rest.
Like a transform, you register the function in code and reference it by name
in the spec — so the YAML stays the full contract and enrichment can vary per
source. The signature is (record, source): record is the mapped target dict,
source is the raw input row.
import fidelis
@fidelis.register_enrichment("fill_domain")
def fill_domain(record, source):
record["domain"] = record["email"].split("@", 1)[1]
return record # or mutate in place and return None
# Reach RAW columns that aren't mapped to any target — e.g. combine two:
@fidelis.register_enrichment("full_name_from_parts")
def full_name_from_parts(record, source):
record["full_name"] = f"{source['First Name']} {source['Last Name']}".strip()
return record
# in the spec for this source:
enrich:
- fill_domain
- full_name_from_parts
- Enrichers are applied in spec order — each sees the previous one's output.
validate_specflags anenrichname that isn't registered; at parse time an unknown name raises loudly.- If an enricher raises, that record becomes a
RowError(never silently dropped), with the failing enricher's name in the reason. available_enrichments()lists what's registered.
Global default. Parser(User, enrich=["fill_domain"]) still works — it
applies to every spec that doesn't declare its own enrich. The constructor arg
also accepts callables directly (handy for quick scripts); specs reference names
only. Spec-declared steps take precedence over the Parser default.
How it differs from a transform:
| Transform | Enrichment | |
|---|---|---|
| Sees | one cell value | the mapped record + the source row |
| Configured | transform: in the spec |
enrich: in the spec |
| Can add fields with no source column | no | yes |
| Runs when | during mapping | after mapping, before validation |
Combine (many → one), split (one → many), coalesce, and conditional mapping are all just enrichers that read
source— declared in the spec, registered in code. Plain 1→1 column mapping and constants stay inmappings.
Batch enrichment — one bulk lookup instead of N
A per-row enricher that calls a database or an API runs once per row. When the lookup can be batched, register a batch enricher instead: it receives every clean record at once and returns a list of the same length, in order — so you make a single bulk call for the whole feed.
@fidelis.register_batch_enrichment("attach_scores")
def attach_scores(records):
ids = [r["user_id"] for r in records]
scores = score_service.bulk_fetch(ids) # ONE call, not len(records)
for r, s in zip(records, scores):
r["score"] = s
return records # same length, same order
# in the spec:
batch_enrich:
- attach_scores
- Batch enrichers run after the per-row
enrich, over the clean rows only — rows that failed mapping are excluded and preserved as errors. - The one-to-one contract is enforced: returning a different length (or a
non-dict) raises
BatchEnrichmentError. Batch enrichment is all-or-nothing — use it to fill/derive fields across the feed, not to filter rows. - Configuring
batch_enrichmaterializes the feed (it needs the whole set); without it the pipeline stays lazy. - As with
enrich,Parser(batch_enrich=[...])is a global default for specs that don't declare their own.
Row expansion (one row → many)
Sometimes a single source row stands for several entities — one line whose
Airports column lists "JFK;LAX;ORD" should become three rows. An
expander returns a list of records; the row fans out, and then enrich /
batch_enrich / validation / dedup all run per fanned-out row — so each
airport gets its own lookup and its own derived fields.
The common case — split one column — is fully declarative, no code: name the column and the delimiter right in the spec.
expand:
- field: airport_code # the (target) column holding the list
delimiter: "|" # the list separator — each vendor sets its own
enrich:
- resolve_airport # runs once per airport, not per source line
For arbitrary fan-out, register a custom expander — (record, source) → list —
and reference it with expander::
@fidelis.register_expander("by_region")
def by_region(record, source):
return [{**record, "region": r} for r in lookup_regions(source["Airports"])]
expand:
- expander: by_region
Expanders run before enrich, so enrichment is per fanned-out row (per
airport).
- Pipeline order: map → expand → enrich → batch_enrich → validate → dedup.
- A step may return zero, one, or many rows. If one fanned-out row fails, it
becomes a
RowErrorpointing back at the source line; its siblings still pass. Parser(expand=[...])is a global default (registered names or callables) for specs that don't declare their own.
Shaping output (multi-record, multi-field, whole-column)
Beyond 1 row → 1 record, the spec can reshape the output. Full pipeline: skip_when → unpivot → rules → map → column_steps → expand → enrich → batch_enrich → validate → dedup.
Conditional multi-record (rules) — one row → a record per firing rule
(base mappings + the rule's). E.g. a row with retail and wholesale prices
becomes two records:
mappings:
- {target: sku, source: SKU, transform: strip} # shared
rules:
- when: {field: RETAIL_PRICE, op: not_empty} # ops: not_empty/empty/eq/ne/in/gt/lt/ge/le
mappings:
- {target: kind, value: retail}
- {target: amount, source: RETAIL_PRICE, transform: to_float}
- when: {field: WHOLESALE_PRICE, op: not_empty}
mappings:
- {target: kind, value: wholesale}
- {target: amount, source: WHOLESALE_PRICE, transform: to_float}
Column-group fan-out (unpivot) — repeating groups → one record each:
unpivot:
count: 3 # indices 1..3 (or list them in `index`)
index_field: quarter
columns: {PRICE: "Q{i}_PRICE", QTY: "Q{i}_QTY"}
Multi-target transform (targets) — one transform fills several fields
(keep the converted and the original). The transform returns a dict:
mappings:
- {source: WEIGHT, transform: lbs_to_kg,
targets: {kg: weight_kg, original: weight_original, unit: weight_unit}}
Whole-column stage (column_steps) — decisions over a column's distribution,
applied before validation (median→/100, 2→4-digit year):
@fidelis.register_column_step("cents_if_huge")
def cents_if_huge(values, context=None): # whole column as a list, same length out
import statistics
nums = [v for v in values if isinstance(v, (int, float))]
if nums and statistics.median(nums) > 10_000:
return [v / 100 if isinstance(v, (int, float)) else v for v in values]
return values
column_steps: {price: cents_if_huge}
Skip & clip — drop rows declaratively, clamp numbers:
skip_when:
- {field: STATUS, op: in, value: [CANCELLED, VOID]} # drop these rows
mappings:
- {target: pct, source: PCT, transform: "clip:0:100"} # clamp into [0, 100]
Skipped / no-rule-fired rows produce no record but still count in the coverage denominator (below).
Run context, coverage & LLM hints
Run context (context=) — hand lookups / thresholds / config to hooks
instead of hardcoding globals. Any transform / enricher / batch / expander /
column-step that declares a context parameter receives it (others are
unaffected); it's isolated per parse():
@fidelis.register_transform("canon")
def canon(value, arg, context):
return context["synonyms"].get(str(value).strip(), value)
Parser(Supplier, context={"synonyms": {...}, "threshold": 0.85})
Coverage (result.coverage) — one quality number for the whole run: the
fraction of source rows that produced a record.
result.coverage # Coverage(rows_in=1000, rows_with_output=980, rows_with_error=12, …)
result.coverage.score # 0.98 — also in result.summary()
Domain hints (domain_hints=) — give the LLM typed context when it generates
a spec (only affects generation, never the deterministic path):
Parser(Item, llm="anthropic:claude-opus-4-8",
domain_hints={"currencies": ["USD", "EUR"], "categories": ["A", "B", "C"]})
Provider strings (anthropic / openai / local)
Pick a provider with a "provider:model" string (or pass a provider instance).
The provider is only ever used for one-shot schema inference — never per row.
Parser(target_model=User, llm="anthropic:claude-opus-4-8") # Anthropic Messages API
Parser(target_model=User, llm="openai:gpt-4o-mini") # OpenAI
Parser(target_model=User, llm="local:llama3") # any OpenAI-compatible endpoint
anthropic— readsANTHROPIC_API_KEYfrom the environment (or passllm_options={"api_key": ...}). Needshttpx(pip install 'fidelis[anthropic]').openai— readsOPENAI_API_KEY. Needshttpx.local— any OpenAI-compatible server (Ollama, vLLM, LM Studio…); defaults tohttp://localhost:11434/v1/chat/completions, no API key required. Override withllm_options={"base_url": ...}orLOCAL_LLM_BASE_URL.
Extra constructor arguments go through llm_options:
Parser(target_model=User, llm="anthropic:claude-opus-4-8",
llm_options={"api_key": "sk-...", "timeout": 30})
If no spec matches and no llm is configured, parse() raises
SpecNotFoundError — it will never reach the network behind your back.
Testing offline with FakeProvider
FakeProvider is a deterministic, network-free LLMProvider for tests and
offline flows. Hand it the mappings (or a canned JSON response, or a
responder callable) you want it to "infer":
from fidelis import Parser, FakeProvider
provider = FakeProvider(mappings=[
{"source": "E-mail", "target": "email", "transform": "strip_lower", "confidence": 0.98},
{"source": "Name", "target": "full_name", "transform": "strip", "confidence": 0.91},
{"source": "Date", "target": "signup_date", "transform": "parse_date:%d.%m.%Y", "confidence": 0.62},
])
parser = Parser(target_model=User, spec_store=tmp_dir, llm=provider)
result = parser.parse([{"E-mail": "a@b.com", "Name": "Alice", "Date": "01.02.2026"}])
assert provider.call_count == 1 # generated once
parser.parse(...) # subsequent calls: still 1 — cached, deterministic
This is exactly how the examples in examples/ run with no API key.
API surface
from fidelis import (
Parser, ParseResult, RowError, DriftReport, DuplicateRow, Coverage, SpecNotFoundError,
Spec, Mapping, ParsingSpec, DedupSpec, UnpivotSpec, Condition, Rule, ExpandStep,
compute_signature, find_spec_by_signature,
normalize_field_name, detect_drift, DriftError,
SpecStore, FileSpecStore, MemorySpecStore,
quarantine_rows, write_quarantine, read_quarantine, infer_model_source,
register_transform, apply_transform, available_transforms, TransformError,
register_enrichment, available_enrichments, EnrichmentError,
register_batch_enrichment, available_batch_enrichments, BatchEnrichmentError,
register_expander, available_expanders, split_rows, ExpansionError,
register_column_step, available_column_steps, ColumnStepError,
LLMProvider, resolve_provider,
AnthropicProvider, OpenAIProvider, LocalProvider, FakeProvider,
from_csv, from_records, from_json, from_excel, SourceData,
)
License
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fidelis-0.1.1.tar.gz.
File metadata
- Download URL: fidelis-0.1.1.tar.gz
- Upload date:
- Size: 138.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
39f8d7485e4167c587765cdd13863bd9c56a3a92fb28a15a96eed513edbeb4f7
|
|
| MD5 |
80c698f484f0ebe6c73aca6c599ce5cc
|
|
| BLAKE2b-256 |
bec5ea52d4bac294f21d750dc2f2de6fa9e7bd74fda48c0fcd47bd4cdc8bb41f
|
File details
Details for the file fidelis-0.1.1-py3-none-any.whl.
File metadata
- Download URL: fidelis-0.1.1-py3-none-any.whl
- Upload date:
- Size: 72.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8bdfbcc3b26b8de02a8ae4f8b177328108d530ce9dd46cada936c94f9e490c95
|
|
| MD5 |
44a297d6ecd29bd89e7f9141e4c68fab
|
|
| BLAKE2b-256 |
66be0d59a9260bf0381424f0c5555cfabc127a683598f017a5719ed8a71384cd
|