bedrok 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bedrok-0.2.0/LICENSE +21 -0
- bedrok-0.2.0/PKG-INFO +113 -0
- bedrok-0.2.0/README.md +74 -0
- bedrok-0.2.0/bedrok/__init__.py +22 -0
- bedrok-0.2.0/bedrok/__main__.py +75 -0
- bedrok-0.2.0/bedrok/core/__init__.py +5 -0
- bedrok-0.2.0/bedrok/core/aliases.py +25 -0
- bedrok-0.2.0/bedrok/core/charting.py +176 -0
- bedrok-0.2.0/bedrok/core/config.py +228 -0
- bedrok-0.2.0/bedrok/core/geography.py +97 -0
- bedrok-0.2.0/bedrok/core/licensing.py +152 -0
- bedrok-0.2.0/bedrok/core/query.py +47 -0
- bedrok-0.2.0/bedrok/core/schema.py +46 -0
- bedrok-0.2.0/bedrok/core/sources.py +84 -0
- bedrok-0.2.0/bedrok/core/store.py +233 -0
- bedrok-0.2.0/bedrok/core/units.py +64 -0
- bedrok-0.2.0/bedrok/core/warnings.py +139 -0
- bedrok-0.2.0/bedrok/engines/__init__.py +0 -0
- bedrok-0.2.0/bedrok/engines/pctile.py +104 -0
- bedrok-0.2.0/bedrok/engines/rates_infl.py +91 -0
- bedrok-0.2.0/bedrok/ingest/__init__.py +4 -0
- bedrok-0.2.0/bedrok/ingest/adapters/__init__.py +0 -0
- bedrok-0.2.0/bedrok/ingest/adapters/_util.py +92 -0
- bedrok-0.2.0/bedrok/ingest/adapters/bea.py +88 -0
- bedrok-0.2.0/bedrok/ingest/adapters/bls.py +115 -0
- bedrok-0.2.0/bedrok/ingest/adapters/boc.py +41 -0
- bedrok-0.2.0/bedrok/ingest/adapters/boe.py +85 -0
- bedrok-0.2.0/bedrok/ingest/adapters/census.py +81 -0
- bedrok-0.2.0/bedrok/ingest/adapters/cftc.py +139 -0
- bedrok-0.2.0/bedrok/ingest/adapters/destatis.py +107 -0
- bedrok-0.2.0/bedrok/ingest/adapters/eia.py +51 -0
- bedrok-0.2.0/bedrok/ingest/adapters/estat.py +73 -0
- bedrok-0.2.0/bedrok/ingest/adapters/eurostat.py +112 -0
- bedrok-0.2.0/bedrok/ingest/adapters/fedboard.py +139 -0
- bedrok-0.2.0/bedrok/ingest/adapters/fedfiles.py +259 -0
- bedrok-0.2.0/bedrok/ingest/adapters/fred.py +32 -0
- bedrok-0.2.0/bedrok/ingest/adapters/ine.py +63 -0
- bedrok-0.2.0/bedrok/ingest/adapters/mof_jp.py +64 -0
- bedrok-0.2.0/bedrok/ingest/adapters/nber.py +46 -0
- bedrok-0.2.0/bedrok/ingest/adapters/nyfed.py +118 -0
- bedrok-0.2.0/bedrok/ingest/adapters/ons.py +78 -0
- bedrok-0.2.0/bedrok/ingest/adapters/pxweb.py +91 -0
- bedrok-0.2.0/bedrok/ingest/adapters/sdmx.py +143 -0
- bedrok-0.2.0/bedrok/ingest/adapters/statcan.py +53 -0
- bedrok-0.2.0/bedrok/ingest/adapters/worldbank.py +127 -0
- bedrok-0.2.0/bedrok/ingest/backfill.py +74 -0
- bedrok-0.2.0/bedrok/ingest/derived.py +225 -0
- bedrok-0.2.0/bedrok/ingest/live.py +38 -0
- bedrok-0.2.0/bedrok/ingest/pipeline.py +820 -0
- bedrok-0.2.0/bedrok/ingest/refresh.py +73 -0
- bedrok-0.2.0/bedrok/ingest/transforms.py +228 -0
- bedrok-0.2.0/bedrok/ops/__init__.py +4 -0
- bedrok-0.2.0/bedrok/ops/docs.py +219 -0
- bedrok-0.2.0/bedrok/ops/doctor.py +142 -0
- bedrok-0.2.0/bedrok/ops/freshness.py +69 -0
- bedrok-0.2.0/bedrok/ops/smoke.py +108 -0
- bedrok-0.2.0/bedrok/ops/tieout.py +201 -0
- bedrok-0.2.0/bedrok/ops/validate.py +172 -0
- bedrok-0.2.0/bedrok/seeds/charting.md +70 -0
- bedrok-0.2.0/bedrok/seeds/gaps.md +51 -0
- bedrok-0.2.0/bedrok/seeds/macro_catalog.csv +199 -0
- bedrok-0.2.0/bedrok/seeds/sources.csv +32 -0
- bedrok-0.2.0/bedrok/seeds/warnings.csv +44 -0
- bedrok-0.2.0/bedrok/surfaces/__init__.py +5 -0
- bedrok-0.2.0/bedrok/surfaces/mcp_server.py +950 -0
- bedrok-0.2.0/bedrok.egg-info/PKG-INFO +113 -0
- bedrok-0.2.0/bedrok.egg-info/SOURCES.txt +107 -0
- bedrok-0.2.0/bedrok.egg-info/dependency_links.txt +1 -0
- bedrok-0.2.0/bedrok.egg-info/entry_points.txt +2 -0
- bedrok-0.2.0/bedrok.egg-info/requires.txt +15 -0
- bedrok-0.2.0/bedrok.egg-info/top_level.txt +1 -0
- bedrok-0.2.0/pyproject.toml +67 -0
- bedrok-0.2.0/setup.cfg +4 -0
- bedrok-0.2.0/tests/test_boe_adapter.py +73 -0
- bedrok-0.2.0/tests/test_cache_rule.py +80 -0
- bedrok-0.2.0/tests/test_cftc_adapter.py +113 -0
- bedrok-0.2.0/tests/test_charting.py +82 -0
- bedrok-0.2.0/tests/test_derived.py +166 -0
- bedrok-0.2.0/tests/test_doctor.py +111 -0
- bedrok-0.2.0/tests/test_eia_adapter.py +26 -0
- bedrok-0.2.0/tests/test_eurostat_bulk.py +135 -0
- bedrok-0.2.0/tests/test_fedboard_adapter.py +76 -0
- bedrok-0.2.0/tests/test_fedfiles_adapter.py +216 -0
- bedrok-0.2.0/tests/test_freshness.py +58 -0
- bedrok-0.2.0/tests/test_geography.py +57 -0
- bedrok-0.2.0/tests/test_install.py +128 -0
- bedrok-0.2.0/tests/test_layering.py +102 -0
- bedrok-0.2.0/tests/test_licensing.py +77 -0
- bedrok-0.2.0/tests/test_macro_seed.py +121 -0
- bedrok-0.2.0/tests/test_mcp_server.py +346 -0
- bedrok-0.2.0/tests/test_mof_jp_adapter.py +30 -0
- bedrok-0.2.0/tests/test_ons_adapter.py +66 -0
- bedrok-0.2.0/tests/test_pctile.py +56 -0
- bedrok-0.2.0/tests/test_profile.py +121 -0
- bedrok-0.2.0/tests/test_publication.py +142 -0
- bedrok-0.2.0/tests/test_refresh.py +58 -0
- bedrok-0.2.0/tests/test_repoint.py +114 -0
- bedrok-0.2.0/tests/test_restricted_sync.py +94 -0
- bedrok-0.2.0/tests/test_sdmx_adapter.py +155 -0
- bedrok-0.2.0/tests/test_second_wave_adapters.py +162 -0
- bedrok-0.2.0/tests/test_smoke.py +107 -0
- bedrok-0.2.0/tests/test_sources.py +102 -0
- bedrok-0.2.0/tests/test_store.py +79 -0
- bedrok-0.2.0/tests/test_tieout.py +54 -0
- bedrok-0.2.0/tests/test_transforms.py +143 -0
- bedrok-0.2.0/tests/test_us_agency_adapters.py +159 -0
- bedrok-0.2.0/tests/test_validate.py +52 -0
- bedrok-0.2.0/tests/test_warnings.py +93 -0
- bedrok-0.2.0/tests/test_worldbank_adapter.py +45 -0
bedrok-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bedrok contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
bedrok-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bedrok
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Public macroeconomic data from central banks, statistics offices and international organisations, in your AI assistant: a local MCP server and store. No keys needed to start. Not financial advice.
|
|
5
|
+
Author: Andreu Paddack
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/KrustyKronk/bedrok
|
|
8
|
+
Project-URL: Source, https://github.com/KrustyKronk/bedrok
|
|
9
|
+
Project-URL: Issues, https://github.com/KrustyKronk/bedrok/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/KrustyKronk/bedrok/releases
|
|
11
|
+
Keywords: macroeconomics,economic data,mcp,duckdb,sdmx,central banks
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: duckdb<2,>=1.5
|
|
27
|
+
Requires-Dist: pandas>=2.2.0
|
|
28
|
+
Requires-Dist: numpy>=1.26
|
|
29
|
+
Requires-Dist: requests>=2.31.0
|
|
30
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
31
|
+
Requires-Dist: openpyxl>=3.1
|
|
32
|
+
Requires-Dist: xlrd>=2.0.1
|
|
33
|
+
Requires-Dist: mcp<3,>=2
|
|
34
|
+
Requires-Dist: platformdirs>=4
|
|
35
|
+
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# Bedrok
|
|
41
|
+
|
|
42
|
+
**Public macroeconomic data, in your AI assistant.**
|
|
43
|
+
|
|
44
|
+
Bedrok is a local MCP server. Ask Claude — or any assistant that speaks MCP —
|
|
45
|
+
where euro-area inflation sits in its history, how far US real yields have
|
|
46
|
+
risen this year, or what the BIS credit gap says about China, and it answers
|
|
47
|
+
from more than 500 curated series with deep history: US rates and the Fed's
|
|
48
|
+
releases, US labour and prices, the euro area country by country, the UK, the
|
|
49
|
+
OECD, the BIS, the IMF, national statistics offices from Canada to Japan,
|
|
50
|
+
commodities, futures positioning and FX. Each series is fetched from the
|
|
51
|
+
publisher that produces it, into a store on your own machine, with its licence
|
|
52
|
+
and the line to credit it with. Percentiles always come with their sample, and
|
|
53
|
+
charts come with a recipe so they look the same in every assistant.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
**Claude Desktop:** download `bedrok-<version>.mcpb` from the
|
|
58
|
+
[latest release](https://github.com/KrustyKronk/bedrok/releases/latest) and
|
|
59
|
+
open it.
|
|
60
|
+
|
|
61
|
+
**Claude Code** (needs [uv](https://docs.astral.sh/uv/getting-started/installation/)):
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
claude mcp add bedrok --scope user -- uvx --python 3.12 bedrok serve
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Any other MCP host:** a stdio server, `uvx --python 3.12 bedrok serve`.
|
|
68
|
+
|
|
69
|
+
**No keys needed to start.** Six free keys add US GDP and PCE, retail and
|
|
70
|
+
housing, energy, Germany's and Japan's national statistics, and the series
|
|
71
|
+
only FRED publishes. Details, keys and the data folder: [INSTALL](docs/INSTALL.md).
|
|
72
|
+
|
|
73
|
+
## The shape of it
|
|
74
|
+
|
|
75
|
+
- **Local, and nothing hosted.** Each install fetches its own data from the
|
|
76
|
+
publishers; nobody redistributes anything.
|
|
77
|
+
- **The publisher's terms travel with the number.** Every series carries its
|
|
78
|
+
licence class and attribution; series whose terms forbid storing them (FRED's)
|
|
79
|
+
are fetched live and never written. See [SOURCES](docs/SOURCES.md),
|
|
80
|
+
[LICENSES](docs/LICENSES.md) and the reasoning in [LICENSING](docs/LICENSING.md).
|
|
81
|
+
- **It says when a number is in doubt.** Documented cases where an official
|
|
82
|
+
figure should not be taken at face value — a suspended release, a
|
|
83
|
+
restatement, a break — ride along with the answer. See
|
|
84
|
+
[WARNINGS](docs/WARNINGS.md) and what no open source provides, [GAPS](docs/GAPS.md).
|
|
85
|
+
- **Fetch on demand.** A series is fetched the first time it is asked for and
|
|
86
|
+
refreshed when it goes stale.
|
|
87
|
+
- **Never advice.** Bedrok measures, contextualises and premise-checks. You
|
|
88
|
+
decide. See [DISCLAIMER](docs/DISCLAIMER.md).
|
|
89
|
+
|
|
90
|
+
## What it is not
|
|
91
|
+
|
|
92
|
+
Not financial advice and never a buy/sell signal. No equity prices — official
|
|
93
|
+
rates, FX, energy and positioning are the markets layer. Not hosted: there is
|
|
94
|
+
no Bedrok server anywhere but your own machine.
|
|
95
|
+
|
|
96
|
+
## Contributing
|
|
97
|
+
|
|
98
|
+
A new publisher is mostly a row in a CSV. [CONTRIBUTING](CONTRIBUTING.md) says
|
|
99
|
+
how, and the [source request](https://github.com/KrustyKronk/bedrok/issues/new?template=source-request.yml)
|
|
100
|
+
form is the place to ask for one. Developer setup is [SETUP](docs/SETUP.md).
|
|
101
|
+
The project is built in sessions with an AI assistant; `CLAUDE.md` holds its
|
|
102
|
+
rules and `STATE.md` where it stands.
|
|
103
|
+
|
|
104
|
+
## Docs
|
|
105
|
+
|
|
106
|
+
[INSTALL](docs/INSTALL.md) · [SOURCES](docs/SOURCES.md) · [LICENSES](docs/LICENSES.md) ·
|
|
107
|
+
[WARNINGS](docs/WARNINGS.md) · [GAPS](docs/GAPS.md) · [CHARTING](docs/CHARTING.md) ·
|
|
108
|
+
[SETUP](docs/SETUP.md) · [LICENSING](docs/LICENSING.md) ·
|
|
109
|
+
[DISCLAIMER](docs/DISCLAIMER.md) · [Plan](docs/rebuild-plan.md) ·
|
|
110
|
+
[Engineering notes](docs/engineering/notes.md)
|
|
111
|
+
|
|
112
|
+
Python ≥3.11 · macOS and Linux · MIT licence for the code; the data keeps its
|
|
113
|
+
publishers' terms. By Andreu Paddack.
|
bedrok-0.2.0/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Bedrok
|
|
2
|
+
|
|
3
|
+
**Public macroeconomic data, in your AI assistant.**
|
|
4
|
+
|
|
5
|
+
Bedrok is a local MCP server. Ask Claude — or any assistant that speaks MCP —
|
|
6
|
+
where euro-area inflation sits in its history, how far US real yields have
|
|
7
|
+
risen this year, or what the BIS credit gap says about China, and it answers
|
|
8
|
+
from more than 500 curated series with deep history: US rates and the Fed's
|
|
9
|
+
releases, US labour and prices, the euro area country by country, the UK, the
|
|
10
|
+
OECD, the BIS, the IMF, national statistics offices from Canada to Japan,
|
|
11
|
+
commodities, futures positioning and FX. Each series is fetched from the
|
|
12
|
+
publisher that produces it, into a store on your own machine, with its licence
|
|
13
|
+
and the line to credit it with. Percentiles always come with their sample, and
|
|
14
|
+
charts come with a recipe so they look the same in every assistant.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
**Claude Desktop:** download `bedrok-<version>.mcpb` from the
|
|
19
|
+
[latest release](https://github.com/KrustyKronk/bedrok/releases/latest) and
|
|
20
|
+
open it.
|
|
21
|
+
|
|
22
|
+
**Claude Code** (needs [uv](https://docs.astral.sh/uv/getting-started/installation/)):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
claude mcp add bedrok --scope user -- uvx --python 3.12 bedrok serve
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Any other MCP host:** a stdio server, `uvx --python 3.12 bedrok serve`.
|
|
29
|
+
|
|
30
|
+
**No keys needed to start.** Six free keys add US GDP and PCE, retail and
|
|
31
|
+
housing, energy, Germany's and Japan's national statistics, and the series
|
|
32
|
+
only FRED publishes. Details, keys and the data folder: [INSTALL](docs/INSTALL.md).
|
|
33
|
+
|
|
34
|
+
## The shape of it
|
|
35
|
+
|
|
36
|
+
- **Local, and nothing hosted.** Each install fetches its own data from the
|
|
37
|
+
publishers; nobody redistributes anything.
|
|
38
|
+
- **The publisher's terms travel with the number.** Every series carries its
|
|
39
|
+
licence class and attribution; series whose terms forbid storing them (FRED's)
|
|
40
|
+
are fetched live and never written. See [SOURCES](docs/SOURCES.md),
|
|
41
|
+
[LICENSES](docs/LICENSES.md) and the reasoning in [LICENSING](docs/LICENSING.md).
|
|
42
|
+
- **It says when a number is in doubt.** Documented cases where an official
|
|
43
|
+
figure should not be taken at face value — a suspended release, a
|
|
44
|
+
restatement, a break — ride along with the answer. See
|
|
45
|
+
[WARNINGS](docs/WARNINGS.md) and what no open source provides, [GAPS](docs/GAPS.md).
|
|
46
|
+
- **Fetch on demand.** A series is fetched the first time it is asked for and
|
|
47
|
+
refreshed when it goes stale.
|
|
48
|
+
- **Never advice.** Bedrok measures, contextualises and premise-checks. You
|
|
49
|
+
decide. See [DISCLAIMER](docs/DISCLAIMER.md).
|
|
50
|
+
|
|
51
|
+
## What it is not
|
|
52
|
+
|
|
53
|
+
Not financial advice and never a buy/sell signal. No equity prices — official
|
|
54
|
+
rates, FX, energy and positioning are the markets layer. Not hosted: there is
|
|
55
|
+
no Bedrok server anywhere but your own machine.
|
|
56
|
+
|
|
57
|
+
## Contributing
|
|
58
|
+
|
|
59
|
+
A new publisher is mostly a row in a CSV. [CONTRIBUTING](CONTRIBUTING.md) says
|
|
60
|
+
how, and the [source request](https://github.com/KrustyKronk/bedrok/issues/new?template=source-request.yml)
|
|
61
|
+
form is the place to ask for one. Developer setup is [SETUP](docs/SETUP.md).
|
|
62
|
+
The project is built in sessions with an AI assistant; `CLAUDE.md` holds its
|
|
63
|
+
rules and `STATE.md` where it stands.
|
|
64
|
+
|
|
65
|
+
## Docs
|
|
66
|
+
|
|
67
|
+
[INSTALL](docs/INSTALL.md) · [SOURCES](docs/SOURCES.md) · [LICENSES](docs/LICENSES.md) ·
|
|
68
|
+
[WARNINGS](docs/WARNINGS.md) · [GAPS](docs/GAPS.md) · [CHARTING](docs/CHARTING.md) ·
|
|
69
|
+
[SETUP](docs/SETUP.md) · [LICENSING](docs/LICENSING.md) ·
|
|
70
|
+
[DISCLAIMER](docs/DISCLAIMER.md) · [Plan](docs/rebuild-plan.md) ·
|
|
71
|
+
[Engineering notes](docs/engineering/notes.md)
|
|
72
|
+
|
|
73
|
+
Python ≥3.11 · macOS and Linux · MIT licence for the code; the data keeps its
|
|
74
|
+
publishers' terms. By Andreu Paddack.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Bedrok — a local store of public macroeconomic and market data, for analysis
|
|
2
|
+
from inside an AI assistant.
|
|
3
|
+
|
|
4
|
+
The package is layered, and the direction of dependency is one way:
|
|
5
|
+
|
|
6
|
+
surfaces -> ops -> engines -> ingest -> core
|
|
7
|
+
|
|
8
|
+
core/ configuration, the store, and shared vocabulary
|
|
9
|
+
ingest/ source adapters and the sync drivers
|
|
10
|
+
engines/ the analysis maths — percentiles, rates
|
|
11
|
+
ops/ the audit reports and the connectivity check
|
|
12
|
+
surfaces/ things that render the store for a human or an assistant
|
|
13
|
+
|
|
14
|
+
`config` and `store` are re-exported here because they are the documented
|
|
15
|
+
embedding entry point — a sandbox session does `from bedrok import config;
|
|
16
|
+
config.set_profile(...)`, and that recipe is written down in
|
|
17
|
+
docs/engineering/notes.md. Those two names do not move. Everything else should
|
|
18
|
+
be imported from its real home so the layer it belongs to stays visible.
|
|
19
|
+
"""
|
|
20
|
+
from bedrok.core import config, store # noqa: F401
|
|
21
|
+
|
|
22
|
+
__all__ = ["config", "store"]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Bedrok CLI — one entry point over the store and the engines.
|
|
2
|
+
|
|
3
|
+
bedrok [--profile PATH] <command> [command args...]
|
|
4
|
+
|
|
5
|
+
Commands:
|
|
6
|
+
smoke connectivity check, one call per data source
|
|
7
|
+
backfill full deep historical pull (idempotent) [--only ids --stage S]
|
|
8
|
+
refresh incremental sync of the local store
|
|
9
|
+
validate coverage + trust audit (staleness, licence)
|
|
10
|
+
query read-only SQL over the store "SELECT ..." [--csv]
|
|
11
|
+
tieout compare re-pointed series with the store [--only ids]
|
|
12
|
+
doctor store + freshness report (offline; run this first)
|
|
13
|
+
schema the store's table/column layout
|
|
14
|
+
pctile percentile of a series' latest value SERIES_ID [--value V --since D]
|
|
15
|
+
rates EUR-investor rates/inflation toolkit [--csv]
|
|
16
|
+
serve MCP server over stdio, for Claude and other assistants
|
|
17
|
+
docs regenerate docs/SOURCES.md and docs/LICENSES.md [--check]
|
|
18
|
+
|
|
19
|
+
The profile (user config + data) resolves from --profile, $BEDROK_PROFILE, or
|
|
20
|
+
./profile. See docs/SETUP.md.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
_COMMANDS = {
|
|
27
|
+
"smoke": "bedrok.ops.smoke",
|
|
28
|
+
"backfill": "bedrok.ingest.backfill",
|
|
29
|
+
"refresh": "bedrok.ingest.refresh",
|
|
30
|
+
"validate": "bedrok.ops.validate",
|
|
31
|
+
"query": "bedrok.core.query",
|
|
32
|
+
"tieout": "bedrok.ops.tieout",
|
|
33
|
+
"doctor": "bedrok.ops.doctor",
|
|
34
|
+
"schema": "bedrok.core.schema",
|
|
35
|
+
"pctile": "bedrok.engines.pctile",
|
|
36
|
+
"rates": "bedrok.engines.rates_infl",
|
|
37
|
+
"serve": "bedrok.surfaces.mcp_server",
|
|
38
|
+
"docs": "bedrok.ops.docs",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main() -> int:
|
|
43
|
+
argv = sys.argv[1:]
|
|
44
|
+
|
|
45
|
+
profile = None
|
|
46
|
+
if "--profile" in argv:
|
|
47
|
+
i = argv.index("--profile")
|
|
48
|
+
try:
|
|
49
|
+
profile = argv[i + 1]
|
|
50
|
+
except IndexError:
|
|
51
|
+
print("--profile requires a path", file=sys.stderr)
|
|
52
|
+
return 2
|
|
53
|
+
argv = argv[:i] + argv[i + 2:]
|
|
54
|
+
|
|
55
|
+
if not argv or argv[0] in ("-h", "--help", "help"):
|
|
56
|
+
print(__doc__)
|
|
57
|
+
return 0
|
|
58
|
+
cmd, rest = argv[0], argv[1:]
|
|
59
|
+
if cmd not in _COMMANDS:
|
|
60
|
+
print(f"unknown command {cmd!r}\n", file=sys.stderr)
|
|
61
|
+
print(__doc__)
|
|
62
|
+
return 2
|
|
63
|
+
|
|
64
|
+
from bedrok.core import config
|
|
65
|
+
if profile:
|
|
66
|
+
config.set_profile(profile)
|
|
67
|
+
|
|
68
|
+
import importlib
|
|
69
|
+
mod = importlib.import_module(_COMMANDS[cmd])
|
|
70
|
+
sys.argv = [f"bedrok {cmd}"] + rest
|
|
71
|
+
return mod.main()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
sys.exit(main())
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""One id per concept: the ids that point at another.
|
|
2
|
+
|
|
3
|
+
The bulk families name a series by family and geography (`hicp_yoy_es`); the
|
|
4
|
+
seed names the most-asked ones by geography first (`es_hicp_yoy`), the
|
|
5
|
+
convention every instruction teaches. Where both exist they are the same
|
|
6
|
+
number from the same dataset, so the family id becomes an alias: it still
|
|
7
|
+
answers, with `alias_of` naming the canonical id, but it is neither stored nor
|
|
8
|
+
listed (session 11, after the feedback session found the duplicates).
|
|
9
|
+
|
|
10
|
+
Pure data, in core: nothing here imports a layer above.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
ALIASES: dict[str, str] = {
|
|
15
|
+
"hicp_yoy_ea": "ea_hicp_yoy",
|
|
16
|
+
"hicp_yoy_es": "es_hicp_yoy",
|
|
17
|
+
"hicp_yoy_de": "de_hicp_yoy",
|
|
18
|
+
"hicp_yoy_fr": "fr_hicp_yoy",
|
|
19
|
+
"gdp_qoq_ea20": "ea_gdp_qoq",
|
|
20
|
+
"gdp_qoq_es": "es_gdp_qoq",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def canonical(series_id: str) -> str:
|
|
25
|
+
return ALIASES.get(series_id, series_id)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""The chart recipe: what every Bedrok chart says, whoever draws it.
|
|
2
|
+
|
|
3
|
+
Bedrok does not draw. The assistant in the host does, with whatever chart tool
|
|
4
|
+
that host has, so two charts drawn in two places used to look like two
|
|
5
|
+
products. What Bedrok can fix is the content: the same title rules, the same
|
|
6
|
+
subtitle carrying the units, the same axes, colours, labels, source line and
|
|
7
|
+
footnotes, every time. The prose version is bedrok/seeds/charting.md
|
|
8
|
+
(bedrok://charting); this module is the arithmetic of it.
|
|
9
|
+
|
|
10
|
+
The house decisions (2026-10-11): two vertical scales when units differ; units
|
|
11
|
+
in the subtitle and on the vertical axis, never in the title; a chart by
|
|
12
|
+
default, a table only when asked; short answers.
|
|
13
|
+
|
|
14
|
+
Pure data, in core: nothing here imports a layer above.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import math
|
|
19
|
+
from datetime import date
|
|
20
|
+
|
|
21
|
+
# Okabe–Ito, without its yellow (unreadable on white): seven hues that stay
|
|
22
|
+
# distinct for the common forms of colour blindness. Assigned in the order the
|
|
23
|
+
# series were asked for, so a series keeps its colour across charts.
|
|
24
|
+
PALETTE = ["#0072B2", "#E69F00", "#009E73", "#D55E00", "#56B4E9", "#CC79A7", "#7F7F7F"]
|
|
25
|
+
|
|
26
|
+
# The house style, repeated on every recipe so a host that never read
|
|
27
|
+
# bedrok://charting still draws it the same way.
|
|
28
|
+
LAYOUT = {
|
|
29
|
+
"container": "a card with a border",
|
|
30
|
+
"title": "top, bold; never carries units",
|
|
31
|
+
"subtitle": "under the title; carries the units and how the data is measured",
|
|
32
|
+
"legend": "below the chart; clicking a series hides or shows it",
|
|
33
|
+
"source": "one line under the legend",
|
|
34
|
+
"footnotes": "under the source line, smaller",
|
|
35
|
+
"toggle": "a switch between the chart and a table of the same numbers",
|
|
36
|
+
"y_axis": "labelled with the units; left scale solid lines, right scale dashed",
|
|
37
|
+
"x_axis": "the labels given, every `tick_every`-th one shown",
|
|
38
|
+
"gaps": "leave empty: never draw a line across a null",
|
|
39
|
+
"type": "lines; bars only when every series has a single period",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_HOW_WORDS = {
|
|
43
|
+
("weekly", "last"): "weekly closes", ("weekly", "mean"): "weekly averages",
|
|
44
|
+
("weekly", "sum"): "weekly totals", ("weekly", "first"): "first value of each week",
|
|
45
|
+
("monthly", "mean"): "monthly averages", ("monthly", "last"): "month-end values",
|
|
46
|
+
("monthly", "sum"): "monthly totals", ("monthly", "first"): "first value of each month",
|
|
47
|
+
("quarterly", "mean"): "quarterly averages", ("quarterly", "last"): "quarter-end values",
|
|
48
|
+
("quarterly", "sum"): "quarterly totals", ("quarterly", "first"): "first value of each quarter",
|
|
49
|
+
("annual", "mean"): "annual averages", ("annual", "last"): "year-end values",
|
|
50
|
+
("annual", "sum"): "annual totals", ("annual", "first"): "first value of each year",
|
|
51
|
+
}
|
|
52
|
+
_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
|
53
|
+
MAX_TICKS = 10
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def colour(i: int) -> str:
|
|
57
|
+
return PALETTE[i % len(PALETTE)]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def title_suggestion(names: list[str]) -> str:
|
|
61
|
+
"""One series: its name. Two: both. More: the first and a count. The
|
|
62
|
+
assistant may always replace it; it never carries units."""
|
|
63
|
+
if not names:
|
|
64
|
+
return ""
|
|
65
|
+
if len(names) == 1:
|
|
66
|
+
return names[0]
|
|
67
|
+
if len(names) == 2:
|
|
68
|
+
return f"{names[0]} and {names[1]}"
|
|
69
|
+
return f"{names[0]} and {len(names) - 1} others"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def axes(series: list[tuple[str, str]]) -> dict:
|
|
73
|
+
"""[(id, units)] → which series go on the left scale and which on the right.
|
|
74
|
+
|
|
75
|
+
One unit: one scale. Two: the first-listed series' units on the left, the
|
|
76
|
+
other on the right. Three or more cannot be read on two scales: the first
|
|
77
|
+
two groups are placed and `problem` says what to do instead."""
|
|
78
|
+
groups: list[str] = []
|
|
79
|
+
for _, u in series:
|
|
80
|
+
if u not in groups:
|
|
81
|
+
groups.append(u)
|
|
82
|
+
left = [i for i, u in series if u == groups[0]] if groups else []
|
|
83
|
+
right = [i for i, u in series if len(groups) > 1 and u == groups[1]]
|
|
84
|
+
out = {"left": {"units": groups[0] if groups else None, "series": left},
|
|
85
|
+
"right": {"units": groups[1], "series": right} if len(groups) > 1 else None,
|
|
86
|
+
"problem": None}
|
|
87
|
+
if len(groups) > 2:
|
|
88
|
+
dropped = [i for i, u in series if u in groups[2:]]
|
|
89
|
+
out["problem"] = (f"{len(groups)} different units cannot share two scales; "
|
|
90
|
+
f"{', '.join(dropped)} are not placed. Draw two charts, or ask "
|
|
91
|
+
"for transform='index_to' so every series is an index.")
|
|
92
|
+
return out
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _span_label(d: date, frequency: str) -> str:
|
|
96
|
+
if frequency == "quarterly":
|
|
97
|
+
return f"Q{(d.month - 1) // 3 + 1} {d.year}"
|
|
98
|
+
if frequency == "annual":
|
|
99
|
+
return str(d.year)
|
|
100
|
+
return f"{_MONTHS[d.month - 1]} {d.year}"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def subtitle(placed: dict, frequency: str, how: str | None, first: date | None,
|
|
104
|
+
last: date | None) -> str:
|
|
105
|
+
"""Units first (per scale when there are two), then how the data is
|
|
106
|
+
measured, then the span: "Percent; weekly closes; Oct 2023 to Oct 2026"."""
|
|
107
|
+
def cap(u: str | None) -> str:
|
|
108
|
+
return (u[:1].upper() + u[1:]) if u else ""
|
|
109
|
+
if placed.get("right"):
|
|
110
|
+
units = f"Left: {placed['left']['units']}. Right: {placed['right']['units']}"
|
|
111
|
+
else:
|
|
112
|
+
units = cap(placed["left"]["units"])
|
|
113
|
+
parts = [units]
|
|
114
|
+
parts.append(_HOW_WORDS.get((frequency, how or ""), frequency))
|
|
115
|
+
if first and last:
|
|
116
|
+
parts.append(f"{_span_label(first, frequency)} to {_span_label(last, frequency)}")
|
|
117
|
+
return "; ".join(p for p in parts if p)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def x_labels(dates: list[date], frequency: str) -> dict:
|
|
121
|
+
"""Labels by frequency, and how many to skip so about ten show."""
|
|
122
|
+
def one(d: date) -> str:
|
|
123
|
+
if frequency == "weekly":
|
|
124
|
+
return f"{d.day} {_MONTHS[d.month - 1]} {d.year}"
|
|
125
|
+
if frequency == "monthly":
|
|
126
|
+
return f"{_MONTHS[d.month - 1]} {str(d.year)[2:]}"
|
|
127
|
+
if frequency == "quarterly":
|
|
128
|
+
return f"Q{(d.month - 1) // 3 + 1} {str(d.year)[2:]}"
|
|
129
|
+
return str(d.year)
|
|
130
|
+
n = len(dates)
|
|
131
|
+
return {"labels": [one(d) for d in dates],
|
|
132
|
+
"tick_every": max(1, math.ceil(n / MAX_TICKS))}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def source_line(publishers: list[tuple[str, bool, bool]]) -> str:
|
|
136
|
+
"""[(publisher name, personal use?, fetched live?)] → one line, each
|
|
137
|
+
publisher once, in order, with its condition when it has one."""
|
|
138
|
+
seen, parts = set(), []
|
|
139
|
+
for name, personal, live in publishers:
|
|
140
|
+
if name in seen:
|
|
141
|
+
continue
|
|
142
|
+
seen.add(name)
|
|
143
|
+
cond = [c for c, on in (("personal use only", personal), ("fetched live, not stored", live)) if on]
|
|
144
|
+
parts.append(name + (f" ({', '.join(cond)})" if cond else ""))
|
|
145
|
+
return "Source: " + "; ".join(parts) + "." if parts else ""
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def footnotes(entries: list[dict], table: dict, frequency: str) -> list[str]:
|
|
149
|
+
"""Everything the chart cannot say by itself, one short line each: the
|
|
150
|
+
loud warnings for the period shown, periods not over or missing a part,
|
|
151
|
+
a cut, and series drawn as steps."""
|
|
152
|
+
notes: list[str] = []
|
|
153
|
+
names = {e["series_id"]: e.get("name", e["series_id"]) for e in entries}
|
|
154
|
+
for e in entries:
|
|
155
|
+
for w in e.get("warnings") or []:
|
|
156
|
+
period = w.get("period_start") or ""
|
|
157
|
+
if w.get("period_end"):
|
|
158
|
+
period += f" to {w['period_end']}"
|
|
159
|
+
# The whole effect: warning texts are kept short in the seed, and a
|
|
160
|
+
# cut at the first ". " breaks on abbreviations ("incl.").
|
|
161
|
+
line = f"{names[e['series_id']]}, {period}: {(w.get('effect_on_data') or '').strip()}"
|
|
162
|
+
if line not in notes:
|
|
163
|
+
notes.append(line)
|
|
164
|
+
for sid, stepped in (table.get("stepped") or {}).items():
|
|
165
|
+
if stepped:
|
|
166
|
+
notes.append(f"{names.get(sid, sid)} is published less often than {frequency}; "
|
|
167
|
+
"it is drawn as a step, each value held until the next.")
|
|
168
|
+
for sid, partial in (table.get("partial_last") or {}).items():
|
|
169
|
+
if partial:
|
|
170
|
+
notes.append(f"{names.get(sid, sid)}: the latest period is not over yet.")
|
|
171
|
+
for sid, short in (table.get("incomplete") or {}).items():
|
|
172
|
+
notes.append(f"{names.get(sid, sid)}: {', '.join(short)} hold only part of their period "
|
|
173
|
+
"(a month or quarter not published).")
|
|
174
|
+
if table.get("cut"):
|
|
175
|
+
notes.append(table["cut"])
|
|
176
|
+
return notes
|