# Python API reference

Source: https://getanalog.io/docs/api-reference/

Compact reference for the analog package's public calls, results, fetchers, and exceptions.

Start with `analog(url)` to view a page and save its result. Use
[`assess(url)`](https://getanalog.io/docs/assess/) for help choosing between a structured view
and local Markdown. The [Fetching](https://getanalog.io/docs/fetching/) and
[Working with results](https://getanalog.io/docs/results/) guides explain the task-level choices; this
page is the compact lookup for names and parameters.

The declaration blocks below show parameter types, defaults, and return
types. They describe the API; the accompanying examples show runnable calls.

<details class="reference-index">
<summary>On this page</summary>

- [View a page](https://getanalog.io/docs/api-reference/#view-a-page) and [assess a page](https://getanalog.io/docs/api-reference/#assess-a-page)
- [Client and fetchers](https://getanalog.io/docs/api-reference/#client-and-fetchers): [service calls](https://getanalog.io/docs/api-reference/#call-the-service-directly),
  [browser](https://getanalog.io/docs/api-reference/#configure-the-browser), [HTTP](https://getanalog.io/docs/api-reference/#fetch-a-static-page),
  [robots rules](https://getanalog.io/docs/api-reference/#check-robots-rules), [feedback](https://getanalog.io/docs/api-reference/#submit-feedback)
- [Read a response](https://getanalog.io/docs/api-reference/#read-a-response): [preview](https://getanalog.io/docs/api-reference/#preview-a-response-or-section),
  [select sections](https://getanalog.io/docs/api-reference/#select-sections), [search](https://getanalog.io/docs/api-reference/#search-the-page-or-a-section),
  [serialize](https://getanalog.io/docs/api-reference/#serialize-the-result), [numeric values](https://getanalog.io/docs/api-reference/#read-numeric-values),
  [DataFrames](https://getanalog.io/docs/api-reference/#use-a-dataframe)
- [Saved-result store](https://getanalog.io/docs/api-reference/#saved-result-store): [save and name](https://getanalog.io/docs/api-reference/#save-and-name-a-result),
  [reopen](https://getanalog.io/docs/api-reference/#reopen-a-saved-result), [inspect](https://getanalog.io/docs/api-reference/#inspect-the-local-store),
  [edit fields](https://getanalog.io/docs/api-reference/#rename-and-order-fields), [remove](https://getanalog.io/docs/api-reference/#remove-saved-results)
- [Complete root export inventory](https://getanalog.io/docs/api-reference/#complete-root-export-inventory)

</details>

<!-- BEGIN GENERATED: sdk-api-reference -->
<!-- Generated by tools/sdk_api_reference/render.py; edit the source API, guidance templates, or renderer. -->

## Extract and assess



## View a page

`analog()` returns an `AnalogResponse`. By default it renders the page in
Analog's browser, gives you a structured view, and saves the result locally.
[Sign in](https://getanalog.io/docs/authenticate/) before calling it in the default mode.

### `analog`

Turn a webpage into something you can understand and use.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
analog(
    url: str,
    *,
    html: str | None = None,
    fetcher: Fetcher | None = None,
    base_url: str | None = None,
    save: bool = True,
    reveal_all: bool = True,
    load_all: bool = False,
    expand_all: bool = False,
    mode: Mode = 'auto',
    pages: int = 1,
    allow_private: bool = False,
) -> AnalogResponse
```
<!-- END GENERATED: sdk-api-declaration -->


`mode="auto"` includes advice about reading the page; it still produces the
structured result when that advice favors Markdown. `mode="structured"`
omits the assessment. `mode="local"` produces Markdown locally without an
account or backend request.

Use `save=False` when you do not need a saved result. For supplied HTML,
custom fetchers, browser controls, and pagination, see [Fetching](https://getanalog.io/docs/fetching/).



### Get a response

```python example=runnable scenario=python-reference-response
from analog import analog

result = analog("https://quotes.toscrape.com/js/")
print(result.preview())
```

Keep `result` for the examples in [Read a response](https://getanalog.io/docs/api-reference/#read-a-response).
The preview identifies the quote records and the page's navigation.
The output shows the returned preview; fetch progress is reported separately.
The live page may change.

<details>
<summary>View output</summary>

```text wrap=true example=output scenario=python-reference-response
pagination: this looks like page 1 of a paginated collection (its links reach page 2) — these records cover this page only.

3 sections extracted.

section[0]  10 records · 3 fields
  fields: tags:text[], text:text, text_2:text

section[1]  navigation  2 links (header) · 4 fields
  fields: text:text, url:url, group:text, depth:text

section[2]  navigation  2 links (footer) · 4 fields
  fields: text:text, url:url, group:text, depth:text

(A single-subject record was also weighed and withheld — Analog couldn't verify enough of the page's facts for a record we'd trust.)
```

</details>





## Assess a page

`assess()` returns a `FitAssessment` with advice about how to read the page.
It fetches the page and assesses it locally, without calling Analog's
backend or requiring an account. It does not return page records or save
an `AnalogResponse`.

### `assess`

Assess whether a page is worth a structured Analog extraction.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
assess(
    url: str,
    *,
    html: str | None = None,
    fetcher: Fetcher | None = None,
    reveal_all: bool = True,
    load_all: bool = False,
    expand_all: bool = False,
    probe_feed: bool = True,
    allow_private: bool = False,
) -> FitAssessment
```
<!-- END GENERATED: sdk-api-declaration -->


Read `guidance` for the explanation and possible next steps.
`recommendation` is the default steer for a one-off read, not a substitute
for choosing based on your task. See [Assess a page](https://getanalog.io/docs/assess/).

`probe_feed=False` skips the optional feed and sitemap probes; the page
itself is still fetched. Supplying `html` avoids that page fetch.



### Read the advice

```python example=runnable scenario=python-reference-assess
from analog import assess

assessment = assess("https://quotes.toscrape.com/js/")
print(assessment.guidance)
```

Advice depends on the fetched page.

<details>
<summary>View output</summary>

```text wrap=true example=output scenario=python-reference-assess
This page as markdown: ~426 tokens (measured). Repeating structure: substantial.
- pagination: this looks like page 1 of a paginated collection (its links reach page 2) — these records cover this page only. Analog can follow the site's own next links and merge the pages into one result; pass pages=2 to analog(...).
Task-by-task guidance (records vs markdown vs feeds): https://getanalog.io/docs/assess/
```

</details>

The pagination hint uses Python arguments. Use
`assessment.guidance_for("shell")` or `assessment.guidance_for("mcp")`
when presenting the advice through those surfaces.




## Client and fetchers

For most tasks, use [`analog()`](https://getanalog.io/docs/api-reference/#analog). Use `Client` when your program
needs to call the service directly, and a fetcher when it needs to control
how the webpage is downloaded. Fetchers return HTML; the client returns a
structured response.



## Call the service directly

`Client` reads the credential stored after [signing in](https://getanalog.io/docs/authenticate/).
There is no API-key constructor argument. Use a context manager to close
the client's connection pool when you are finished.

### `Client`

Synchronous Analog client.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
Client(
    *,
    base_url: str | None = None,
    timeout: float = 60.0,
    max_retries: int = 3,
    http_client: httpx.Client | None = None,
)
```
<!-- END GENERATED: sdk-api-declaration -->


`timeout` is in seconds. `max_retries` bounds automatic retries for eligible
requests. An injected `http_client` must have its own `base_url`; your code
owns and closes that HTTP client. A stored credential is sent only to the
backend origin for which it was issued.

### `Client.extract`

Send HTML to the backend for extraction.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
Client.extract(*, html: str, url: str) -> AnalogResponse
```
<!-- END GENERATED: sdk-api-declaration -->


This call sends the supplied HTML to Analog. It neither fetches the URL nor
saves the returned response. Pass the address that served the HTML, including
any redirect, as `url`.

For the complete page view with locally rendered Markdown and automatic
saving, prefer `analog(url, html=html)`. `Client.extract()` returns the
service response without those local additions.

### `Client.info`

Fetch backend version + wire schema version. Lets the caller warn on skew.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
Client.info() -> InfoResponse
```
<!-- END GENERATED: sdk-api-declaration -->


`info()` makes an authenticated service request. Its response includes the
service version, wire schema version, and minimum supported SDK version.



### Send HTML you fetched

```python example=runnable scenario=python-reference-client-extract
from analog import Client, HttpFetcher

with (
    HttpFetcher() as fetcher,
    Client(timeout=60) as client,
):
    page = fetcher.fetch(
        "https://quotes.toscrape.com/"
    )
    response = client.extract(
        html=page.html, url=page.resolved_url
    )
    print(response.preview())
```

This fetches the static quotes page over HTTP, then sends its HTML to the
service. The preview shows the returned record sections.

The output shows the full returned preview; the live page may change.

<details>
<summary>View output</summary>

```text wrap=true example=output scenario=python-reference-client-extract
2 sections extracted.

section[0]  10 records · 5 fields
  fields: tags_2:text[], text:text, text_2:text, about_url:url, tags:url[]

section[1]  navigation  2 links (footer) · 4 fields
  fields: text:text, url:url, group:text, depth:text

page outline:
  not extracted: "Top Ten tags" (unknown, 10 items) — read as page structure, not records

(A single-subject record was also weighed and withheld — Analog couldn't verify enough of the page's facts for a record we'd trust.)
```

</details>

### Read service compatibility information

```python example=runnable scenario=python-reference-client-info
from analog import Client

with Client() as client:
    info = client.info()
print("Schema:", info.schema_version)
print("Minimum SDK:", info.min_supported_sdk)
```

```text wrap=true example=output scenario=python-reference-client-info
Schema: 29
Minimum SDK: 0.19.0
```

These values can change as the service is updated.





## Configure the browser

`Browser` renders JavaScript and returns a `FetchResult` containing HTML,
the final URL, HTTP status, and headers. Use `fetch()` directly when you
need HTML, or pass the instance as `analog(..., fetcher=browser)` to get
Analog's structured response.

### `Browser`

JS-aware `analog.fetcher.Fetcher` powered by Playwright.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
Browser(
    *,
    timeout: float = 30.0,
    wait_for: str | None = None,
    wait_for_load_state: Literal['commit', 'domcontentloaded', 'load', 'networkidle'] = 'load',
    additional_wait_ms: int = 0,
    scroll: bool = True,
    reveal_all: bool = True,
    load_all: bool = False,
    expand_all: bool = False,
    page_batches: int = 1,
    max_scroll_iterations: int | None = None,
    headless: bool = True,
    user_agent: str | None = None,
    use_system_chrome: bool = False,
    robots_checker: RobotsChecker | None = None,
    allow_private: bool = False,
    checkpoint_sink: FetchCheckpointSink | None = None,
)
```
<!-- END GENERATED: sdk-api-declaration -->


Construction does not launch the browser; the first fetch does. Reuse the
instance for several URLs on the same thread, and close it with a context
manager or `close()` when finished.

`wait_for` waits for a CSS selector before capture. `timeout` applies to
individual navigation waits, not the whole fetch. For scrolling, load-more
controls, and per-item expansion, see [Fetching](https://getanalog.io/docs/fetching/). Configure
those options on your Browser instance when passing an explicit fetcher.



### Wait for the quotes to appear

```python example=runnable scenario=python-reference-browser-fetch
from analog import Browser

with Browser(wait_for=".quote") as browser:
    rendered_page = browser.fetch(
        "https://quotes.toscrape.com/js/"
    )
print(rendered_page.status)
print(rendered_page.resolved_url)
```

```text wrap=true example=output scenario=python-reference-browser-fetch
200
https://quotes.toscrape.com/js/
```

`rendered_page.html` contains the rendered HTML;
this example prints the response status and final address. Waiting for
`.quote` makes the capture wait for a quote element on this particular page.

For a structured view of the same page, see [View a page](https://getanalog.io/docs/api-reference/#view-a-page).





## Fetch a static page

`HttpFetcher` downloads HTML over HTTP and follows redirects. It does not
run JavaScript or interact with the page. Choose it when the initial HTML
already contains the content you need.

### `HttpFetcher`

Minimal httpx-based fetcher — single GET, follows redirects.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
HttpFetcher(
    *,
    timeout: float = 30.0,
    user_agent: str | None = None,
    robots_checker: RobotsChecker | None = None,
    max_response_bytes: int | None = 26214400,
    allow_private: bool = False,
)
```
<!-- END GENERATED: sdk-api-declaration -->


Reuse an instance across calls and close its connection pool with a context
manager or `close()`. `timeout` is in seconds; `max_response_bytes` limits
the decompressed response body.

Both built-in fetchers consult `robots.txt`. A rule refusal raises
`RobotsTxtDisallowedError`; an unreachable rules file raises
`RobotsTxtUnreachableError`. See [fetching limitations](https://getanalog.io/docs/limitations/).



### Download HTML without rendering JavaScript

```python example=runnable scenario=python-reference-http-fetch
from analog import HttpFetcher

with HttpFetcher(timeout=15) as fetcher:
    static_page = fetcher.fetch("https://quotes.toscrape.com/")
print(static_page.status)
print(static_page.resolved_url)
```

```text wrap=true example=output scenario=python-reference-http-fetch
200
https://quotes.toscrape.com/
```

`static_page.html` contains the downloaded HTML.
Use this site's static `/` page here; its `/js/` counterpart needs a browser.

For the structured response from an HTTP fetch, see
[Choose a fetch path](https://getanalog.io/docs/fetching/#choose-a-fetch-path).





## Check robots rules

Built-in fetchers create a checker automatically. Construct one explicitly
when you need to inspect rules or share their cache across fetchers. Use
the same user agent for the checker and the requests it governs.

### `RobotsChecker`

Checks URLs against per-origin `robots.txt` rules.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
RobotsChecker(
    user_agent: str,
    *,
    cache_ttl_seconds: float = 86400,
    unreachable_retry_seconds: float = 300,
    timeout: float = 10.0,
    allow_private: bool = False,
)
```
<!-- END GENERATED: sdk-api-declaration -->


### `RobotsChecker.check`

Raise `RobotsTxtDisallowedError` if `robots.txt` disallows this URL.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
RobotsChecker.check(url: str) -> None
```
<!-- END GENERATED: sdk-api-declaration -->


`check()` returns `None` when access is allowed. It raises
`RobotsTxtDisallowedError` for a rule refusal and `RobotsTxtUnreachableError`
when the rules cannot be determined.

### `RobotsChecker.sitemaps`

The `Sitemap:` URLs declared by the origin's `robots.txt`.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
RobotsChecker.sitemaps(url: str) -> list[str] | None
```
<!-- END GENERATED: sdk-api-declaration -->


`sitemaps()` returns the declared URLs, `[]` when a readable rules file
has no sitemaps, or `None` when the rules are unavailable. These methods
share a per-origin cache; they may fetch `robots.txt` when a cached entry
is absent or has expired.



### Check the quotes page

```python example=runnable scenario=python-reference-robots-check
from analog import RobotsChecker
from analog.fetcher import DEFAULT_USER_AGENT

checker = RobotsChecker(DEFAULT_USER_AGENT)
url = "https://quotes.toscrape.com/"
checker.check(url)
print("Allowed by robots.txt")
print("Sitemaps:", checker.sitemaps(url))
```

```text wrap=true example=output scenario=python-reference-robots-check
Allowed by robots.txt
Sitemaps: []
```

The check allowed the page; no sitemap URLs were declared. You can pass
this checker as `robots_checker=checker` to a fetcher using the same user agent.





## Submit feedback

This authenticated call sends a report to Analog and returns a receipt.
For the usual reporting flow and supported quality labels, see
[Feedback](https://getanalog.io/docs/feedback/).

### `Client.submit_feedback`

Send one feedback submission and return its durable receipt.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
Client.submit_feedback(payload: FeedbackRequest) -> FeedbackResponse
```
<!-- END GENERATED: sdk-api-declaration -->


A `feature_request` requires a non-empty `note` and carries no URL or
labels. An `extraction_quality` report requires a URL and at least one
supported label. It shares the submitted URL and note, without attaching
page HTML or records.



### Send a feature request

Replace the placeholder with your request. Running this sends it to Analog.

```python example=illustrative
from analog import Client, FeedbackRequest

request = FeedbackRequest(
    kind="feature_request",
    note="<your-request>",
)
with Client() as client:
    receipt = client.submit_feedback(request)
print(receipt.report_id)
```

The returned `report_id` identifies the recorded submission.




## Read a response

The examples below reuse `result` from [View a page](https://getanalog.io/docs/api-reference/#view-a-page).
They read that response locally, without fetching again. In this capture,
`result.structured_content[0]` is the section with ten quotes. On another
page, search for known content or request a preview when orientation helps.

`AnalogResponse` keeps record sections in `structured_content`, combined
record views in `collections`, and the mixed record-and-Markdown reading
order in `sections`, described by `document_section_plan`. Its `outline`
and `warnings` provide page context and coverage notes. `handle` identifies
the saved result when one was created.

`result.language_observations` holds raw root `<html lang>` declarations,
in retained page or batch order. Each `LanguageObservation` contains its
capture `url` and exact `html_lang`: `None` means the attribute was observed
absent; `""` means present but empty. A `None` entry means that capture was
unrecorded; a `None` trail means no trail was saved, including older results.
These are page-authored declarations, not inferred or verified languages.
Saved observations do not configure browsing or extraction. Saves retain them locally;
they are not added to the extraction service's wire payload.



## Preview a response or section

Use `result.preview()` when a page overview helps, or `Section.preview()`
to inspect one record section's shape. Previews show field names and types,
`[]` for multivalued fields, and exact non-null coverage when sparse.
Values remain in `records` and `field_stats()` samples.

The page preview includes Markdown regions in reading order, showing their
available labels, roles, and rendered sizes. Their text remains in
`result.sections` and `result.markdown`. Printed `section[N]` identifiers
always refer to `result.structured_content[N]`, even when prose appears before
that record section.

### `AnalogResponse.preview`

Token-optimized page overview for LLM agents.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
AnalogResponse.preview(
    *,
    complete: bool = False,
    voice: Voice = 'python',
    guidance: bool = False,
) -> str
```
<!-- END GENERATED: sdk-api-declaration -->


### `Section.preview`

Token-optimized overview of this section.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
Section.preview(
    *,
    complete: bool = False,
    voice: Voice = 'python',
    guidance: bool = False,
) -> str
```
<!-- END GENERATED: sdk-api-declaration -->


`complete=True` includes hidden fields in the summary;
it does not turn the preview into a full record export. `guidance=True`
may add one available next action, phrased in the selected `voice`:
Python, shell, or MCP. Guidance is off by default and changes no field metadata,
coverage notes or necessary recovery. `PaginationInfo.describe()` and
`PageSweep.describe()` accept the same opt-in for their pagination advice.

### `Section.field_stats`

Per-field statistics for this section — the data behind `describe`.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
Section.field_stats() -> list[FieldStat]
```
<!-- END GENERATED: sdk-api-declaration -->


`field_stats()` reports each field's coverage and number of distinct
values, including fields omitted from the preview summary. Coverage is the
fraction of records with a non-null value.



### Preview the quotes

Use `result` from [Get a response](https://getanalog.io/docs/api-reference/#get-a-response), which views the quotes page.

```python example=runnable scenario=python-reference-section-preview
quotes = result.structured_content[0]
print(quotes.preview())
```

```text wrap=true example=output scenario=python-reference-section-preview
10 records · 3 fields
  fields: tags:text[], text:text, text_2:text
```

### Check field coverage

Use the `quotes` section defined in [Preview the quotes](https://getanalog.io/docs/api-reference/#preview-the-quotes).

```python example=runnable scenario=python-reference-field-stats
for field in quotes.field_stats():
    print(
        f"{field.name}: coverage={field.coverage:.0%}, "
        f"distinct={field.cardinality}"
    )
```

```text wrap=true example=output scenario=python-reference-field-stats
tags: coverage=100%, distinct=10
text: coverage=100%, distinct=10
text_2: coverage=100%, distinct=8
```

All ten quotes have an author, but some authors appear more than once.





## Select sections

`sections` follows the page's reading order and can contain both record
Sections and Markdown Sections. `structured_content` contains only record
Sections, including navigation. The two lists need not have matching indices.

### `AnalogResponse.sections`

Record and Markdown Sections together in document order.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
AnalogResponse.sections: list[Section | MarkdownSection]
```
<!-- END GENERATED: sdk-api-declaration -->


### `AnalogResponse.sections_by_kind`

Every extracted section classified as `kind`, in document order.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
AnalogResponse.sections_by_kind(kind: str) -> list[Section]
```
<!-- END GENERATED: sdk-api-declaration -->


### `AnalogResponse.section`

The extracted section whose `label` matches, or `None`.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
AnalogResponse.section(label: str) -> Section | None
```
<!-- END GENERATED: sdk-api-declaration -->


Kind and label matching ignore case and surrounding whitespace.
`sections_by_kind()` returns every matching record Section, or `[]`.
`section()` returns the first matching record Section, or `None`.
An empty kind or label matches nothing. Inspect the response's actual
kinds and labels before relying on either accessor.



### Inspect the available sections

```python example=runnable scenario=python-reference-select-sections
for index, section in enumerate(result.sections):
    kind = section.kind or "(no kind)"
    print(index, type(section).__name__, kind)
navigation = result.sections_by_kind("navigation")
print("Navigation sections:", len(navigation))
named = result.section("Quotes")
print("Section named Quotes:", named)
```

```text wrap=true example=output scenario=python-reference-select-sections
0 Section (no kind)
1 Section navigation
2 Section navigation
Navigation sections: 2
Section named Quotes: None
```

The quote section has no label in this response, so looking it up as
`"Quotes"` returns `None`. The numbered record section remains available
as `result.structured_content[0]`.





## Search the page or a section

A string performs a case-insensitive substring search. Use `re.compile()`
to supply a regular expression and choose its flags.

### `AnalogResponse.find`

Search record and Markdown Sections for matching content.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
AnalogResponse.find(
    pattern: str | re.Pattern[str],
    field: str | None = None,
) -> list[dict[str, FieldValue]]
```
<!-- END GENERATED: sdk-api-declaration -->


Page-wide hits include `_source` to identify where they came from.
Without a `field`, the search can also return Markdown excerpts and
matching outline labels. With a `field`, it searches records only and
raises `KeyError` if no section has that field.

### `Section.find`

Search this section's records for matching values.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
Section.find(
    pattern: str | re.Pattern[str],
    field: str | None = None,
) -> list[dict[str, FieldValue]]
```
<!-- END GENERATED: sdk-api-declaration -->


A Section search returns full matching records without adding `_source`.
It raises `KeyError` when the requested field is absent from that section.
Both searches return `[]` when nothing matches.



### Find an author across the page

```python example=runnable scenario=python-reference-find-page
import json

hits = result.find("Albert Einstein", field="text_2")
print(json.dumps(hits, ensure_ascii=False, indent=2))
```

<details>
<summary>View output</summary>

```json wrap=true example=output scenario=python-reference-find-page
[
  {
    "tags": [
      "change",
      "deep-thoughts",
      "thinking",
      "world"
    ],
    "text": "“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”",
    "text_2": "by Albert Einstein",
    "_source": "structured_content[0]"
  },
  {
    "tags": [
      "inspirational",
      "life",
      "live",
      "miracle",
      "miracles"
    ],
    "text": "“There are only two ways to live your life. One is as though nothing is a miracle. The other is as though everything is a miracle.”",
    "text_2": "by Albert Einstein",
    "_source": "structured_content[0]"
  },
  {
    "tags": [
      "adulthood",
      "success",
      "value"
    ],
    "text": "“Try not to become a man of success. Rather become a man of value.”",
    "text_2": "by Albert Einstein",
    "_source": "structured_content[0]"
  }
]
```

</details>

### Find quotes with a topic

```python example=runnable scenario=python-reference-find-section
import json

hits = quotes.find("change", field="tags")
print(json.dumps(hits, ensure_ascii=False, indent=2))
```

```json wrap=true example=output scenario=python-reference-find-section
[
  {
    "tags": [
      "change",
      "deep-thoughts",
      "thinking",
      "world"
    ],
    "text": "“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”",
    "text_2": "by Albert Einstein"
  }
]
```

This searches the values in the `tags` field, which holds a list in each
record. It returns the full quote, author, and tags for the matching record.





## Serialize the result

Choose JSON to retain the page's sections and their context. YAML and CSV
provide a flat records view and require compatible section schemas.

### `AnalogResponse.to_json`

Self-contained JSON serialization of the result.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
AnalogResponse.to_json() -> str
```
<!-- END GENERATED: sdk-api-declaration -->


The JSON includes per-section records, field information, and rendered
Markdown. `model_dump_json()` instead serializes the underlying model
without adding the rendered Markdown views.

### `AnalogResponse.to_yaml`

YAML serialization of the extracted records.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
AnalogResponse.to_yaml() -> str
```
<!-- END GENERATED: sdk-api-declaration -->


### `AnalogResponse.to_csv`

CSV serialization of the extracted records.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
AnalogResponse.to_csv() -> str
```
<!-- END GENERATED: sdk-api-declaration -->


YAML and CSV raise `AnalogIncompatibleSectionsError` when sections cannot
be pooled. That includes this example page: quote records and navigation
links have different fields. To work with just the quotes, use
`quotes.records` or [a section DataFrame](https://getanalog.io/docs/api-reference/#use-a-dataframe).



### Write the complete JSON view

```python example=runnable scenario=python-reference-write-json
from pathlib import Path

_ = Path("quotes-page.json").write_text(result.to_json(), encoding="utf-8")
```

This creates or replaces `quotes-page.json` in the working directory.
It writes the whole result, including the quote and navigation sections;
there is no terminal output. See [Working with results](https://getanalog.io/docs/results/)
for choosing among record and document views.





## Read numeric values

A numeric column supplies numbers in record order while leaving
the original display strings in `records`.

### `Section.numeric`

The normalized numeric column for `field`, aligned with `records`.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
Section.numeric(field: str) -> list[float | None]
```
<!-- END GENERATED: sdk-api-declaration -->


### `Collection.numeric`

Normalized numeric values for each canonical record, when available.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
Collection.numeric(field: str) -> list[float | None]
```
<!-- END GENERATED: sdk-api-declaration -->


Each value is a number or `None` when that record has no parsed number.
A field without a numeric column returns `[]`. A Collection combines
compatible Sections into one records view, removing repeated copies of the
same record. Original placements remain in `structured_content`. Its numeric
column follows the order of `collection.records`.



### Read a price column

This illustrates a section with a numeric `price` field, rather than the
quotes example. Check `section.fields` to find the available fields and
which have `numeric=True`.

```python example=illustrative fragment=true
for record, price in zip(section.records, section.numeric("price")):
    print(record["price"], price)
```

The first value is the source display text; the second is the number to
use for comparisons. For a Collection, use `collection.records` and
`collection.numeric("price")` together in the same way.





## Use a DataFrame

Install the optional DataFrame dependency in your Python project:

```bash example=runnable scenario=python-reference-dataframe-install
uv add "analog-sdk[dataframe]"
```

### `AnalogResponse.to_dataframe`

Every record on the page as one pandas DataFrame.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
AnalogResponse.to_dataframe() -> pd.DataFrame
```
<!-- END GENERATED: sdk-api-declaration -->


The page-level method requires compatible schemas. It raises
`AnalogIncompatibleSectionsError` for this example's mixture of quotes and
navigation, so choose the quote section instead.

### `Section.to_dataframe`

This section's records as a pandas DataFrame.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
Section.to_dataframe() -> pd.DataFrame
```
<!-- END GENERATED: sdk-api-declaration -->


Numeric columns become floats, with missing values represented by `NaN`.
Other fields keep their values, including lists. Original display strings
remain in `records`; column units are available through `frame.attrs["units"]`.



### Work with the quote section

```python example=runnable scenario=python-reference-dataframe
frame = quotes.to_dataframe()
print(frame[["text_2"]].head(3).to_string(index=False))
```

```text wrap=true example=output scenario=python-reference-dataframe
            text_2
by Albert Einstein
   by J.K. Rowling
by Albert Einstein
```

This displays just the first three authors. `frame` contains every quote
record and all three fields, without the page's navigation records.




## Saved-result store

These functions operate on the local saved-result store without calling
the service or fetching a webpage. The examples below reuse `result` from
[View a page](https://getanalog.io/docs/api-reference/#view-a-page), save a new working artifact, then read, edit,
and remove that artifact.

These examples start with an empty store. Your handles, history,
and disk usage will differ. Choose another name if `quotes-work` is already
taken in your store.



## Save and name a result

`analog()` saves by default. Call `save()` explicitly when you used
`save=False` or want another saved artifact from a response you already have.

### `save`

Persist `response` as a new artifact and return its handle.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
save(
    response: AnalogResponse,
    *,
    url: str,
    fetch_settings: FetchSettings | None = None,
    partial: bool = False,
    name: str | None = None,
) -> str
```
<!-- END GENERATED: sdk-api-declaration -->


Each call creates a new artifact and sets `response.handle` to its handle.
It stores records and Markdown, never raw HTML. Saving can evict the
least-recently-opened artifacts when the store exceeds its configured caps.
Previously recorded field names and ordering for the URL may be applied
to the response as it is saved.

### `rename`

Attach a friendly `name` to a saved result.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
rename(handle: str, name: str) -> None
```
<!-- END GENERATED: sdk-api-declaration -->


A friendly name is an alternative to the handle. Names must be unique,
start with a letter or digit, and contain only letters, digits, `-`, or `_`,
up to 64 characters. `latest` and handle-shaped names are reserved.
An invalid or already-used name raises `InvalidResultNameError` or
`ResultNameInUseError` respectively.



### Save a working result

```python example=runnable scenario=python-reference-store-save
from analog import results

working_handle = results.save(
    result, url="https://quotes.toscrape.com/js/"
)
results.rename(working_handle, "quotes-work")
print(working_handle)
```

```text wrap=true example=output scenario=python-reference-store-save
20260912-2zult7
```

`working_handle` refers to the new save. The original saved artifact remains
available under its original handle unless normal store eviction removes it.
The response object's `handle` now refers to this new artifact.





## Reopen a saved result

Use `open()` with a handle, friendly name, or `"latest"` to restore an
`AnalogResponse`. The returned object has the same reading APIs described
in [Read a response](https://getanalog.io/docs/api-reference/#read-a-response).

### `open`

Re-hydrate a saved result by handle, friendly name, or `"latest"`.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
open(handle: str) -> AnalogResponse
```
<!-- END GENERATED: sdk-api-declaration -->


An unknown reference raises `ResultNotFoundError`. Unreadable saved data raises
`ResultSchemaDriftError`; this does not establish whether an upgrade or damaged
data caused the failure. The error carries the saved `handle`, `url`, and
recorded `fetcher` kind (`None` when unknown).

Recovery guidance respects that source: supplied pages need their original
HTML or a new capture of the intended browser state; browser-backed saves
can be fetched again. Custom fetchers must be supplied again by the caller.
The store retains neither the input file nor its path. A new acquisition does
not restore earlier browser state or capture settings, and opening never runs
recovery automatically. A successful open updates the result's last-opened time.

### `latest`

Re-hydrate the most recently created result, or `None` if empty.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
latest() -> AnalogResponse | None
```
<!-- END GENERATED: sdk-api-declaration -->


### `latest_handle`

The handle of the most recently created result, or `None`.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
latest_handle() -> str | None
```
<!-- END GENERATED: sdk-api-declaration -->


These refer to the most recently **created** result, regardless of which
one was opened last. Both return `None` for an empty store; `open("latest")`
raises `ResultNotFoundError` instead. Use `latest_handle()` when you need
only the identifier.



### Read the named save

```python example=runnable scenario=python-reference-store-open
saved = results.open("quotes-work")
print(saved.structured_content[0].records[0]["text"])
```

```text wrap=true example=output scenario=python-reference-store-open
“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”
```

This reads the full quote from the saved records, without visiting the site.

### Open the newest save if one exists

```python example=runnable scenario=python-reference-store-latest
newest = results.latest()
if newest is not None:
    print(newest.handle)
```

```text wrap=true example=output scenario=python-reference-store-latest
20260912-2zult7
```

Here, the working result is still the newest save. Creating another result
would change what `latest()` returns; opening an older one would not.





## Inspect the local store

`history()` returns metadata for every saved result, newest-created first.
It does not open each result or download any page.

### `history`

All saved results, newest first.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
history() -> list[ResultMeta]
```
<!-- END GENERATED: sdk-api-declaration -->


Metadata includes the handle, friendly name, URL, creation and last-opened
times, section and record counts, and recorded size. `partial` identifies
an incomplete capture. `language_observations` retains the
[raw declaration trail](https://getanalog.io/docs/api-reference/#read-a-response), also restored on `open()`.
It is separate from `fetch_settings`: observations describe captured documents,
not the settings that requested them. Plain supplied HTML and fetchers that
record no observations remain unrecorded; verified browser-capture files
retain their observed root declaration.

### `store_stats`

The store's disk footprint: entry count, total bytes, and the caps.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
store_stats() -> StoreStats
```
<!-- END GENERATED: sdk-api-declaration -->


`store_stats()` reports the count, recorded bytes, and effective limits.
The limits honor `ANALOG_RESULTS_MAX_COUNT` and `ANALOG_RESULTS_MAX_BYTES`.

### `results_dir`

Resolve the directory holding saved result artifacts.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
results_dir() -> Path
```
<!-- END GENERATED: sdk-api-declaration -->


`results_dir()` returns a `Path` without creating the directory. The store
lives under `ANALOG_CACHE_DIR` when set, otherwise under the platform's
cache directory, and ends in `results/`. See
[Working with results](https://getanalog.io/docs/results/) for storage and retention behavior.



### List saved results

```python example=runnable scenario=python-reference-store-history
for meta in results.history():
    print(meta.name or meta.handle, meta.records, meta.url)
```

```text wrap=true example=output scenario=python-reference-store-history
quotes-work 14 https://quotes.toscrape.com/js/
```

The count includes the ten quotes and four navigation records.

### Read the size and limits

```python example=runnable scenario=python-reference-store-stats
print(results.store_stats().model_dump_json(indent=2))
```

```json wrap=true example=output scenario=python-reference-store-stats
{
  "count": 1,
  "bytes": 11260,
  "max_count": 500,
  "max_bytes": 536870912
}
```

`bytes` is the store's recorded artifact size. The limits govern eviction
when another result is saved.





## Rename and order fields

These functions edit the saved artifact and return the updated response.
Use the returned object, or reopen the save, to read those edits.

### `rename_fields`

Rename fields on a saved result, persisted in place.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
rename_fields(
    handle: str,
    renames: dict[str, str],
    *,
    sticky: bool = True,
) -> AnalogResponse
```
<!-- END GENERATED: sdk-api-declaration -->


Renames apply to matching fields in every record section. An unknown source
field raises `KeyError`.

### `reorder_fields`

Put `order`'s fields first on a saved result, persisted in place.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
reorder_fields(handle: str, order: list[str], *, sticky: bool = True) -> AnalogResponse
```
<!-- END GENERATED: sdk-api-declaration -->


Named fields move to the front in the supplied order; remaining fields keep
their order. Sections with none of those fields stay unchanged. Unknown
fields raise `KeyError`; duplicate names in the order raise `ValueError`.

Both functions default to `sticky=True`, recording the change for future
saves of the same page. Use `sticky=False` to edit only this saved result,
as the examples do. See [Working with results](https://getanalog.io/docs/results/) for choosing
field names and keeping them across visits.



### Give the quote fields meaningful names

```python example=runnable scenario=python-reference-store-rename-fields
import json

renamed = results.rename_fields(
    working_handle,
    {"text": "quote", "text_2": "author"},
    sticky=False,
)
print(json.dumps(
    renamed.structured_content[0].records[0],
    ensure_ascii=False,
    indent=2,
))
```

```json wrap=true example=output scenario=python-reference-store-rename-fields
{
  "tags": [
    "change",
    "deep-thoughts",
    "thinking",
    "world"
  ],
  "quote": "“The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.”",
  "author": "by Albert Einstein"
}
```

The original values remain intact. Field names depend on the saved response;
inspect its fields before adapting this example to another page.

### Put the author and quote first

```python example=runnable scenario=python-reference-store-order-fields
ordered = results.reorder_fields(
    working_handle, ["author", "quote"], sticky=False
)
print(list(ordered.structured_content[0].records[0]))
```

```text wrap=true example=output scenario=python-reference-store-order-fields
['author', 'quote', 'tags']
```

The unspecified `tags` field follows the two named fields.





## Remove saved results

Deletion removes local saved artifacts. Keep any exports you want before
removing the corresponding save.

### `delete`

Remove a saved result by handle, friendly name, or `"latest"`.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
delete(handle: str) -> None
```
<!-- END GENERATED: sdk-api-declaration -->


`delete()` accepts a handle, friendly name, or `"latest"`, and returns
`None`. Deleting a well-formed handle again succeeds even if its
artifact is already gone. An unknown friendly name, malformed reference,
or `"latest"` on an empty store raises `ResultNotFoundError`.

### `delete_many`

Delete every saved result whose meta satisfies `predicate`.

<!-- BEGIN GENERATED: sdk-api-declaration -->
```python wrap=true
delete_many(predicate: Callable[[ResultMeta], bool]) -> list[str]
```
<!-- END GENERATED: sdk-api-declaration -->


The predicate receives each result's metadata. Every matching artifact is
removed; the returned list contains its handle, newest first, or `[]` if
nothing matched. Keep the predicate limited to the saves you intend to remove.



### Remove the selected saves

```python example=runnable scenario=python-reference-store-delete-many
selected = {working_handle}
deleted = results.delete_many(
    lambda meta: meta.handle in selected
)
print(deleted)
```

```text wrap=true example=output scenario=python-reference-store-delete-many
['20260912-2zult7']
```

This selects only the working result created above. Add other handles to
`selected` when you intend to remove more than one save.

### Delete one handle

```python example=runnable scenario=python-reference-store-delete
results.delete(working_handle)
```

There is no output. The preceding example already removed this artifact;
deleting it again by handle succeeds. Using the removed friendly
name `"quotes-work"` here would raise `ResultNotFoundError`.



## Complete root export inventory

- **Primary calls:** `analog`, `assess`

- **Client and fetching:** `Browser`, `BrowserRecipe`, `Client`, `Fetcher`, `FetchResult`, `HttpFetcher`, `RobotsChecker`

- **Results and response models:** `AnalogResponse`, `BrowseAction`, `Collection`, `DocumentReference`, `DocumentSectionReference`, `ErrorResponse`, `ExtractRequest`, `FeedbackRequest`, `FeedbackResponse`, `FieldInfo`, `FieldStat`, `FieldValue`, `FitAssessment`, `InfoResponse`, `LanguageObservation`, `MarkdownSection`, `OutlineNode`, `OutlineReference`, `PageSweep`, `PaginationInfo`, `RateLimitInfo`, `ResponseWarning`, `Section`, `StructuredContentReference`

- **Saved results:** `FetchSettings`, `ResultMeta`, `StoreStats`, `delete`, `delete_many`, `history`, `latest`, `latest_handle`, `open`, `rename`, `rename_fields`, `reorder_fields`, `results_dir`, `save`, `store_stats`

- **Exceptions:** `AnalogAPIError`, `AnalogAccountDeactivatedError`, `AnalogAuthError`, `AnalogConnectionError`, `AnalogError`, `AnalogIncompatibleSectionsError`, `AnalogRateLimitError`, `AnalogVersionSkewError`, `FetchBlockedError`, `FetchError`, `FetchStatusError`, `InvalidResultNameError`, `ResultNameInUseError`, `ResultNotFoundError`, `ResultSchemaDriftError`, `RobotsTxtDisallowedError`, `RobotsTxtUnreachableError`, `UrlNotAllowedError`

- **Constants and progress:** `DEFAULT_ROBOTS_CACHE_TTL_SECONDS`, `DEFAULT_UNREACHABLE_RETRY_SECONDS`, `FEEDBACK_LABELS`, `SCHEMA_VERSION`, `__version__`, `noop_progress`, `set_progress`

<!-- END GENERATED: sdk-api-reference -->

## Related guides

- [Install](https://getanalog.io/docs/install/) and [authenticate](https://getanalog.io/docs/authenticate/)
- [Look up CLI commands and options](https://getanalog.io/docs/cli-reference/)
- [View a page](https://getanalog.io/docs/extract/)
- [Control fetching](https://getanalog.io/docs/fetching/)
- [Query, export, and reopen results](https://getanalog.io/docs/results/)
- [Submit feedback](https://getanalog.io/docs/feedback/)
- [Use the MCP server](https://getanalog.io/docs/mcp/)
- [Limitations](https://getanalog.io/docs/limitations/), [privacy](https://getanalog.io/docs/privacy/), and [security](https://getanalog.io/docs/security/)