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.
- liander_open_data/__init__.py +49 -0
- liander_open_data/__main__.py +5 -0
- liander_open_data/cli.py +178 -0
- liander_open_data/data/sbi_dictionary.csv +1428 -0
- liander_open_data/database.py +595 -0
- liander_open_data/dictionary.py +265 -0
- liander_open_data/py.typed +0 -0
- liander_open_data-0.1.0.dist-info/METADATA +122 -0
- liander_open_data-0.1.0.dist-info/RECORD +12 -0
- liander_open_data-0.1.0.dist-info/WHEEL +4 -0
- liander_open_data-0.1.0.dist-info/entry_points.txt +2 -0
- liander_open_data-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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
|
+
]
|
liander_open_data/cli.py
ADDED
|
@@ -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())
|