liander-open-data 0.1.0__py3-none-any.whl

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,49 @@
1
+ """Lightweight SQLite database for Liander's open SBI load profiles.
2
+
3
+ Quick start::
4
+
5
+ from liander_open_data import ProfileDatabase, build_database
6
+
7
+ build_database("data/Profiles", "liander_profiles.db")
8
+ with ProfileDatabase("liander_profiles.db") as db:
9
+ print(db.lookup("8411"))
10
+ df = db.get_profile("8411", columns=["mean", "p90"])
11
+ """
12
+
13
+ from importlib.metadata import PackageNotFoundError, version
14
+
15
+ try:
16
+ __version__ = version("liander-open-data")
17
+ except PackageNotFoundError: # pragma: no cover - running from a source tree
18
+ __version__ = "0.0.0"
19
+
20
+ from .database import (
21
+ SCHEMA_VERSION,
22
+ VALUE_COLUMNS,
23
+ ProfileDatabase,
24
+ ProfileNotFoundError,
25
+ build_database,
26
+ )
27
+ from .dictionary import (
28
+ SBICode,
29
+ load_dictionary,
30
+ normalize_code,
31
+ read_dictionary_csv,
32
+ read_dictionary_excel,
33
+ write_dictionary_csv,
34
+ )
35
+
36
+ __all__ = [
37
+ "SCHEMA_VERSION",
38
+ "VALUE_COLUMNS",
39
+ "ProfileDatabase",
40
+ "ProfileNotFoundError",
41
+ "SBICode",
42
+ "__version__",
43
+ "build_database",
44
+ "load_dictionary",
45
+ "normalize_code",
46
+ "read_dictionary_csv",
47
+ "read_dictionary_excel",
48
+ "write_dictionary_csv",
49
+ ]
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,178 @@
1
+ """Command line interface: ``liander-open-data <command> ...``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import logging
7
+ import sys
8
+ from collections.abc import Sequence
9
+ from pathlib import Path
10
+
11
+ from . import __version__
12
+ from .database import VALUE_COLUMNS, ProfileDatabase, ProfileNotFoundError, build_database
13
+ from .dictionary import load_dictionary, write_dictionary_csv
14
+
15
+ DEFAULT_DB = "liander_profiles.db"
16
+
17
+
18
+ def _progress(done: int, total: int, code: str) -> None:
19
+ width = 30
20
+ filled = int(width * done / total)
21
+ bar = "#" * filled + "-" * (width - filled)
22
+ end = "\n" if done == total else ""
23
+ print(f"\r[{bar}] {done}/{total} {code:<6}", end=end, file=sys.stderr, flush=True)
24
+
25
+
26
+ def _cmd_build(args: argparse.Namespace) -> int:
27
+ path = build_database(
28
+ args.profiles_dir,
29
+ args.output,
30
+ dictionary=args.dictionary,
31
+ overwrite=args.overwrite,
32
+ progress=None if args.quiet else _progress,
33
+ )
34
+ size_mb = path.stat().st_size / 1e6
35
+ print(f"Database written to {path} ({size_mb:.0f} MB)")
36
+ return 0
37
+
38
+
39
+ def _cmd_info(args: argparse.Namespace) -> int:
40
+ with ProfileDatabase(args.db) as db:
41
+ for key, value in db.metadata.items():
42
+ print(f"{key:>16}: {value}")
43
+ return 0
44
+
45
+
46
+ def _cmd_list(args: argparse.Namespace) -> int:
47
+ with ProfileDatabase(args.db) as db:
48
+ df = db.list_profiles()
49
+ print(df[["description", "n_timeseries"]].to_string(max_colwidth=70))
50
+ return 0
51
+
52
+
53
+ def _cmd_lookup(args: argparse.Namespace) -> int:
54
+ with ProfileDatabase(args.db) as db:
55
+ entry = db.lookup(args.code, fallback_to_parent=True)
56
+ for key, value in entry.to_dict().items():
57
+ print(f"{key:>22}: {value}")
58
+ print(f"{'recommended_profile':>22}: {entry.recommended_profile}")
59
+ return 0
60
+
61
+
62
+ def _cmd_search(args: argparse.Namespace) -> int:
63
+ with ProfileDatabase(args.db) as db:
64
+ df = db.dictionary(args.text, only_with_profile=args.with_profile)
65
+ cols = ["description", "has_profile", "best_profile", "ko_profile"]
66
+ print(df[cols].to_string(max_colwidth=60) if len(df) else "No matches.")
67
+ return 0
68
+
69
+
70
+ def _cmd_export(args: argparse.Namespace) -> int:
71
+ with ProfileDatabase(args.db) as db:
72
+ if args.recommended:
73
+ df = db.recommended_profile(args.code, args.columns, start=args.start, end=args.end)
74
+ else:
75
+ df = db.get_profile(args.code, args.columns, start=args.start, end=args.end)
76
+ out = Path(args.output)
77
+ if out.suffix.lower() == ".parquet":
78
+ df.to_parquet(out)
79
+ else:
80
+ df.to_csv(out)
81
+ print(f"Wrote {len(df)} rows to {out}")
82
+ return 0
83
+
84
+
85
+ def _cmd_dictionary(args: argparse.Namespace) -> int:
86
+ records = load_dictionary(args.source)
87
+ write_dictionary_csv(records, args.output)
88
+ print(f"Wrote {len(records)} SBI codes to {args.output}")
89
+ return 0
90
+
91
+
92
+ def build_parser() -> argparse.ArgumentParser:
93
+ """Create the argument parser (exposed for documentation generation)."""
94
+ parser = argparse.ArgumentParser(
95
+ prog="liander-open-data",
96
+ description="Build and query a SQLite database of Liander SBI load profiles.",
97
+ )
98
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
99
+ parser.add_argument("-v", "--verbose", action="store_true", help="show debug logging")
100
+ sub = parser.add_subparsers(dest="command", required=True, metavar="COMMAND")
101
+
102
+ def add_db(p: argparse.ArgumentParser) -> None:
103
+ p.add_argument("--db", default=DEFAULT_DB, help=f"database file (default: {DEFAULT_DB})")
104
+
105
+ p = sub.add_parser("build", help="build the database from the profile CSV files")
106
+ p.add_argument("profiles_dir", help="directory with <SBI code>.csv files")
107
+ p.add_argument(
108
+ "-o", "--output", default=DEFAULT_DB, help=f"output file (default: {DEFAULT_DB})"
109
+ )
110
+ p.add_argument(
111
+ "-d",
112
+ "--dictionary",
113
+ help="SBI dictionary (.xlsx or .csv); defaults to the bundled copy",
114
+ )
115
+ p.add_argument("--overwrite", action="store_true", help="replace an existing database")
116
+ p.add_argument("-q", "--quiet", action="store_true", help="hide the progress bar")
117
+ p.set_defaults(func=_cmd_build)
118
+
119
+ p = sub.add_parser("info", help="show build metadata of a database")
120
+ add_db(p)
121
+ p.set_defaults(func=_cmd_info)
122
+
123
+ p = sub.add_parser("list", help="list all stored profiles")
124
+ add_db(p)
125
+ p.set_defaults(func=_cmd_list)
126
+
127
+ p = sub.add_parser("lookup", help="show the dictionary entry of an SBI code")
128
+ p.add_argument("code")
129
+ add_db(p)
130
+ p.set_defaults(func=_cmd_lookup)
131
+
132
+ p = sub.add_parser("search", help="search SBI codes by description or code prefix")
133
+ p.add_argument("text")
134
+ p.add_argument("--with-profile", action="store_true", help="only codes with a profile")
135
+ add_db(p)
136
+ p.set_defaults(func=_cmd_search)
137
+
138
+ p = sub.add_parser("export", help="export one profile to CSV or Parquet")
139
+ p.add_argument("code")
140
+ p.add_argument("-o", "--output", required=True, help="output .csv or .parquet file")
141
+ p.add_argument(
142
+ "-c", "--columns", nargs="+", choices=VALUE_COLUMNS, help="columns (default: all)"
143
+ )
144
+ p.add_argument("--start", help="inclusive start timestamp, e.g. 2023-06-01")
145
+ p.add_argument("--end", help="exclusive end timestamp")
146
+ p.add_argument(
147
+ "--recommended",
148
+ action="store_true",
149
+ help="export the profile recommended for CODE instead of CODE itself",
150
+ )
151
+ add_db(p)
152
+ p.set_defaults(func=_cmd_export)
153
+
154
+ p = sub.add_parser("dictionary", help="convert the SBI dictionary workbook to CSV")
155
+ p.add_argument("source", nargs="?", help=".xlsx workbook (default: bundled copy)")
156
+ p.add_argument("-o", "--output", required=True, help="output CSV file")
157
+ p.set_defaults(func=_cmd_dictionary)
158
+
159
+ return parser
160
+
161
+
162
+ def main(argv: Sequence[str] | None = None) -> int:
163
+ """Entry point of the ``liander-open-data`` command."""
164
+ parser = build_parser()
165
+ args = parser.parse_args(argv)
166
+ logging.basicConfig(
167
+ level=logging.DEBUG if args.verbose else logging.WARNING,
168
+ format="%(levelname)s: %(message)s",
169
+ )
170
+ try:
171
+ return args.func(args)
172
+ except (FileNotFoundError, FileExistsError, ProfileNotFoundError, ValueError) as exc:
173
+ print(f"error: {exc}", file=sys.stderr)
174
+ return 1
175
+
176
+
177
+ if __name__ == "__main__": # pragma: no cover
178
+ sys.exit(main())