Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
60f9f5b
internal: carry scrubbed response body on FredRequestError
joce Jul 5, 2026
83042ea
internal: add probe scaffolding with secret scrubbing
joce Jul 5, 2026
9df6162
internal: share API-key redaction constants between client and probe
joce Jul 5, 2026
71a1e2d
internal: add probe coverage matrix over all 35 commands
joce Jul 5, 2026
335a15e
internal: pin sanitized case-key uniqueness; note time-relative probe…
joce Jul 5, 2026
8fb635a
internal: add probe runner writing corpus and manifest
joce Jul 5, 2026
48c0189
internal: pin http_status semantics on non-JSON-200 path; document or…
joce Jul 5, 2026
3a91e50
internal: add committed FRED capture corpus with hygiene gates
joce Jul 5, 2026
fb833d7
internal: document corpus regeneration and curation rules
joce Jul 5, 2026
a0a2471
internal: fix probe docstring manifest field list
joce Jul 6, 2026
929e32a
internal: rescope raw-JSON rules to CLI layer; add library-layer rules
joce Jul 6, 2026
99b3433
internal: add background-loop sync bridge for the library layer
joce Jul 6, 2026
7967111
internal: add FredApiError and corpus-pinned error interpretation
joce Jul 6, 2026
24de1c8
internal: add shared-client core with configure() and typed endpoint …
joce Jul 6, 2026
5dcf993
fix: join CSV list params on the separator coercion splits on
joce Jul 6, 2026
84d45ef
internal: add Frame and corpus-pinned Observations container
joce Jul 6, 2026
c9fb237
feat: add the public synchronous library surface
joce Jul 6, 2026
08cb428
internal: polish api docstrings; pin DateLike wire form and observati…
joce Jul 6, 2026
fb076c4
feat: expose the lazy typed public surface with py.typed and a pandas…
joce Jul 6, 2026
c99b694
internal: add spell-check words for the library surface
joce Jul 6, 2026
2295d4a
feat: add corpus-gated response models for flagship and catalog endpo…
joce Jul 6, 2026
d4d288b
feat: complete the typed surface - vintage dates, updates, search tag…
joce Jul 6, 2026
2090f51
fix: guard offset padding against bare date strings
joce Jul 6, 2026
072877e
internal: allowlist padder for cspell (CI-blocking word)
joce Jul 6, 2026
e3ab157
internal: add packaging membership tests
joce Jul 6, 2026
0fed965
feat: rewrite README for the typed library surface
joce Jul 6, 2026
e3439da
change: stage 0.4.0 changelog
joce Jul 6, 2026
702dcac
feat: remove the GeoFRED command group
joce Jul 6, 2026
14e2397
internal: prune GeoFRED captures from the corpus
joce Jul 6, 2026
f1e860f
internal: remove GeoFRED from docs and spell words
joce Jul 6, 2026
93605dc
fix: refresh stale pre-typed-surface wording in api docstring and AGE…
joce Jul 6, 2026
ff61306
internal: pin the unwrap-violation malformed-response contract
joce Jul 7, 2026
fc7fbdf
feat: add the FRED explorer marimo example notebook
joce Jul 7, 2026
443d483
fix: gate the boolean serialization bypass on BOOLEAN-kind params
joce Jul 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,24 @@ jobs:
- name: Run tox
run: uvx --with tox-uv tox -e py${{ matrix.python-version }}

ci-pandas:
needs: spell-check
runs-on: ubuntu-latest
steps:
- name: Check out the repository
uses: actions/checkout@v7
with:
fetch-depth: 0 # tags needed for hatch-vcs version

- name: Install uv
uses: astral-sh/setup-uv@v8.2.0
with:
version: "latest"
python-version: "3.12"

- name: Run tox
run: uvx --with tox-uv tox -e pandas

coverage:
needs: spell-check
runs-on: ubuntu-latest
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,8 @@ tmp/
# Ad-hoc fredq report fleet (generated artifacts, data dumps, builders)
report/

