manualforge 0.1.1__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.
@@ -0,0 +1,236 @@
1
+ Metadata-Version: 2.4
2
+ Name: manualforge
3
+ Version: 0.1.1
4
+ Requires-Python: >=3.10
5
+ Description-Content-Type: text/markdown
6
+ Requires-Dist: kedro~=1.2.0
7
+ Requires-Dist: kedro-datasets~=9.2
8
+ Requires-Dist: polars>=1.0
9
+ Requires-Dist: polars-runtime-compat>=1.38.1
10
+ Requires-Dist: fastexcel~=0.19
11
+ Requires-Dist: Jinja2>=3.0
12
+ Requires-Dist: duckdb>=1.0
13
+ Provides-Extra: docs
14
+ Requires-Dist: docutils<0.21; extra == "docs"
15
+ Requires-Dist: sphinx<7.3,>=5.3; extra == "docs"
16
+ Requires-Dist: sphinx_rtd_theme==2.0.0; extra == "docs"
17
+ Requires-Dist: nbsphinx==0.8.1; extra == "docs"
18
+ Requires-Dist: sphinx-autodoc-typehints==1.20.2; extra == "docs"
19
+ Requires-Dist: sphinx_copybutton==0.5.2; extra == "docs"
20
+ Requires-Dist: ipykernel<7.0,>=5.3; extra == "docs"
21
+ Requires-Dist: Jinja2<3.2.0; extra == "docs"
22
+ Requires-Dist: myst-parser<2.1,>=1.0; extra == "docs"
23
+ Provides-Extra: dev
24
+ Requires-Dist: ipython>=8.10; extra == "dev"
25
+ Requires-Dist: jupyterlab>=3.0; extra == "dev"
26
+ Requires-Dist: notebook; extra == "dev"
27
+ Requires-Dist: pytest-cov<7,>=3; extra == "dev"
28
+ Requires-Dist: pytest-mock<2.0,>=1.7.1; extra == "dev"
29
+ Requires-Dist: pytest~=9.0; extra == "dev"
30
+ Requires-Dist: ruff~=0.15.0; extra == "dev"
31
+
32
+ # ManualForge
33
+
34
+ > **Configuration-driven management manual generation framework.**
35
+ > Define your data sources, fields, and templates in YAML — get a formatted report.
36
+
37
+ Built on [Kedro](https://kedro.org) pipelines with [Polars](https://pola.rs) for data processing and [Typst](https://typst.app) for document rendering.
38
+
39
+ ## Philosophy
40
+
41
+ ManualForge separates **what** you want to produce from **how** it's produced.
42
+
43
+ - **What**: Defined in `conf/base/parameters_manualforge.yml` — your data sources, expected columns, standardization rules, sort orders, summary dimensions, and report templates.
44
+ - **How**: Implemented by the pipeline nodes — reusable data processing functions that read from your config.
45
+
46
+ To create a new manual for a different domain, you only need to edit the config file (and optionally provide new templates). No Python code changes required.
47
+
48
+ ## Features
49
+
50
+ | Capability | Description |
51
+ |---|---|
52
+ | **Multi-sheet Excel ingestion** | Auto-detect headers, filter cover sheets, merge into structured DataFrames |
53
+ | **Field standardization** | Mapping files + exact matching + fuzzy matching (difflib / duckdb) |
54
+ | **Config-driven summaries** | Define group-by dimensions, sort orders, ability categories, and output paths in YAML |
55
+ | **Typst report generation** | Jinja2 templates → Typst source → PDF compilation |
56
+ | **Pipeline hooks** | Shell command hooks at pipeline/node granularity for pre/post processing |
57
+
58
+ ## Quick Start
59
+
60
+ ```bash
61
+ # 1. Install dependencies
62
+ pip install -r requirements.txt
63
+
64
+ # 2. Copy and customize configuration
65
+ cp conf/examples/parameters_manualforge.yml.example conf/base/parameters_manualforge.yml
66
+ cp conf/examples/catalog.yml.example conf/base/catalog.yml
67
+ cp conf/examples/hooks.yml.example conf/base/hooks.yml
68
+ cp conf/examples/parameters.yml.example conf/base/parameters.yml
69
+ cp conf/examples/credentials.yml.example conf/local/credentials.yml
70
+
71
+ # 3. Edit the config files to point to your data sources
72
+ # (conf/base/ is gitignored — your real configs stay local)
73
+
74
+ # 4. Run the pipeline
75
+ kedro run
76
+
77
+ # Run specific node groups
78
+ kedro run --tags conversion # Excel → Parquet only
79
+ kedro run --tags standardization # Standardization only
80
+ kedro run --tags csv # Summary tables only
81
+ ```
82
+
83
+ ## Project Structure
84
+
85
+ ```
86
+ ├── conf/
87
+ │ ├── base/ # ★ Gitignored — copy from examples/
88
+ │ │ ├── parameters_manualforge.yml # Central project configuration
89
+ │ │ ├── catalog.yml # Kedro data catalog
90
+ │ │ ├── hooks.yml # Pipeline hooks (shell commands)
91
+ │ │ └── parameters.yml # Pipeline parameters
92
+ │ ├── examples/ # ★ Tracked example templates
93
+ │ │ ├── parameters_manualforge.yml.example
94
+ │ │ ├── catalog.yml.example
95
+ │ │ ├── hooks.yml.example
96
+ │ │ ├── parameters.yml.example
97
+ │ │ └── credentials.yml.example
98
+ │ ├── local/ # Local-only (gitignored)
99
+ │ │ └── credentials.yml
100
+ │ └── logging.yml
101
+ ├── data/ # Gitignored except .gitkeep
102
+ │ ├── 01_raw/ # Raw Excel/CSV + mapping files
103
+ │ ├── 02_intermediate/ # Parquet, reconcile reports
104
+ │ ├── 03_primary/ # Standardized data
105
+ │ ├── 04_feature/ # Summary tables (CSV + Markdown)
106
+ │ └── 08_reporting/ # Typst sources & compiled PDFs
107
+ ├── scripts/ # Auxiliary scripts
108
+ │ ├── convert_csv_to_md.py # CSV → Markdown conversion
109
+ │ ├── extract_rule_field_mapping.py # Rule field extraction
110
+ │ ├── extract_rule_overview.py # Rule overview extraction
111
+ │ └── render_with_forge.py # Markdown → DOCX/PDF rendering
112
+ ├── src/manualforge/ # Framework source code
113
+ │ ├── config.py # Configuration helper utilities
114
+ │ ├── hooks.py # Kedro pipeline hooks
115
+ │ ├── io/ # Custom Kedro datasets (PolarsExcelDataset)
116
+ │ ├── pipelines/ # Pipeline definitions & node functions
117
+ │ └── settings.py # Kedro project settings
118
+ ├── templates/ # Jinja2 Typst templates
119
+ │ └── report.typ.j2
120
+ ├── pyproject.toml # Project metadata & dependencies
121
+ └── requirements.txt
122
+ ```
123
+
124
+ ## Configuration Guide
125
+
126
+ The central configuration file is `conf/base/parameters_manualforge.yml`. Copy from `conf/examples/` and customize:
127
+
128
+ ### 1. Data Sources
129
+
130
+ Define your Excel files, expected headers, and sheet filtering rules:
131
+
132
+ ```yaml
133
+ datasources:
134
+ primary_data:
135
+ filepath: "data/01_raw/your_data.xlsx"
136
+ sheet:
137
+ exclude_names: ["封面", "封皮"]
138
+ name_becomes_column: "sheet_name"
139
+ header_detection:
140
+ mode: keyword_match
141
+ expected_headers:
142
+ - "column_a"
143
+ - "column_b"
144
+ cleaning:
145
+ drop_rows_where:
146
+ column_a: ["column_a"] # drop residual header rows
147
+ fill_null: forward
148
+ deduplicate: true
149
+ ```
150
+
151
+ ### 2. Field Standardization
152
+
153
+ Define which fields to standardize, their mapping files, and special corrections:
154
+
155
+ ```yaml
156
+ standardization:
157
+ fields:
158
+ - name: "dept_name"
159
+ mapping_file: "data/01_raw/dept_list"
160
+ case_corrections:
161
+ wrong_name: "correct_name"
162
+ special_mappings:
163
+ alias: "canonical_name"
164
+ fuzzy:
165
+ enabled: true
166
+ threshold: 0.8
167
+ method: difflib # difflib | duckdb
168
+ ```
169
+
170
+ ### 3. Sort Orders
171
+
172
+ Define reusable sort order lists referenced by summaries:
173
+
174
+ ```yaml
175
+ sort_orders:
176
+ model_names:
177
+ - "Model A"
178
+ - "Model B"
179
+ dep_names:
180
+ - "HR"
181
+ - "Finance"
182
+ ```
183
+
184
+ ### 4. Summaries
185
+
186
+ Define what summary tables to generate:
187
+
188
+ ```yaml
189
+ summaries:
190
+ my_summary:
191
+ description: "Fields grouped by model and department"
192
+ group_by: ["model", "department"]
193
+ struct_columns: ["module", "system", "field_name"]
194
+ sort_by:
195
+ department: dep_names
196
+ output:
197
+ csv: "data/04_feature/my_summary.csv"
198
+ ```
199
+
200
+ ### 5. Reports
201
+
202
+ Define report templates and output:
203
+
204
+ ```yaml
205
+ reports:
206
+ my_report:
207
+ description: "Rules cookbook"
208
+ template_source: inline
209
+ data_source: rules_data
210
+ output_typ: "data/08_reporting/output.typ"
211
+ typst_compile:
212
+ enabled: true
213
+ ```
214
+
215
+ ## Data Layers
216
+
217
+ | Layer | Directory | Description |
218
+ |---|---|---|
219
+ | Raw | `data/01_raw/` | Source Excel/CSV files, mapping files |
220
+ | Intermediate | `data/02_intermediate/` | Parquet, reconcile reports |
221
+ | Primary | `data/03_primary/` | Standardized data |
222
+ | Feature | `data/04_feature/` | Summary tables (CSV + Markdown) |
223
+ | Reporting | `data/08_reporting/` | Typst sources & PDF output |
224
+
225
+ ## Requirements
226
+
227
+ - Python >= 3.10
228
+ - [Typst](https://github.com/typst/typst) CLI (for PDF compilation)
229
+
230
+ ## Development
231
+
232
+ ```bash
233
+ pip install -e ".[dev]"
234
+ ruff check src/
235
+ pytest
236
+ ```
@@ -0,0 +1,205 @@
1
+ # ManualForge
2
+
3
+ > **Configuration-driven management manual generation framework.**
4
+ > Define your data sources, fields, and templates in YAML — get a formatted report.
5
+
6
+ Built on [Kedro](https://kedro.org) pipelines with [Polars](https://pola.rs) for data processing and [Typst](https://typst.app) for document rendering.
7
+
8
+ ## Philosophy
9
+
10
+ ManualForge separates **what** you want to produce from **how** it's produced.
11
+
12
+ - **What**: Defined in `conf/base/parameters_manualforge.yml` — your data sources, expected columns, standardization rules, sort orders, summary dimensions, and report templates.
13
+ - **How**: Implemented by the pipeline nodes — reusable data processing functions that read from your config.
14
+
15
+ To create a new manual for a different domain, you only need to edit the config file (and optionally provide new templates). No Python code changes required.
16
+
17
+ ## Features
18
+
19
+ | Capability | Description |
20
+ |---|---|
21
+ | **Multi-sheet Excel ingestion** | Auto-detect headers, filter cover sheets, merge into structured DataFrames |
22
+ | **Field standardization** | Mapping files + exact matching + fuzzy matching (difflib / duckdb) |
23
+ | **Config-driven summaries** | Define group-by dimensions, sort orders, ability categories, and output paths in YAML |
24
+ | **Typst report generation** | Jinja2 templates → Typst source → PDF compilation |
25
+ | **Pipeline hooks** | Shell command hooks at pipeline/node granularity for pre/post processing |
26
+
27
+ ## Quick Start
28
+
29
+ ```bash
30
+ # 1. Install dependencies
31
+ pip install -r requirements.txt
32
+
33
+ # 2. Copy and customize configuration
34
+ cp conf/examples/parameters_manualforge.yml.example conf/base/parameters_manualforge.yml
35
+ cp conf/examples/catalog.yml.example conf/base/catalog.yml
36
+ cp conf/examples/hooks.yml.example conf/base/hooks.yml
37
+ cp conf/examples/parameters.yml.example conf/base/parameters.yml
38
+ cp conf/examples/credentials.yml.example conf/local/credentials.yml
39
+
40
+ # 3. Edit the config files to point to your data sources
41
+ # (conf/base/ is gitignored — your real configs stay local)
42
+
43
+ # 4. Run the pipeline
44
+ kedro run
45
+
46
+ # Run specific node groups
47
+ kedro run --tags conversion # Excel → Parquet only
48
+ kedro run --tags standardization # Standardization only
49
+ kedro run --tags csv # Summary tables only
50
+ ```
51
+
52
+ ## Project Structure
53
+
54
+ ```
55
+ ├── conf/
56
+ │ ├── base/ # ★ Gitignored — copy from examples/
57
+ │ │ ├── parameters_manualforge.yml # Central project configuration
58
+ │ │ ├── catalog.yml # Kedro data catalog
59
+ │ │ ├── hooks.yml # Pipeline hooks (shell commands)
60
+ │ │ └── parameters.yml # Pipeline parameters
61
+ │ ├── examples/ # ★ Tracked example templates
62
+ │ │ ├── parameters_manualforge.yml.example
63
+ │ │ ├── catalog.yml.example
64
+ │ │ ├── hooks.yml.example
65
+ │ │ ├── parameters.yml.example
66
+ │ │ └── credentials.yml.example
67
+ │ ├── local/ # Local-only (gitignored)
68
+ │ │ └── credentials.yml
69
+ │ └── logging.yml
70
+ ├── data/ # Gitignored except .gitkeep
71
+ │ ├── 01_raw/ # Raw Excel/CSV + mapping files
72
+ │ ├── 02_intermediate/ # Parquet, reconcile reports
73
+ │ ├── 03_primary/ # Standardized data
74
+ │ ├── 04_feature/ # Summary tables (CSV + Markdown)
75
+ │ └── 08_reporting/ # Typst sources & compiled PDFs
76
+ ├── scripts/ # Auxiliary scripts
77
+ │ ├── convert_csv_to_md.py # CSV → Markdown conversion
78
+ │ ├── extract_rule_field_mapping.py # Rule field extraction
79
+ │ ├── extract_rule_overview.py # Rule overview extraction
80
+ │ └── render_with_forge.py # Markdown → DOCX/PDF rendering
81
+ ├── src/manualforge/ # Framework source code
82
+ │ ├── config.py # Configuration helper utilities
83
+ │ ├── hooks.py # Kedro pipeline hooks
84
+ │ ├── io/ # Custom Kedro datasets (PolarsExcelDataset)
85
+ │ ├── pipelines/ # Pipeline definitions & node functions
86
+ │ └── settings.py # Kedro project settings
87
+ ├── templates/ # Jinja2 Typst templates
88
+ │ └── report.typ.j2
89
+ ├── pyproject.toml # Project metadata & dependencies
90
+ └── requirements.txt
91
+ ```
92
+
93
+ ## Configuration Guide
94
+
95
+ The central configuration file is `conf/base/parameters_manualforge.yml`. Copy from `conf/examples/` and customize:
96
+
97
+ ### 1. Data Sources
98
+
99
+ Define your Excel files, expected headers, and sheet filtering rules:
100
+
101
+ ```yaml
102
+ datasources:
103
+ primary_data:
104
+ filepath: "data/01_raw/your_data.xlsx"
105
+ sheet:
106
+ exclude_names: ["封面", "封皮"]
107
+ name_becomes_column: "sheet_name"
108
+ header_detection:
109
+ mode: keyword_match
110
+ expected_headers:
111
+ - "column_a"
112
+ - "column_b"
113
+ cleaning:
114
+ drop_rows_where:
115
+ column_a: ["column_a"] # drop residual header rows
116
+ fill_null: forward
117
+ deduplicate: true
118
+ ```
119
+
120
+ ### 2. Field Standardization
121
+
122
+ Define which fields to standardize, their mapping files, and special corrections:
123
+
124
+ ```yaml
125
+ standardization:
126
+ fields:
127
+ - name: "dept_name"
128
+ mapping_file: "data/01_raw/dept_list"
129
+ case_corrections:
130
+ wrong_name: "correct_name"
131
+ special_mappings:
132
+ alias: "canonical_name"
133
+ fuzzy:
134
+ enabled: true
135
+ threshold: 0.8
136
+ method: difflib # difflib | duckdb
137
+ ```
138
+
139
+ ### 3. Sort Orders
140
+
141
+ Define reusable sort order lists referenced by summaries:
142
+
143
+ ```yaml
144
+ sort_orders:
145
+ model_names:
146
+ - "Model A"
147
+ - "Model B"
148
+ dep_names:
149
+ - "HR"
150
+ - "Finance"
151
+ ```
152
+
153
+ ### 4. Summaries
154
+
155
+ Define what summary tables to generate:
156
+
157
+ ```yaml
158
+ summaries:
159
+ my_summary:
160
+ description: "Fields grouped by model and department"
161
+ group_by: ["model", "department"]
162
+ struct_columns: ["module", "system", "field_name"]
163
+ sort_by:
164
+ department: dep_names
165
+ output:
166
+ csv: "data/04_feature/my_summary.csv"
167
+ ```
168
+
169
+ ### 5. Reports
170
+
171
+ Define report templates and output:
172
+
173
+ ```yaml
174
+ reports:
175
+ my_report:
176
+ description: "Rules cookbook"
177
+ template_source: inline
178
+ data_source: rules_data
179
+ output_typ: "data/08_reporting/output.typ"
180
+ typst_compile:
181
+ enabled: true
182
+ ```
183
+
184
+ ## Data Layers
185
+
186
+ | Layer | Directory | Description |
187
+ |---|---|---|
188
+ | Raw | `data/01_raw/` | Source Excel/CSV files, mapping files |
189
+ | Intermediate | `data/02_intermediate/` | Parquet, reconcile reports |
190
+ | Primary | `data/03_primary/` | Standardized data |
191
+ | Feature | `data/04_feature/` | Summary tables (CSV + Markdown) |
192
+ | Reporting | `data/08_reporting/` | Typst sources & PDF output |
193
+
194
+ ## Requirements
195
+
196
+ - Python >= 3.10
197
+ - [Typst](https://github.com/typst/typst) CLI (for PDF compilation)
198
+
199
+ ## Development
200
+
201
+ ```bash
202
+ pip install -e ".[dev]"
203
+ ruff check src/
204
+ pytest
205
+ ```
@@ -0,0 +1,92 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ requires-python = ">=3.10"
7
+ name = "manualforge"
8
+ readme = "README.md"
9
+ dynamic = ["version"]
10
+ dependencies = [
11
+ "kedro~=1.2.0",
12
+ "kedro-datasets~=9.2",
13
+ "polars>=1.0",
14
+ "polars-runtime-compat>=1.38.1",
15
+ "fastexcel~=0.19",
16
+ "Jinja2>=3.0",
17
+ "duckdb>=1.0",
18
+ ]
19
+
20
+ [project.scripts]
21
+ "manualforge" = "manualforge.__main__:main"
22
+
23
+ [project.entry-points."kedro.hooks"]
24
+
25
+ [project.optional-dependencies]
26
+ docs = [
27
+ "docutils<0.21",
28
+ "sphinx>=5.3,<7.3",
29
+ "sphinx_rtd_theme==2.0.0",
30
+ "nbsphinx==0.8.1",
31
+ "sphinx-autodoc-typehints==1.20.2",
32
+ "sphinx_copybutton==0.5.2",
33
+ "ipykernel>=5.3, <7.0",
34
+ "Jinja2<3.2.0",
35
+ "myst-parser>=1.0,<2.1"
36
+ ]
37
+ dev = [
38
+ "ipython>=8.10",
39
+ "jupyterlab>=3.0",
40
+ "notebook",
41
+ "pytest-cov>=3,<7",
42
+ "pytest-mock>=1.7.1, <2.0",
43
+ "pytest~=9.0",
44
+ "ruff~=0.15.0"
45
+ ]
46
+
47
+ [tool.setuptools.dynamic]
48
+ version = {attr = "manualforge.__version__"}
49
+
50
+ [tool.setuptools.packages.find]
51
+ where = ["src"]
52
+ namespaces = false
53
+
54
+ [tool.kedro]
55
+ package_name = "manualforge"
56
+ project_name = "manualforge"
57
+ kedro_init_version = "1.1.1"
58
+ tools = "['Linting', 'Testing', 'Custom Logging', 'Documentation', 'Data Structure']"
59
+ example_pipeline = "False"
60
+ source_dir = "src"
61
+
62
+ [tool.pytest.ini_options]
63
+ addopts = """
64
+ --cov-report term-missing \
65
+ --cov src/manualforge -ra"""
66
+
67
+ [tool.coverage.report]
68
+ fail_under = 0
69
+ show_missing = true
70
+ exclude_lines = ["pragma: no cover", "raise NotImplementedError"]
71
+
72
+ [tool.ruff.format]
73
+ docstring-code-format = true
74
+
75
+ [tool.ruff]
76
+ line-length = 88
77
+ show-fixes = true
78
+
79
+ [tool.ruff.lint]
80
+ select = [
81
+ "F", # Pyflakes
82
+ "W", # pycodestyle
83
+ "E", # pycodestyle
84
+ "I", # isort
85
+ "UP", # pyupgrade
86
+ "PL", # Pylint
87
+ "T201", # Print Statement
88
+ ]
89
+ ignore = ["E501"] # Ruff format takes care of line-too-long
90
+
91
+ [tool.kedro_telemetry]
92
+ project_id = "d5d6a6859fac4a9c899980b536809946"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """ManualForge — A configuration-driven management manual generation framework."""
2
+
3
+ __version__ = "0.1.1"
@@ -0,0 +1,25 @@
1
+ """manualforge file for ensuring the package is executable
2
+ as `manualforge` and `python -m manualforge`
3
+ """
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from kedro.framework.cli.utils import find_run_command
10
+ from kedro.framework.project import configure_project
11
+
12
+
13
+ def main(*args, **kwargs) -> Any:
14
+ package_name = Path(__file__).parent.name
15
+ configure_project(package_name)
16
+
17
+ interactive = hasattr(sys, "ps1")
18
+ kwargs["standalone_mode"] = not interactive
19
+
20
+ run = find_run_command(package_name)
21
+ return run(*args, **kwargs)
22
+
23
+
24
+ if __name__ == "__main__":
25
+ main()
@@ -0,0 +1,73 @@
1
+ """ManualForge configuration helpers.
2
+
3
+ Utilities for safely reading project configuration and providing sensible
4
+ defaults, so node functions stay clean when operating in config-driven mode.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from typing import Any
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Sentinel for "not set" to distinguish from explicit None.
15
+ _UNSET = object()
16
+
17
+
18
+ def get_datasource(config: dict, source_id: str) -> dict:
19
+ """Return the datasource sub-config for *source_id*."""
20
+ sources = config.get("datasources", {})
21
+ if source_id not in sources:
22
+ raise KeyError(f"Datasource '{source_id}' not found in config.datasources")
23
+ return sources[source_id]
24
+
25
+
26
+ def get_standardization(config: dict) -> dict:
27
+ """Return the standardization config section."""
28
+ return config.get("standardization", {})
29
+
30
+
31
+ def get_standardization_fields(config: dict) -> list[dict]:
32
+ """Return the list of field-standardization definitions."""
33
+ return get_standardization(config).get("fields", [])
34
+
35
+
36
+ def get_summary(config: dict, summary_id: str) -> dict:
37
+ """Return the summary sub-config for *summary_id*."""
38
+ summaries = config.get("summaries", {})
39
+ if summary_id not in summaries:
40
+ raise KeyError(f"Summary '{summary_id}' not found in config.summaries")
41
+ return summaries[summary_id]
42
+
43
+
44
+ def get_sort_order(config: dict, order_name: str) -> list[str]:
45
+ """Return a named sort-order list."""
46
+ return config.get("sort_orders", {}).get(order_name, [])
47
+
48
+
49
+ def _resolve_sort_ref(config: dict, ref: str | list) -> list[str]:
50
+ """Resolve a sort_by value which is either a sort-order name or an inline list."""
51
+ if isinstance(ref, list):
52
+ return ref
53
+ if isinstance(ref, str):
54
+ return get_sort_order(config, ref)
55
+ return []
56
+
57
+
58
+ def get_sort_list(config: dict, sort_by: dict) -> dict[str, list[str]]:
59
+ """Resolve a sort_by dict {column: order_name_or_list} → {column: [values]}."""
60
+ return {col: _resolve_sort_ref(config, ref) for col, ref in sort_by.items()}
61
+
62
+
63
+ def get_report(config: dict, report_id: str) -> dict:
64
+ """Return the report sub-config for *report_id*."""
65
+ reports = config.get("reports", {})
66
+ if report_id not in reports:
67
+ raise KeyError(f"Report '{report_id}' not found in config.reports")
68
+ return reports[report_id]
69
+
70
+
71
+ def get_project(config: dict) -> dict:
72
+ """Return the project metadata section."""
73
+ return config.get("project", {})