Python API¶
MANNA is used through MCP, not as a library, but the modules below are stable
enough to read and useful for contributors. Tools never import pyvo directly;
every archive call goes through manna.backends.
Server¶
- manna.app.build_mcp()[source]¶
Construct the FastMCP server with all tools registered.
Every tool is read-only (the server never mutates archive state) except
abort_async_job, which deletes an upstream UWS job. Tools that hit live archive services are open-world; list_archives is the one closed-world archive-notes reader (describe_table left that set when it grew a live column fetch).- Return type:
FastMCP
Connections (manna.backends)¶
IVOA TAP backend: sync + async wrappers over pyvo.
This module is the ONLY place that imports pyvo. Tools work in terms of job_url strings and astropy.Table — they do not touch AsyncTAPJob.
- manna.backends.tap.job_error_message(job)[source]¶
The upstream UWS errorSummary message for a job, or None if there is none.
pyvo’s AsyncTAPJob has no public accessor for this: the parsed UWS tree hangs off job._job, and the text sits at errorsummary.message.content. This is the exact traversal pyvo’s own raise_if_error() performs; it lives here so the tools (which never import pyvo) get the message through one seam instead of guessing at attribute names — the previous getattr(job, “error_summary”) read an attribute that never existed and silently discarded every archive’s diagnostic for months.
- Parameters:
job (
AsyncTAPJob)- Return type:
- manna.backends.tap.job_phase(job)[source]¶
The phase pyvo parsed when the job was last fetched, without refetching.
AsyncTAPJob.phaseis a property that issues a new GET on every access; the constructor has already fetched the job, so the cached UWS tree atjob._jobis current for the read that just happened. Test doubles that carry a plainphaseattribute and no cached tree fall through to it.- Parameters:
job (
AsyncTAPJob)- Return type:
- class manna.backends.tap.TapClient(*, sync_timeout_seconds=20.0)[source]¶
Bases:
objectWrapper around pyvo.dal.TAPService + AsyncTAPJob.
All pyvo exceptions are mapped to project ToolExecutionError subclasses at the boundary; tools never see DAL errors directly.
- Parameters:
sync_timeout_seconds (
float)
- query(*, endpoint, adql, maxrec=10000)[source]¶
Synchronous TAP query. Bounded by sync_timeout_seconds (default 20s).
- submit_async(*, endpoint, adql, maxrec=10000)[source]¶
Submit a TAP query as an async UWS job and start execution.
Returns the absolute job_url. The caller (the LLM client holding the returned job_url) is the job’s lifetime owner; this method records no server-side state.
IMPORTANT: pyvo’s AsyncTAPJob defaults to delete=True, which registers an atexit-style cleanup that DELETEs the upstream job when the Python object is garbage-collected. We MUST disable that. Setting _delete_on_exit = False before the local reference falls out of scope prevents the upstream job from being silently destroyed between tool calls.
- load_job(job_url)[source]¶
Re-hydrate an AsyncTAPJob from a previously-stored job_url.
delete=False is critical — without it, the AsyncTAPJob’s __del__ would DELETE the upstream job as soon as the caller (a tool function) returned. See submit_async docstring.
- Parameters:
job_url (
str)- Return type:
- abort_job(job_url)[source]¶
Send UWS DELETE for a job. Idempotent: 404 / connection-refused on a previously-deleted job is swallowed so callers can abort twice without branching.
We construct the AsyncTAPJob directly here (instead of via load_job) so that DALServiceError from the construction-time GET — which happens on already-deleted jobs — is caught and swallowed at the same level as failures from job.delete().
- class manna.backends.sia.SiaClient[source]¶
Bases:
objectSync wrapper over pyvo SIA — SIA 2.0 by default, SIA 1.0 fallback.
Most modern archives (ALMA, CADC, ESO) speak SIA2. A few major ones — notably NOIRLab Astro Data Lab — only expose SIA v1. version=’auto’ tries SIA2 first and, when its capabilities probe fails (a DALAccessError, the tell that the endpoint isn’t SIA2), retries the same URL as SIA1.
- class manna.backends.cone.ConeSearchClient[source]¶
Bases:
objectSync wrapper over pyvo.dal.SCSService.search().
- class manna.backends.registry.RegistryClient[source]¶
Bases:
objectSync wrapper over pyvo.registry. No async.
- Two operations:
search(keywords/servicetype/waveband) -> list of service dicts
describe(ivoid_or_url) -> service dict with capabilities + tables
Name resolver backend — wraps CDS Sesame via astropy.coordinates.
- class manna.backends.resolver.ResolverClient[source]¶
Bases:
objectWraps CDS Sesame (SIMBAD → NED → VizieR fallback chain) for name resolution.
Result handling (manna.results)¶
Result handling — inline small results; link + pyvo fetch recipe for large ones.
Envelopes (shape_*), the sync→async promotion envelope, and the query_fingerprint / save_recipe cache fields all live here.
- manna.results.shape_inline_table(table, *, archive, maxrec)[source]¶
Convert an astropy.Table into the inline response envelope.
Rows flow inline. Callers that must NOT inline large results (TAP) check is_oversize first and route to the async/result-URL path; discovery tools (cone / SIA search) inline up to the cap and let shape_table truncate the tail.
- manna.results.shape_table(table, *, archive, maxrec)[source]¶
Inline envelope for discovery tools (cone / SIA search).
These are synchronous DAL queries with no async job or persistent archive URL to hand back, so a result larger than the inline cap is truncated inline with truncated=true and a hint to narrow the search. TAP does NOT use this path — it checks is_oversize and routes oversize results to the async / result-URL flow instead.
- manna.results.is_oversize(table)[source]¶
True if table would exceed the inline row OR byte cap.
TAP tools call this to decide between inlining a result and routing it to the async / result-URL flow. Cheap: the byte estimate only runs when the row count is already within the row cap.
- manna.results.build_fetch_recipe(job_url, result_url=None)[source]¶
Client-side recipe for loading an async TAP result with pyvo.
The server never fetches the result bytes; it hands the LLM the upstream job URL and the code to load it in the user’s own Python environment (e.g. a Jupyter kernel). Anonymous access only.
- manna.results.build_load_recipe(*, endpoint, adql)[source]¶
Client-side recipe re-executing a sync TAP query in the user’s kernel.
Inline rows in the envelope are for the MODEL to read; getting them into a notebook by pasting literals truncates tool calls past ~100 rows (observed live 2026-08-11: edit_cell arrived with empty args). This recipe is the size-independent transport — the kernel re-runs the same sync query directly against the archive (constant-size cell, same trade as auto-promote’s discarded first execution) and binds table + df.
- manna.results.build_save_recipe(*, fingerprint, tool, endpoint, archive, query, truncated, maxrec=None)[source]¶
Client-side recipe for persisting a query result as a CSV + catalog row.
Same philosophy as build_fetch_recipe: the server never touches the user’s filesystem — it hands the LLM a self-contained snippet to run in the user’s own Python environment. The snippet assumes the result is in a pandas DataFrame named df (built from the inline rows or loaded via fetch_recipe), writes manna_cache/<fingerprint>.csv, and appends a metadata row to manna_cache/catalog.csv.
Catalog appends go through csv.QUOTE_ALL — ADQL is full of commas and quotes, and a hand-concatenated row would corrupt the catalog. The target column is left empty for the agent to fill when it resolved a target name this conversation; saved_at is stamped client-side at execution time. The embedded values use !r so the generated snippet stays valid Python for any query text. maxrec records the upstream row cap that was in effect (or ‘’ when unknown, e.g. from get_async_job_results) — without it, a result capped upstream catalogs as truncated=False with no record of the cap, and a capped CSV can be mistaken for the full result.
The catalog append is deliberately append-only, not idempotent. Models retype this snippet by hand into notebook cells rather than executing it verbatim (observed live), and the previous read-filter-rewrite version was long enough that retyping corrupted it in roughly a third of runs — a dropped header line in one, a SyntaxError in another (observed live 2026-08-11). Every physical line is a retype hazard, so this version just appends: it can produce duplicate rows for the same fingerprint (rerunning the save cell, or refreshing the same query later). That’s accepted here — READERS must dedupe by taking the newest (last) row per fingerprint; that rule lives in the deployment persona, not in this snippet.
- manna.results.attach_cache_fields(envelope, *, fingerprint, tool, endpoint, query, maxrec=None, load_recipe=None)[source]¶
Add query_fingerprint + save_recipe (+ optional load_recipe) to a success envelope (mutates).
Called by the primitive query tools (TAP/cone/SIA) after shaping. archive and truncated are read off the envelope so the recipe’s catalog row always matches what the envelope claims — a truncated inline result is recorded as truncated and never mistaken for the full result by the client-side cache. maxrec is the upstream row cap in effect for this query (None when unknown, e.g. get_async_job_results, where the original maxrec isn’t recoverable from the job). Error payloads never pass through here (wrap_tool_errors short-circuits before shaping).
load_recipe (from build_load_recipe) is passed by the inline sync TAP paths only — it re-executes the query kernel-side so the client never has to paste inline rows into a cell to build df. When None (the async get_async_job_results path, where fetch_recipe already covers loading), no load_recipe key is added to the envelope.
When load_recipe is given, its code is fused with the save snippet’s code (load lines, then save lines, joined with a newline) — saving becomes a side effect of running load_recipe.code, not a separate cell the model can skip. Live runs showed models executing load_recipe + plotting but never running the standalone save cell at all (observed live, roughly half of runs) — a save action described as a second, later step is easy to drop once the model has what it wanted (df). save_recipe is still attached standalone too: async results load via fetch_recipe (not load_recipe) and still need it, and it covers a cache-only re-save of already-loaded data.
Also appends an imperative save instruction to the envelope’s top-level next_steps — nested fields like save_recipe.instructions get ignored by weak models (the PR-#64 lesson, observed live with a small local model, 2026-08-10), so the save action must live where those models reliably read it.
- manna.results.shape_result_url(*, job_url, result_url, archive, phase='COMPLETED')[source]¶
Envelope for a COMPLETED async TAP job.
Carries the upstream job URL, the direct result URL, and a pyvo fetch recipe. No result bytes flow through the server — the client loads the data itself.
next_steps is written for the weakest reader: imperative, names the model’s own code-execution tool, and forbids re-running the query. Small models (observed: small local models behind the Jupyter AI persona) treat descriptive phrasing like “fetch client-side” as someone else’s job and abandon the completed result.
- manna.results.shape_registry_search_result(services, *, maxrec)[source]¶
Envelope for search_ivoa_registry results.
- manna.results.shape_registry_describe_result(described, *, table_filter=None)[source]¶
Envelope for describe_ivoa_service.
When table_filter is given, the table set is narrowed (case-insensitive substring over name/description) before shaping — a narrow filter yields a small set, so full per-column detail fits inline and the model gets exactly the tables it wants, with columns, in one call.
Otherwise: small services pass through with full per-column detail for every table. Large services (tables x columns — e.g. Gaia/Data Lab) would overflow the model context, so the response degrades to a table catalog: every table keeps its name, (clipped) description, and column_count, but the per-column arrays are dropped. truncated discloses the degradation; a hint points the model at the per-table columns drill-down and at table_filter for narrowing.
Always carries top-level truncated (project contract) and total_tables; matched_tables is added when a filter is applied.
- manna.results.shape_promotion(*, job_url, archive, phase, submitted_at)[source]¶
Envelope returned when run_adql_query goes async (explicit mode=async, auto-mode timeout fallback, or an oversize sync result). The server has already waited its default window, so phase may be COMPLETED, in which case next_steps sends the model straight to get_async_job_results.
Shape-disjoint from the inline tabular envelope: there are no rows. The LLM branches on the literal mode: “async”.
The upstream job_url is the job’s only handle — pass it back to get_async_job_status / get_async_job_results / abort_async_job. There is deliberately no server-side job id: the server holds no per-job state, so nothing in this process can be reached by a caller who did not submit the job.
Errors (manna.errors)¶
- exception manna.errors.ToolExecutionError(error_class='internal_error', retry_strategy='abandon', message='', hint=None, retry_after_seconds=None, request_id=None)[source]¶
Bases:
ExceptionBase class — every concrete subclass sets error_class + retry_strategy.
Subclasses that should have their message redacted in the LLM-facing payload (e.g. internal server errors) override redact_message to True.
- Parameters:
- Return type:
None
- exception manna.errors.ValidationError(error_class='validation_error', retry_strategy='fix_and_retry', message='', hint=None, retry_after_seconds=None, request_id=None)[source]¶
Bases:
ToolExecutionError- Parameters:
- Return type:
None
- exception manna.errors.ArchiveError(error_class='archive_error', retry_strategy='wait_and_retry', message='', hint=None, retry_after_seconds=None, request_id=None)[source]¶
Bases:
ToolExecutionError- Parameters:
- Return type:
None
- exception manna.errors.TimeoutArchiveError(error_class='archive_error', retry_strategy='wait_and_retry', message='', hint=None, retry_after_seconds=None, request_id=None)[source]¶
Bases:
ArchiveErrorA sync request that exceeded the client timeout.
Distinct Python type so run_adql_query’s auto-promote path can branch on the failure mode directly, instead of substring-matching the error message. The wire contract is unchanged: error_class stays ‘archive_error’ and a sync-mode caller sees an ordinary archive_error.
- exception manna.errors.DalQueryError(error_class='tap_query_error', retry_strategy='fix_and_retry', message='', hint=None, retry_after_seconds=None, request_id=None)[source]¶
Bases:
ToolExecutionError- Parameters:
- Return type:
None
- exception manna.errors.JobNotReadyError(error_class='job_not_ready', retry_strategy='poll', message='', hint=None, retry_after_seconds=None, request_id=None)[source]¶
Bases:
ToolExecutionErrorRaised by get_async_job_results when the job exists but hasn’t finished yet.
The retry_strategy “poll” tells the LLM to call get_async_job_status in a loop instead of retrying get_async_job_results immediately.
- Parameters:
- Return type:
None
- exception manna.errors.JobGoneError(error_class='job_gone', retry_strategy='abandon', message='', hint=None, retry_after_seconds=None, request_id=None)[source]¶
Bases:
ToolExecutionErrorThe upstream job no longer exists (UWS 404/410).
Distinct from ArchiveError because the retry advice is the opposite: a job that has been deleted or aged out of the archive’s UWS store will never come back, so ‘wait_and_retry’ would loop the model forever. Since the server keeps no job registry, this upstream status is the only signal that a job_url is dead.
- Parameters:
- Return type:
None
- exception manna.errors.InternalError(error_class='internal_error', retry_strategy='abandon', message='', hint=None, retry_after_seconds=None, request_id=None)[source]¶
Bases:
ToolExecutionError- Parameters:
- Return type:
None
- manna.errors.error_to_payload(err, *, request_id=None)[source]¶
Convert any error into the LLM-facing payload shape.
Unknown exceptions (anything not a ToolExecutionError) are coerced into InternalError; the original exception is attached as __cause__ so the caller’s logger can still recover it.
The LLM-facing payload omits hint and retry_after_seconds when not present to save tokens; callers should use payload.get(…) on those keys.
- manna.errors.wrap_tool_errors(fn)[source]¶
Decorate an MCP tool so all errors flow through the structured payload path.
Catches ToolExecutionError subclasses (preserving error_class/retry_strategy) and bare Exception (coerced to InternalError with redacted message). Threads the current request_id from the ContextVar into the payload. Logs at WARN level for expected errors, EXCEPTION for unexpected.
Settings (manna.config)¶
- class manna.config.Settings(_case_sensitive=None, _nested_model_default_partial_update=None, _env_prefix=None, _env_prefix_target=None, _env_file=PosixPath('.'), _env_file_encoding=None, _env_ignore_empty=None, _env_nested_delimiter=None, _env_nested_max_split=None, _env_parse_none_str=None, _env_parse_enums=None, _cli_prog_name=None, _cli_parse_args=None, _cli_settings_source=None, _cli_parse_none_str=None, _cli_hide_none_type=None, _cli_avoid_json=None, _cli_enforce_required=None, _cli_use_class_docs_for_groups=None, _cli_exit_on_error=None, _cli_prefix=None, _cli_flag_prefix_char=None, _cli_implicit_flags=None, _cli_ignore_unknown_args=None, _cli_kebab_case=None, _cli_shortcuts=None, _secrets_dir=None, _build_sources=None, **values)[source]¶
Bases:
BaseSettings- Parameters:
_env_prefix_target (
Optional[Literal['variable','alias','all']])_cli_implicit_flags (
Union[bool,Literal['dual','toggle'],None])_cli_kebab_case (
Union[bool,Literal['all','no_enums'],None])_build_sources (
tuple[tuple[PydanticBaseSettingsSource,...],dict[str,Any]] |None)values (
Any)host (str)
port (int)
archives (str | None)
log_level (Literal['DEBUG', 'INFO', 'WARNING', 'ERROR'])
allowed_hosts (str | None)
tap_sync_timeout_seconds (float)
count_async_budget_seconds (float)
count_async_poll_interval_seconds (float)
async_wait_seconds (float)
async_wait_max_seconds (float)
inline_row_limit (int)
inline_byte_limit (int)
registry_describe_byte_limit (int)
- model_config = {'arbitrary_types_allowed': True, 'case_sensitive': False, 'cli_avoid_json': False, 'cli_enforce_required': False, 'cli_exit_on_error': True, 'cli_flag_prefix_char': '-', 'cli_hide_none_type': False, 'cli_ignore_unknown_args': False, 'cli_implicit_flags': False, 'cli_kebab_case': False, 'cli_parse_args': None, 'cli_parse_none_str': None, 'cli_prefix': '', 'cli_prog_name': None, 'cli_shortcuts': None, 'cli_use_class_docs_for_groups': False, 'enable_decoding': True, 'env_file': '.env', 'env_file_encoding': 'utf-8', 'env_ignore_empty': False, 'env_nested_delimiter': None, 'env_nested_max_split': None, 'env_parse_enums': None, 'env_parse_none_str': None, 'env_prefix': 'MANNA_', 'env_prefix_target': 'variable', 'extra': 'ignore', 'json_file': None, 'json_file_encoding': None, 'nested_model_default_partial_update': False, 'protected_namespaces': ('model_validate', 'model_dump', 'settings_customise_sources'), 'secrets_dir': None, 'toml_file': None, 'validate_default': True, 'yaml_config_section': None, 'yaml_file': None, 'yaml_file_encoding': None}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- manna.config.get_settings()[source]¶
Process-wide Settings singleton.
Cached so runtime consumers (the lazy backend accessors, the URL guard) read environment / .env once rather than re-parsing per call. Tests that mutate the environment must call
get_settings.cache_clear()to force a re-read.- Return type:
The underscore-prefixed modules below (manna.archives._model,
manna.archives._audit) are internal but stable enough to read; they are not
a supported import surface.
Archive notes model (manna.archives)¶
Archive model — the dataclasses one archive’s knowledge is built from.
An archive is the portable, plugin-style unit of archive notes: its identity + endpoints + usage_notes, together with the per-table Schema entries for that same archive. One archive = one file under archives/.
Archive and Schema live here so the model is a dependency-free leaf; the endpoint/schema helpers in archives/_endpoints.py and archives/_knowledge.py import them from here. Note (one atomic curated claim) and its Audit (how the live runner re-checks it) also live here — every usage_notes / Schema.notes entry is a Note, no other form accepted.
- class manna.archives._model.Pitfall(guidance, triggers=())[source]¶
Bases:
objectA pitfall: how a note’s claim gets DELIVERED to the model, and when.
A note in list_archives is knowledge the model can reach. A pitfall is knowledge we push at it, because the eval showed reachable isn’t enough (issue #57: the NRAO LOWER/UPPER note was true, probed, and served — and the model still wrote LOWER()). Like Audit, this is declarative: it carries no delivery code. archives/_pitfalls.py reads these.
Two kinds, split by whether the model can self-correct from the failure, and told apart entirely by
triggers:up-front note (no
triggers; called silent in the code) — the model gets NO usable correction signal, so the claim must arrive BEFORE the query. Either the query silently returns a wrong answer (ALMA: COUNT(*) over-counts, no error) or it errors so cryptically that the message doesn’t imply the fix (Data Lab: ADQL geometry surfaces as function point(…) does not exist, which never suggests q3c). These go in the run_adql_query description — the expensive channel, re-sent every turn, so the bar is high and guidance must be terse.error hint (
triggersset; called loud in the code) — the query throws, and the triggers recognise the cause in the submitted ADQL. guidance rides the error payload’s hint instead, so it costs nothing until it fires.
guidance is the compact, imperative fix — not the note’s full prose.
- class manna.archives._model.Note(id, text, audit, pitfall=None)[source]¶
Bases:
objectOne ATOMIC curated claim + the audit that re-checks it live.
id is a stable slug, unique within its owning archive — the address a stale audit prints so you can jump straight to the note to fix. text is the single-claim, LLM-facing prose surfaced by list_archives / describe_table. audit (mandatory) is how the live runner re-checks it. pitfall (optional) opts the claim into a push channel — see Pitfall.
- manna.archives._model.note_texts(notes)[source]¶
The LLM-facing strings for a tuple of notes, in order. Audit stays internal.
- class manna.archives._model.Schema(archive, table, missing_standard_columns=(), value_enums=<factory>, notes=(), cross_refs=())[source]¶
Bases:
objectArchive notes about ONE table at one archive.
archive is the owning archive’s short_name. It is redundant with the owning Archive.short_name (validated in Archive.__post_init__) but kept because it is part of the describe_table response contract and lets cross_refs name tables as (archive, table) pairs.
- Parameters:
- class manna.archives._model.Archive(short_name, display_name, host_substrings, tap_url=None, sia_url=None, scs_url=None, waveband=None, description='', notable_tables=<factory>, usage_notes=<factory>, schemas=<factory>, count_target=None, priority=100, paused=None)[source]¶
Bases:
objectEverything the server knows about ONE IVOA archive, in one place.
Identity + best-effort endpoints (tap_url / sia_url / scs_url are None when the archive doesn’t expose one we surface), plus:
usage_notes — short agent-facing strings capturing archive-specific gotchas (non-standard table locations, sync-vs-async routing, ADQL quirks, target-name conventions). Surfaced via list_archives.
schemas — curated per-table Schema facts for this archive. Surfaced via describe_table; not echoed by list_archives.
count_target — optional CountTarget: how to build a positional COUNT for this archive’s primary table (used by count_observations_near_target / survey_archives_for_target). None ⇒ not directly countable (still reachable via the atomic tools).
priority — ascending sort key (ties broken by short_name). The explicit replacement for the old “declaration order is load-bearing” convention: the first TAP-having archives become the endpoint examples shown to the LLM, so lower numbers are the archives we steer toward.
paused — None (default) or a dated, one-sentence reason. Non-None means the archive ships and is discoverable but is left OUT of the default active set; the reason is logged at boot. Naming the archive in MANNA_ARCHIVES activates it regardless — pausing is a default, not a lock. Use it when an archive’s service is being rebuilt or asks for less traffic, and you want to keep its notes for when it returns.
An archive is discovered by the registry (see archives/__init__.py) as the module-level ARCHIVE in an archives/<short_name>.py file. Dropping the file (or excluding it via MANNA_ARCHIVES) removes the server’s claims about that archive — never its reachability. See docs/archives-spec.md.
Audit — the falsifiable probe (or manual marker) a Note carries.
An Audit is the declarative half of a curated claim: how to re-check it live. It carries NO network code — the live runner (evals/audit.py) reads these and executes them. Keeping Audit pure keeps the model layer dependency-free.
- expect reuses the caveat vocabulary the probe engine already understands:
ok | error | empty | nonempty | count | manual
manual marks a claim that a single ADQL probe can’t check (download recipes, naming conventions, async-only behaviours) — it must carry a reason.
- manna.archives._audit.has_table(table)[source]¶
ADQL asking whether table is present in tap_schema.tables.
- manna.archives._audit.has_cols(table, columns)[source]¶
ADQL selecting which of columns are present on table.
Archive registry — discovery, selection, and the active-set accessor.
Each archives/<short_name>.py module exports a module-level ARCHIVE of
type Archive. This package discovers every archive physically present,
validates the full set, then narrows to the deployment’s active set via
MANNA_ARCHIVES (an optional comma-separated allow-list of short_names;
unset/empty => all).
Two ways to shape a deployment:
Physical — delete an
archives/<name>.pyfile (forking a deployment).Runtime — set
MANNA_ARCHIVES=datalab,almafrom a shared image.
Archives here are purely additive: absence removes the server’s claims about an archive (usage_notes, schema quirks, endpoint examples, cosmetic label), never its reachability. See docs/archives-spec.md.
get_active_archives() is cached like get_settings(); tests that toggle the
environment call get_active_archives.cache_clear().
- class manna.archives.Archive(short_name, display_name, host_substrings, tap_url=None, sia_url=None, scs_url=None, waveband=None, description='', notable_tables=<factory>, usage_notes=<factory>, schemas=<factory>, count_target=None, priority=100, paused=None)[source]¶
Bases:
objectEverything the server knows about ONE IVOA archive, in one place.
Identity + best-effort endpoints (tap_url / sia_url / scs_url are None when the archive doesn’t expose one we surface), plus:
usage_notes — short agent-facing strings capturing archive-specific gotchas (non-standard table locations, sync-vs-async routing, ADQL quirks, target-name conventions). Surfaced via list_archives.
schemas — curated per-table Schema facts for this archive. Surfaced via describe_table; not echoed by list_archives.
count_target — optional CountTarget: how to build a positional COUNT for this archive’s primary table (used by count_observations_near_target / survey_archives_for_target). None ⇒ not directly countable (still reachable via the atomic tools).
priority — ascending sort key (ties broken by short_name). The explicit replacement for the old “declaration order is load-bearing” convention: the first TAP-having archives become the endpoint examples shown to the LLM, so lower numbers are the archives we steer toward.
paused — None (default) or a dated, one-sentence reason. Non-None means the archive ships and is discoverable but is left OUT of the default active set; the reason is logged at boot. Naming the archive in MANNA_ARCHIVES activates it regardless — pausing is a default, not a lock. Use it when an archive’s service is being rebuilt or asks for less traffic, and you want to keep its notes for when it returns.
An archive is discovered by the registry (see archives/__init__.py) as the module-level ARCHIVE in an archives/<short_name>.py file. Dropping the file (or excluding it via MANNA_ARCHIVES) removes the server’s claims about that archive — never its reachability. See docs/archives-spec.md.
- class manna.archives.Note(id, text, audit, pitfall=None)[source]¶
Bases:
objectOne ATOMIC curated claim + the audit that re-checks it live.
id is a stable slug, unique within its owning archive — the address a stale audit prints so you can jump straight to the note to fix. text is the single-claim, LLM-facing prose surfaced by list_archives / describe_table. audit (mandatory) is how the live runner re-checks it. pitfall (optional) opts the claim into a push channel — see Pitfall.
- class manna.archives.Schema(archive, table, missing_standard_columns=(), value_enums=<factory>, notes=(), cross_refs=())[source]¶
Bases:
objectArchive notes about ONE table at one archive.
archive is the owning archive’s short_name. It is redundant with the owning Archive.short_name (validated in Archive.__post_init__) but kept because it is part of the describe_table response contract and lets cross_refs name tables as (archive, table) pairs.
- Parameters:
- manna.archives.discover_archives()[source]¶
Import every archive module in this package and collect its
ARCHIVE.Modules whose name starts with
_are skipped: the leaf dataclasses and pure helpers (_model,_select,_audit) plus the derived-view helper modules (_endpoints,_knowledge) — none of them define anARCHIVE. A non-underscore module without anARCHIVEattribute is a developer error and raises. The returned tuple is validated (_select.validate_archives()) and sorted by (priority, short_name).