# marimo session/layout caches (regenerated on notebook edit)
__marimo__/

# Local brainstorming specs / plans (not committed)
docs/superpowers/
docs/superpowers/
46 changes: 35 additions & 11 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# AGENTS.md

## Project
fredq exposes FRED (Federal Reserve Economic Data) HTTP endpoints as an LLM-friendly CLI that prints the raw JSON FRED returns.
fredq is a typed Python library and an LLM-friendly CLI over FRED (Federal
Reserve Economic Data) HTTP endpoints. The CLI prints the raw JSON FRED
returns, byte-for-byte. The library (`fredq.api` and below) returns parsed,
typed results. The two share client/commands/params foundations but the CLI
never routes through the library's typed surface.

## Stack
Python 3.10+, uv, httpx2, argparse, pytest, ruff, pyright, tox, hatchling.
Expand All @@ -19,14 +23,13 @@ Python 3.10+, uv, httpx2, argparse, pytest, ruff, pyright, tox, hatchling.
- Full check: `uv run tox`

## Command grouping (noun-verb)
Commands are organized into six noun groups, each with verb leaves:
Commands are organized into five noun groups, each with verb leaves:
- **series**: `show`, `observations`, `search`, `search-tags`, `search-related-tags`, `vintage-dates`, `categories`, `tags`, `release`, `updates`
- **category**: `show`, `children`, `related`, `series`, `tags`, `related-tags`
- **release**: `list`, `show`, `calendar` (all releases dates), `dates` (one release's dates), `series`, `sources`, `tags`, `related-tags`, `tables`
- `release calendar` → `/fred/releases/dates`; `release dates ID` → `/fred/release/dates` (distinct endpoints)
- **source**: `list`, `show`, `releases`
- **tag**: `list`, `series`, `related`
- **geofred**: `series-group`, `series-data`, `regional-data`, `shapes` (unchanged; leaf stays None → uses name)

Each `CommandSpec.name` is globally unique and unchanged (routing key). The `leaf` field is display-only.

Expand All @@ -35,19 +38,41 @@ Each `CommandSpec.name` is globally unique and unchanged (routing key). The `lea
- `src/fredq/auth.py` -> Read FRED_API_KEY from env or fallback file.
- `src/fredq/commands.py` -> command metadata used to build CLI commands, validation, and help.
- `src/fredq/params.py` -> CLI parameter coercion and validation helpers.
- `src/fredq/_bridge.py` -> background event loop; sync-over-async bridge (library only).
- `src/fredq/_core.py` -> async endpoint core: shared client, configure(), param building, error contract (library only).
- `src/fredq/api.py` -> public synchronous library surface (entity classes + module functions).
- `src/fredq/frames.py` -> polars-backed Frame containers for bulk tabular payloads (library only).
- `src/fredq/cli.py` -> argparse command tree and stdout/stderr behavior.
- `tests/` -> pytest tests mirroring `src/fredq/`.

## Rules
## Rules — CLI layer
- IMPORTANT: `--help` is the primary product surface; keep it complete, adaptive, and generated from command metadata where practical.
- Do not add `describe`, `endpoints`, `params`, or other discovery commands; discovery belongs under `fredq --help` and `fredq <endpoint> --help`.
- Print FRED response bodies to stdout exactly as returned; do not model, reshape, pretty-print, or interpret endpoint JSON.
- Keep FRED endpoint knowledge in metadata and validation only; do not create response classes.
- Print FRED response bodies to stdout exactly as returned; do not model, reshape, pretty-print, or interpret endpoint JSON. (CLI layer only; the library layer interprets.)
- In the CLI layer, keep FRED endpoint knowledge in metadata and validation only. Response classes live exclusively in the library layer (src/fredq/models/).
- Use `uv run python` for Python scripts; never use bare `python` or `python3`.
- Use `regex` instead of standard library `re` for regular expressions.
- Never log or print the FRED API key.
- Keep runtime dependencies narrow; do not add TUI, ORM, web framework, or rich formatting libraries.

## Rules — library layer
- The library layer (`api.py`, `_core.py`, `_bridge.py`, `frames.py`, `models/`) parses and types FRED responses; the raw-JSON law above does not apply to it.
- The CLI never imports `api.py`, `frames.py`, or `models/`. `fredq --help` and all CLI commands must never pay the polars import cost.
- Library kwargs mirror wire parameter names exactly as spelled in `CommandSpec`s; never an inverted flag.
- The committed corpus (`tests/fixtures/corpus/`, see its README) is the only authority for wire spellings, presence, and types. Errors are mapped by status + body shape, never message wording.

## Response model conventions (library layer)
- Every response model subclasses `FredModel` (`src/fredq/models/_base.py`): `frozen=True`, `extra="allow"` (drift lands on `model_extra` for the gates — never use `forbid`), `populate_by_name=True`, `str_strip_whitespace=True`. No alias generator; field names mirror wire keys exactly, warts included (`seriess`).
- Required vs optional comes from the corpus, never docs or guesses: present in 100% of corpus records → required (no default); sometimes absent → `T | None = None`; always present but sometimes null → `T | None` (no default). Live evidence may LOOSEN (required → optional) with a dated docstring note and a pinned test; never tighten.
- Every model is registered in `tests/test_models_gates.py` in the same commit that creates it: zero-nested-extras over all relevant captures, required-set == corpus universal keys, alphabetical field order.
- Temporal honesty: ISO date strings → `datetime.date`; FRED's offset datetimes (`2026-04-09 07:53:12-05`) → aware `datetime` via `FredDatetime`. No temporal value stays a bare string.
- Enums only where the vocabulary is closed by request-side validation or explicit FRED documentation; corpus-only closure is insufficient (the corpus's series are not the universe). Open vocabularies stay `str`. `Literal` for true constants (`file_type`).
- `functools.cached_property` for conveniences; NEVER `computed_field` (`model_dump()` stays wire-shaped).
- Nested structures are typed sub-models, never `dict[str, Any]` (documented exceptions: `raw()`, evidence-justified Frame columns); keyed collections are `dict[str, SubModel]`.
- Model reuse across endpoints only after script-validated evidence (zero extras + required set holds on the candidate's captures); the model docstring lists every endpoint it covers.
- Single-entity endpoints unwrap their one-element list; violations raise the malformed-response contract (`FredApiError`, `error_code=None`).
- Module docstring names the endpoint noun + corpus date; sometimes-absent fields carry an applicability note.

## API key
- Primary: `FRED_API_KEY` environment variable.
- Fallback: file at `~/.fredq/api_key` (single line, key only).
Expand All @@ -60,7 +85,7 @@ When adding or editing a CLI command:
3. **Notes**: real clarifications only — FRED quirks, switch-behavior surprises, dependencies. Drop diary entries and redundant restatements.
4. **Order in `COMMANDS` tuple by importance**: daily-driver → discovery → entity lookups → schema introspection. Never append to the end.
5. **Param boilerplate is shared** (`--api-key`, `--realtime-start`, `--realtime-end` use exact strings — copy them). Run `pytest -k help` before and after.
6. **Positional primary args**: each command's single primary required argument is a positional (its `metavar` is shown in usage); all other parameters are flags. `series search` / `series search-tags` / `series search-related-tags` take the search text positionally; `tag series` / `tag related` take the tag list positionally; `geofred regional-data` / `geofred shapes` take their primary (`series_group` / `shape`) positionally.
6. **Positional primary args**: each command's single primary required argument is a positional (its `metavar` is shown in usage); all other parameters are flags. `series search` / `series search-tags` / `series search-related-tags` take the search text positionally; `tag series` / `tag related` take the tag list positionally.

## Output formats
- **Default**: raw FRED JSON to stdout, exactly as returned.
Expand All @@ -81,6 +106,8 @@ When adding or editing a CLI command:
- Ask before making architectural changes that affect the CLI grammar or auth behavior.

## Development workflow
Exception: the library-api feature runs brainstorm → spec → multi-part plans on a single `library-api` branch with ONE PR at the very end, merged by the user. The per-PR merge loop below applies to normal maintenance work.

Use this process for all development work — bug fixes and features alike. For features, brainstorm and plan first, then follow the implement → review → dogfood loop below.

Model / effort split:
Expand Down Expand Up @@ -109,10 +136,7 @@ Steps:
- Sources: `1` (Board of Governors), `3` (Bureau of Labor Statistics)
- Add targeted probes when an endpoint is series-sensitive, but keep this baseline for broad API-surface discovery.

## GeoFRED / Maps
- Implemented under the `geofred` subcommand group (`series-group`, `series-data`, `regional-data`, `shapes`). Different base URL; regional data keyed by FIPS; `shapes` returns Highcharts-format GeoJSON in a Lambert Conformal Conic projection (not WGS84).

## Out of scope
- Mapping FRED JSON into Python domain models.
- Separate documentation/discovery subcommands.
- Secrets or API keys in checked-in files.
- Typed models outside the library layer.
58 changes: 58 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,64 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

fredq is now a typed Python library as well as a CLI. CLI behavior and
output are unchanged, except for the removal of the `geofred` command
group (see Removed).

### Added

- Typed library surface: `fredq.Series`, `fredq.Category`, `fredq.Release`,
and `fredq.Source` entity classes, plus module-level functions
(`search_series`, `releases`, `sources`, `tags`, and related lookups) —
see the README for the full surface table.
- Corpus-gated pydantic response models under `fredq.models` (`SeriesInfo`,
`ReleaseInfo`, `SourceInfo`, `TagsResult`, and others): frozen, with
required/optional derived from real FRED response captures rather than
documentation, and unrecognized fields captured on `model_extra` instead
of raising.
- `fredq.Series.observations()` returns a polars-backed `Observations`
frame (`fredq.frames`) with `.to_polars()`, `.to_pandas()`, `.to_arrow()`,
`.to_dicts()`, and `.save_parquet()`, plus a typed `.meta` envelope.
- `fredq.raw(command, **params)`: an escape hatch that reaches every
command the CLI knows, validated exactly like every other library call.
- `fredq.configure(*, api_key=None, timeout=None)` to set the shared
library client's options before the first call.
- `fredq.FredApiError`, `fredq.FredClientUsageError`, and related
exceptions for typed error handling in library code.
- `py.typed` marker ([PEP 561](https://peps.python.org/pep-0561/)) so type
checkers see the library's real return types without stubs.
- New optional extra: `pip install "fredq[pandas]"` (pandas + pyarrow) for
`Observations.to_pandas()` / `.to_arrow()`.

### Changed

- Added `pydantic` as a core runtime dependency (typed response models).

### Removed

- **Breaking:** the `geofred` CLI command group (`series-group`,
`series-data`, `regional-data`, `shapes`) and its four FRED Maps API
endpoints. The GeoFRED site was sunset in 2022 and the underlying API is
deprecated. These commands are no longer reachable from the CLI, and
`fredq.raw()` no longer recognizes these command names (GeoFRED was
never part of the typed library surface — `raw()` was its only access
point, and that access point is now gone too). Call the FRED Maps API
directly (e.g. with `httpx`) if you still need this data; see the
[FRED API docs](https://fred.stlouisfed.org/docs/api/geofred/) for the
endpoint paths and parameters.

### Internal

- Packaging membership is test-pinned (`tests/test_packaging.py`): the
wheel carries `py.typed` and the library modules and excludes
`tests/`/`node_modules/`/`docs/`/`output/`; the sdist carries the test
corpus and excludes dev-only trees. Added an explicit
`[tool.hatch.build.targets.sdist] exclude` list — hatchling's
VCS-ignore-based exclusion silently no-ops when the build root is passed
as an absolute path inside a git worktree (`.git` is a file there),
which had let dev trees like `node_modules/` leak into a worktree-built
sdist.

## [0.3.3]

Maintenance release — no user-facing changes.
Expand Down
Loading