The connector model¶
A connector is a small async Python callable plus metadata. You write an
async def that fetches data and returns a plain pandas object (or a scalar
or dict); the @connector decorator turns it into a frozen Connector, and the
framework wraps whatever you return into a Result/TabularResult
with framework-built Provenance. The function's parameters are
the connector's parameters — there is no wrapper request object — which is what
makes connectors directly callable by agents.
This page is the map for the connector subsection. It covers what a connector
is, the @connector decorator at a glance, the rules every connector obeys, and
one compact end-to-end example. The deeper pages drill into each piece.
What a connector is¶
Connector is a frozen dataclass that pairs a name, a description, the
wrapped async fn, its inspect.Signature, and optional metadata (tags,
properties, an output schema, declared secrets, namespace hints). You do
not construct it directly — you decorate a function:
import asyncio
import pandas as pd
from parsimony.connector import connector
@connector
async def demo_search(query: str) -> pd.DataFrame:
"""Search demo series by keyword."""
return pd.DataFrame(
{"id": ["A", "B"], "title": [f"Series about {query}", "Another"]}
)
result = asyncio.run(demo_search(query="GDP"))
print(result.provenance.source) # demo_search
print(result.provenance.params) # {'query': 'GDP'}
print(len(result.df)) # 2
Calling a connector is a coroutine, so it must be awaited — here driven by
asyncio.run. The return value is always a Result (or its TabularResult
subclass when the connector returns a DataFrame or Series).
Import path
connector, loader, enumerator, Connector, and Connectors are
re-exported at the package root, so from parsimony import connector also
works. The ResultCallback type alias (covered under
calling, binding, and composing) is not
re-exported — import it from parsimony.connector.
The decorator at a glance¶
@connector works bare or called with options:
from parsimony.connector import connector
from parsimony.result import Column, ColumnRole, OutputConfig
OUTPUT = OutputConfig(
columns=[
Column(name="date", role=ColumnRole.KEY, namespace="demo"),
Column(name="value", role=ColumnRole.DATA, dtype="numeric"),
]
)
@connector(
name="demo_fetch",
output=OUTPUT,
tags=["demo"],
secrets=("api_key",),
)
async def fetch(series_id: str, api_key: str) -> pd.DataFrame:
"""Fetch demo observations for a series."""
return pd.DataFrame({"date": ["2020-01-01"], "value": [1.0]})
| Argument | Default | Meaning |
|---|---|---|
name |
fn.__name__ |
Connector identity; becomes provenance.source. |
description |
stripped fn.__doc__ |
Required. Human/agent-facing summary. |
output |
None |
An OutputConfig schema applied to a DataFrame/Series return. |
tags |
() |
Free-form labels (loader/enumerator set them automatically). |
properties |
{} |
Read-only metadata you can filter the collection on. |
secrets |
() |
Parameter names stripped from provenance; validated against the signature. |
loader and enumerator are stricter factories
built on the same machinery — they add output-schema contracts and a verb tag.
The rules every connector follows¶
These invariants are enforced by the framework — most at decoration time, so a malformed connector fails as soon as the module imports.
Parameters are the call surface¶
A connector exposes flat, top-level parameters. There is no bundled
params: SomeModel object — the conformance suite (see
conformance testing) actively forbids annotating a
public params parameter as a Pydantic BaseModel. Scalar parameters keep the
call surface legible to an agent and let the framework record exactly which
arguments produced a result.
The function must be async¶
The decorator rejects a plain def at decoration time:
from parsimony.connector import connector
try:
@connector
def not_async(q: str) -> str: # missing async
"""Sync functions are rejected."""
return q
except TypeError as exc:
print(exc) # not_async: connector function must be async
A description is mandatory¶
description defaults to the stripped docstring; pass description= to override
it. If both are empty the decorator raises ValueError:
try:
@connector
async def undocumented(q: str) -> str:
return q
except ValueError as exc:
print(exc) # undocumented: add a docstring or pass description= ...
Return raw data — never a Result¶
A connector returns raw data: a pandas.DataFrame/Series, a scalar, or a
dict. The framework builds the output envelope (the Result/TabularResult and
its Provenance); a connector that tries to build it itself fails at call time.
Returning the wrong shape raises TypeError
Returning a Result/TabularResult, or a (data, properties) tuple,
raises TypeError from inside the call — put provider facts in DataFrame
columns and let the framework wrap them. When an output schema is set, a
schema/coercion ValueError during wrapping is re-raised as a typed
ParseError.
Why raw data? It keeps the connector author's job to one thing — fetch and shape
the data — while the framework owns provenance, secret stripping, and the
declarative output schema. It also means a connector cannot
fabricate provenance: Provenance.source, fetched_at, and params are always
framework-built.
Secrets are declared, not exposed¶
Names listed in secrets= must be real parameters (an unknown name raises
ValueError at decoration time). Their values are stripped from
provenance.params whether they were supplied at call time or fixed with
bind. Combined with bind, this is how you inject an API key without it
appearing in agent-facing cards or recorded provenance — see
calling, binding, and composing.
A compact end-to-end example¶
Define a connector, attach an output schema, bind a secret, collect it into a
Connectors bundle, and call it by name. This runs with only parsimony-core
installed — no network, no plugins, no optional extras.
import asyncio
import pandas as pd
from parsimony.connector import Connectors, connector
from parsimony.result import Column, ColumnRole, OutputConfig
OUTPUT = OutputConfig(
columns=[
Column(name="date", role=ColumnRole.KEY, namespace="demo"),
Column(name="value", role=ColumnRole.DATA, dtype="numeric"),
]
)
@connector(output=OUTPUT, tags=["demo"], secrets=("api_key",))
async def demo_fetch(series_id: str, api_key: str) -> pd.DataFrame:
"""Fetch demo observations for a series id."""
# A real connector would call an HTTP API here using api_key.
return pd.DataFrame({"date": ["2020-01-01", "2020-02-01"], "value": ["1", "2"]})
async def main() -> None:
# Bind the secret once; it disappears from the call surface and provenance.
wired = demo_fetch.bind(api_key="s3cr3t")
bundle = Connectors([wired])
print(bundle.names()) # ['demo_fetch']
result = await bundle["demo_fetch"](series_id="X1")
print(result.provenance.source) # demo_fetch
print(result.provenance.params) # {'series_id': 'X1'} — api_key stripped
print(result.df["value"].tolist()) # [1, 2] — coerced to numeric
asyncio.run(main())
A few things to notice:
await bundle["demo_fetch"](...)is the canonical execution pattern:await connectors[name](**kwargs).Connectors.__getitem__takes a connector name, not an integer index.bindreturns a new connector —Connectoris frozen — withapi_keyfixed and removed from the public signature.- The
OutputConfigcoerced the stringvaluecolumn to numeric because the column declareddtype="numeric".
You merge bundles with the + operator (bundle_a + bundle_b); there is no
.merge method. The collection also gives you filter, search,
collection-wide bind, and the describe() / to_llm() prompt projections.
The connector subsection¶
| Page | What it covers |
|---|---|
| Defining connectors | The @connector decorator in depth: defaults, validation timings, namespace hints, and the describe() / to_llm() projections. |
| Loaders and enumerators | The two stricter verbs — fetching observation values vs discovering entities — and their output-schema contracts. |
| Calling, binding, and composing | Invoking connectors, bind, with_callback, and the immutable Connectors collection. |
| Results and output schemas | Result / TabularResult, OutputConfig / Column / ColumnRole, Provenance, and the dtype coercion rules. |
| Errors | The typed, agent-facing exception taxonomy connector authors raise. |
| HTTP transport | The async HTTP layer (HttpClient, retry policy, redaction) that connector authors build on. |
See also¶
- Defining connectors — the decorator and its rules in full
- Calling, binding, and composing —
bind, observers, and theConnectorscollection - Results and output schemas — what a connector's return value becomes
- Plugins and providers — how connectors ship as
parsimony-<name>distributions