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.
Files changed (109) hide show
  1. bedrok-0.2.0/LICENSE +21 -0
  2. bedrok-0.2.0/PKG-INFO +113 -0
  3. bedrok-0.2.0/README.md +74 -0
  4. bedrok-0.2.0/bedrok/__init__.py +22 -0
  5. bedrok-0.2.0/bedrok/__main__.py +75 -0
  6. bedrok-0.2.0/bedrok/core/__init__.py +5 -0
  7. bedrok-0.2.0/bedrok/core/aliases.py +25 -0
  8. bedrok-0.2.0/bedrok/core/charting.py +176 -0
  9. bedrok-0.2.0/bedrok/core/config.py +228 -0
  10. bedrok-0.2.0/bedrok/core/geography.py +97 -0
  11. bedrok-0.2.0/bedrok/core/licensing.py +152 -0
  12. bedrok-0.2.0/bedrok/core/query.py +47 -0
  13. bedrok-0.2.0/bedrok/core/schema.py +46 -0
  14. bedrok-0.2.0/bedrok/core/sources.py +84 -0
  15. bedrok-0.2.0/bedrok/core/store.py +233 -0
  16. bedrok-0.2.0/bedrok/core/units.py +64 -0
  17. bedrok-0.2.0/bedrok/core/warnings.py +139 -0
  18. bedrok-0.2.0/bedrok/engines/__init__.py +0 -0
  19. bedrok-0.2.0/bedrok/engines/pctile.py +104 -0
  20. bedrok-0.2.0/bedrok/engines/rates_infl.py +91 -0
  21. bedrok-0.2.0/bedrok/ingest/__init__.py +4 -0
  22. bedrok-0.2.0/bedrok/ingest/adapters/__init__.py +0 -0
  23. bedrok-0.2.0/bedrok/ingest/adapters/_util.py +92 -0
  24. bedrok-0.2.0/bedrok/ingest/adapters/bea.py +88 -0
  25. bedrok-0.2.0/bedrok/ingest/adapters/bls.py +115 -0
  26. bedrok-0.2.0/bedrok/ingest/adapters/boc.py +41 -0
  27. bedrok-0.2.0/bedrok/ingest/adapters/boe.py +85 -0
  28. bedrok-0.2.0/bedrok/ingest/adapters/census.py +81 -0
  29. bedrok-0.2.0/bedrok/ingest/adapters/cftc.py +139 -0
  30. bedrok-0.2.0/bedrok/ingest/adapters/destatis.py +107 -0
  31. bedrok-0.2.0/bedrok/ingest/adapters/eia.py +51 -0
  32. bedrok-0.2.0/bedrok/ingest/adapters/estat.py +73 -0
  33. bedrok-0.2.0/bedrok/ingest/adapters/eurostat.py +112 -0
  34. bedrok-0.2.0/bedrok/ingest/adapters/fedboard.py +139 -0
  35. bedrok-0.2.0/bedrok/ingest/adapters/fedfiles.py +259 -0
  36. bedrok-0.2.0/bedrok/ingest/adapters/fred.py +32 -0
  37. bedrok-0.2.0/bedrok/ingest/adapters/ine.py +63 -0
  38. bedrok-0.2.0/bedrok/ingest/adapters/mof_jp.py +64 -0
  39. bedrok-0.2.0/bedrok/ingest/adapters/nber.py +46 -0
  40. bedrok-0.2.0/bedrok/ingest/adapters/nyfed.py +118 -0
  41. bedrok-0.2.0/bedrok/ingest/adapters/ons.py +78 -0
  42. bedrok-0.2.0/bedrok/ingest/adapters/pxweb.py +91 -0
  43. bedrok-0.2.0/bedrok/ingest/adapters/sdmx.py +143 -0
  44. bedrok-0.2.0/bedrok/ingest/adapters/statcan.py +53 -0
  45. bedrok-0.2.0/bedrok/ingest/adapters/worldbank.py +127 -0
  46. bedrok-0.2.0/bedrok/ingest/backfill.py +74 -0
  47. bedrok-0.2.0/bedrok/ingest/derived.py +225 -0
  48. bedrok-0.2.0/bedrok/ingest/live.py +38 -0
  49. bedrok-0.2.0/bedrok/ingest/pipeline.py +820 -0
  50. bedrok-0.2.0/bedrok/ingest/refresh.py +73 -0
  51. bedrok-0.2.0/bedrok/ingest/transforms.py +228 -0
  52. bedrok-0.2.0/bedrok/ops/__init__.py +4 -0
  53. bedrok-0.2.0/bedrok/ops/docs.py +219 -0
  54. bedrok-0.2.0/bedrok/ops/doctor.py +142 -0
  55. bedrok-0.2.0/bedrok/ops/freshness.py +69 -0
  56. bedrok-0.2.0/bedrok/ops/smoke.py +108 -0
  57. bedrok-0.2.0/bedrok/ops/tieout.py +201 -0
  58. bedrok-0.2.0/bedrok/ops/validate.py +172 -0
  59. bedrok-0.2.0/bedrok/seeds/charting.md +70 -0
  60. bedrok-0.2.0/bedrok/seeds/gaps.md +51 -0
  61. bedrok-0.2.0/bedrok/seeds/macro_catalog.csv +199 -0
  62. bedrok-0.2.0/bedrok/seeds/sources.csv +32 -0
  63. bedrok-0.2.0/bedrok/seeds/warnings.csv +44 -0
  64. bedrok-0.2.0/bedrok/surfaces/__init__.py +5 -0
  65. bedrok-0.2.0/bedrok/surfaces/mcp_server.py +950 -0
  66. bedrok-0.2.0/bedrok.egg-info/PKG-INFO +113 -0
  67. bedrok-0.2.0/bedrok.egg-info/SOURCES.txt +107 -0
  68. bedrok-0.2.0/bedrok.egg-info/dependency_links.txt +1 -0
  69. bedrok-0.2.0/bedrok.egg-info/entry_points.txt +2 -0
  70. bedrok-0.2.0/bedrok.egg-info/requires.txt +15 -0
  71. bedrok-0.2.0/bedrok.egg-info/top_level.txt +1 -0
  72. bedrok-0.2.0/pyproject.toml +67 -0
  73. bedrok-0.2.0/setup.cfg +4 -0
  74. bedrok-0.2.0/tests/test_boe_adapter.py +73 -0
  75. bedrok-0.2.0/tests/test_cache_rule.py +80 -0
  76. bedrok-0.2.0/tests/test_cftc_adapter.py +113 -0
  77. bedrok-0.2.0/tests/test_charting.py +82 -0
  78. bedrok-0.2.0/tests/test_derived.py +166 -0
  79. bedrok-0.2.0/tests/test_doctor.py +111 -0
  80. bedrok-0.2.0/tests/test_eia_adapter.py +26 -0
  81. bedrok-0.2.0/tests/test_eurostat_bulk.py +135 -0
  82. bedrok-0.2.0/tests/test_fedboard_adapter.py +76 -0
  83. bedrok-0.2.0/tests/test_fedfiles_adapter.py +216 -0
  84. bedrok-0.2.0/tests/test_freshness.py +58 -0
  85. bedrok-0.2.0/tests/test_geography.py +57 -0
  86. bedrok-0.2.0/tests/test_install.py +128 -0
  87. bedrok-0.2.0/tests/test_layering.py +102 -0
  88. bedrok-0.2.0/tests/test_licensing.py +77 -0
  89. bedrok-0.2.0/tests/test_macro_seed.py +121 -0
  90. bedrok-0.2.0/tests/test_mcp_server.py +346 -0
  91. bedrok-0.2.0/tests/test_mof_jp_adapter.py +30 -0
  92. bedrok-0.2.0/tests/test_ons_adapter.py +66 -0
  93. bedrok-0.2.0/tests/test_pctile.py +56 -0
  94. bedrok-0.2.0/tests/test_profile.py +121 -0
  95. bedrok-0.2.0/tests/test_publication.py +142 -0
  96. bedrok-0.2.0/tests/test_refresh.py +58 -0
  97. bedrok-0.2.0/tests/test_repoint.py +114 -0
  98. bedrok-0.2.0/tests/test_restricted_sync.py +94 -0
  99. bedrok-0.2.0/tests/test_sdmx_adapter.py +155 -0
  100. bedrok-0.2.0/tests/test_second_wave_adapters.py +162 -0
  101. bedrok-0.2.0/tests/test_smoke.py +107 -0
  102. bedrok-0.2.0/tests/test_sources.py +102 -0
  103. bedrok-0.2.0/tests/test_store.py +79 -0
  104. bedrok-0.2.0/tests/test_tieout.py +54 -0
  105. bedrok-0.2.0/tests/test_transforms.py +143 -0
  106. bedrok-0.2.0/tests/test_us_agency_adapters.py +159 -0
  107. bedrok-0.2.0/tests/test_validate.py +52 -0
  108. bedrok-0.2.0/tests/test_warnings.py +93 -0
  109. 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,5 @@
1
+ """Foundations: configuration, the store, and vocabulary shared by everything.
2
+
3
+ Nothing here imports from ingest, engines, ops or surfaces. If something in this
4
+ package needs one of those, it is in the wrong package.
5
+ """
@@ -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