macrotrace 0.1.0rc1__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.
- macrotrace/__init__.py +39 -0
- macrotrace/_paths.py +37 -0
- macrotrace/cli.py +73 -0
- macrotrace/graphing.py +183 -0
- macrotrace/models/__init__.py +31 -0
- macrotrace/models/db.py +265 -0
- macrotrace/models/mt/__init__.py +8 -0
- macrotrace/models/mt/analysis.py +990 -0
- macrotrace/models/mt/observation.py +9 -0
- macrotrace/models/mt/plotter.py +818 -0
- macrotrace/models/mt/series_metadata.py +61 -0
- macrotrace/models/mt/time_series.py +1088 -0
- macrotrace/ons_cli/__init__.py +0 -0
- macrotrace/ons_cli/cli.py +1197 -0
- macrotrace/ons_cli/common.py +437 -0
- macrotrace/ons_cli/tui.py +1450 -0
- macrotrace/py.typed +0 -0
- macrotrace/sources/__init__.py +8 -0
- macrotrace/sources/base.py +936 -0
- macrotrace/sources/example.py +190 -0
- macrotrace/sources/fred.py +686 -0
- macrotrace/sources/ons.py +1135 -0
- macrotrace/sources/rtdsm.py +15 -0
- macrotrace-0.1.0rc1.dist-info/METADATA +175 -0
- macrotrace-0.1.0rc1.dist-info/RECORD +28 -0
- macrotrace-0.1.0rc1.dist-info/WHEEL +4 -0
- macrotrace-0.1.0rc1.dist-info/entry_points.txt +4 -0
- macrotrace-0.1.0rc1.dist-info/licenses/LICENSE +674 -0
macrotrace/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Macrotrace top-level package exports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
__version__ = version("macrotrace")
|
|
10
|
+
except PackageNotFoundError:
|
|
11
|
+
__version__ = "unknown"
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"MTTimeSeries",
|
|
15
|
+
"MTObservation",
|
|
16
|
+
"MTSeriesMetadata",
|
|
17
|
+
"MTTimeSeriesPlotter",
|
|
18
|
+
"VintageComparison",
|
|
19
|
+
"__version__",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
_LAZY_IMPORTS = {
|
|
23
|
+
"MTTimeSeries": ("macrotrace.models.mt.time_series", "MTTimeSeries"),
|
|
24
|
+
"MTObservation": ("macrotrace.models.mt.observation", "MTObservation"),
|
|
25
|
+
"MTSeriesMetadata": ("macrotrace.models.mt.series_metadata", "MTSeriesMetadata"),
|
|
26
|
+
"MTTimeSeriesPlotter": ("macrotrace.models.mt.plotter", "MTTimeSeriesPlotter"),
|
|
27
|
+
"VintageComparison": ("macrotrace.models.mt.analysis", "VintageComparison"),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def __getattr__(name: str) -> Any:
|
|
32
|
+
"""Lazily expose heavy model imports at package level."""
|
|
33
|
+
target = _LAZY_IMPORTS.get(name)
|
|
34
|
+
if target is None:
|
|
35
|
+
raise AttributeError(f"module 'macrotrace' has no attribute {name!r}")
|
|
36
|
+
module_name, attr = target
|
|
37
|
+
from importlib import import_module
|
|
38
|
+
|
|
39
|
+
return getattr(import_module(module_name), attr)
|
macrotrace/_paths.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Path resolution for macrotrace's SQLite files.
|
|
2
|
+
|
|
3
|
+
Resolution order for both files: explicit argument, then environment
|
|
4
|
+
variable, then default in the current working directory. If
|
|
5
|
+
``MACROTRACE_DB`` is set but ``MACROTRACE_CACHE`` is not, the cache
|
|
6
|
+
defaults to sitting next to the database file.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
DB_ENV_VAR = "MACROTRACE_DB"
|
|
16
|
+
CACHE_ENV_VAR = "MACROTRACE_CACHE"
|
|
17
|
+
|
|
18
|
+
DEFAULT_DB_NAME = "MacroTrace.db"
|
|
19
|
+
DEFAULT_CACHE_NAME = "MacroTraceRequestCache.sqlite"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def resolve_db_path(arg: Optional[str] = None) -> str:
|
|
23
|
+
if arg is not None:
|
|
24
|
+
return arg
|
|
25
|
+
return os.environ.get(DB_ENV_VAR) or DEFAULT_DB_NAME
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def resolve_cache_path(arg: Optional[str] = None) -> str:
|
|
29
|
+
if arg is not None:
|
|
30
|
+
return arg
|
|
31
|
+
env_value = os.environ.get(CACHE_ENV_VAR)
|
|
32
|
+
if env_value:
|
|
33
|
+
return env_value
|
|
34
|
+
db_env = os.environ.get(DB_ENV_VAR)
|
|
35
|
+
if db_env:
|
|
36
|
+
return str(Path(db_env).resolve().parent / DEFAULT_CACHE_NAME)
|
|
37
|
+
return DEFAULT_CACHE_NAME
|
macrotrace/cli.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Top-level Macrotrace command-line interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _run_ons_explorer(args: argparse.Namespace) -> int:
|
|
11
|
+
from macrotrace.ons_cli.cli import main as ons_main
|
|
12
|
+
|
|
13
|
+
return ons_main(args.args)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _run_ons_tui(args: argparse.Namespace) -> int:
|
|
17
|
+
from macrotrace.ons_cli.tui import main as ons_tui_main
|
|
18
|
+
|
|
19
|
+
return ons_tui_main(args.args)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
23
|
+
parser = argparse.ArgumentParser(
|
|
24
|
+
prog="macrotrace",
|
|
25
|
+
description="Macrotrace command-line tools.",
|
|
26
|
+
)
|
|
27
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
28
|
+
|
|
29
|
+
ons_parser = subparsers.add_parser(
|
|
30
|
+
"ons",
|
|
31
|
+
help="ONS explorer commands.",
|
|
32
|
+
description="ONS explorer commands.",
|
|
33
|
+
)
|
|
34
|
+
ons_subparsers = ons_parser.add_subparsers(dest="ons_command", required=True)
|
|
35
|
+
|
|
36
|
+
ons_subparsers.add_parser(
|
|
37
|
+
"explorer",
|
|
38
|
+
help="Run the interactive ONS explorer CLI.",
|
|
39
|
+
description="Run the interactive ONS explorer CLI.",
|
|
40
|
+
)
|
|
41
|
+
ons_subparsers.add_parser(
|
|
42
|
+
"tui",
|
|
43
|
+
help="Run the Textual ONS explorer TUI.",
|
|
44
|
+
description="Run the Textual ONS explorer TUI.",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return parser
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
51
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
52
|
+
parser = build_parser()
|
|
53
|
+
|
|
54
|
+
if not argv:
|
|
55
|
+
parser.parse_args(argv)
|
|
56
|
+
|
|
57
|
+
if argv == ["--help"] or argv == ["-h"]:
|
|
58
|
+
parser.parse_args(argv)
|
|
59
|
+
|
|
60
|
+
if argv[0] != "ons":
|
|
61
|
+
parser.parse_args(argv)
|
|
62
|
+
|
|
63
|
+
if len(argv) == 1 or argv[1] in {"-h", "--help"}:
|
|
64
|
+
parser.parse_args(argv)
|
|
65
|
+
|
|
66
|
+
if argv[1] == "explorer":
|
|
67
|
+
return _run_ons_explorer(argparse.Namespace(args=argv[2:]))
|
|
68
|
+
|
|
69
|
+
if argv[1] == "tui":
|
|
70
|
+
return _run_ons_tui(argparse.Namespace(args=argv[2:]))
|
|
71
|
+
|
|
72
|
+
parser.parse_args(argv)
|
|
73
|
+
return 2
|
macrotrace/graphing.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
MACROTRACE_COLORWAY = [
|
|
2
|
+
"#4c72b0",
|
|
3
|
+
"#dd8452",
|
|
4
|
+
"#55a868",
|
|
5
|
+
"#c44e52",
|
|
6
|
+
"#8172b3",
|
|
7
|
+
"#937860",
|
|
8
|
+
"#da8bc3",
|
|
9
|
+
"#8c8c8c",
|
|
10
|
+
"#ccb974",
|
|
11
|
+
"#64b5cd",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
MACROTRACE_DIVERGING_COLORSCALE = [
|
|
15
|
+
[0.0, MACROTRACE_COLORWAY[0]],
|
|
16
|
+
[0.5, "#ffffff"],
|
|
17
|
+
[1.0, MACROTRACE_COLORWAY[1]],
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
MACROTRACE_PLOTLY_LAYOUT_TEMPLATE = {
|
|
21
|
+
"layout": {
|
|
22
|
+
"annotationdefaults": {"arrowcolor": "#4368a7"},
|
|
23
|
+
"autotypenumbers": "strict",
|
|
24
|
+
"coloraxis": {
|
|
25
|
+
"colorbar": {
|
|
26
|
+
"outlinewidth": 0,
|
|
27
|
+
"tickcolor": "#242424",
|
|
28
|
+
"ticklen": 8,
|
|
29
|
+
"ticks": "outside",
|
|
30
|
+
"tickwidth": 2,
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"colorscale": {
|
|
34
|
+
"diverging": MACROTRACE_DIVERGING_COLORSCALE,
|
|
35
|
+
"sequential": [
|
|
36
|
+
[0.0, "#020419"],
|
|
37
|
+
[0.06274509803921569, "#180f29"],
|
|
38
|
+
[0.12549019607843137, "#2f1739"],
|
|
39
|
+
[0.18823529411764706, "#471c48"],
|
|
40
|
+
[0.25098039215686274, "#611e52"],
|
|
41
|
+
[0.3137254901960784, "#7b1e59"],
|
|
42
|
+
[0.3764705882352941, "#961b5b"],
|
|
43
|
+
[0.4392156862745098, "#b11658"],
|
|
44
|
+
[0.5019607843137255, "#cb1a4f"],
|
|
45
|
+
[0.5647058823529412, "#df2f43"],
|
|
46
|
+
[0.6274509803921569, "#ec4c3d"],
|
|
47
|
+
[0.6901960784313725, "#f26b49"],
|
|
48
|
+
[0.7529411764705882, "#f4875f"],
|
|
49
|
+
[0.8156862745098039, "#f5a27a"],
|
|
50
|
+
[0.8784313725490196, "#f6bc99"],
|
|
51
|
+
[0.9411764705882353, "#f7d4bb"],
|
|
52
|
+
[1.0, "#faeadc"],
|
|
53
|
+
],
|
|
54
|
+
"sequentialminus": [
|
|
55
|
+
[0.0, "#020419"],
|
|
56
|
+
[0.06274509803921569, "#180f29"],
|
|
57
|
+
[0.12549019607843137, "#2f1739"],
|
|
58
|
+
[0.18823529411764706, "#471c48"],
|
|
59
|
+
[0.25098039215686274, "#611e52"],
|
|
60
|
+
[0.3137254901960784, "#7b1e59"],
|
|
61
|
+
[0.3764705882352941, "#961b5b"],
|
|
62
|
+
[0.4392156862745098, "#b11658"],
|
|
63
|
+
[0.5019607843137255, "#cb1a4f"],
|
|
64
|
+
[0.5647058823529412, "#df2f43"],
|
|
65
|
+
[0.6274509803921569, "#ec4c3d"],
|
|
66
|
+
[0.6901960784313725, "#f26b49"],
|
|
67
|
+
[0.7529411764705882, "#f4875f"],
|
|
68
|
+
[0.8156862745098039, "#f5a27a"],
|
|
69
|
+
[0.8784313725490196, "#f6bc99"],
|
|
70
|
+
[0.9411764705882353, "#f7d4bb"],
|
|
71
|
+
[1.0, "#faeadc"],
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
"colorway": MACROTRACE_COLORWAY,
|
|
75
|
+
"font": {
|
|
76
|
+
"color": "#242424",
|
|
77
|
+
"family": "CMU Serif, Old Standard TT, Georgia, Times New Roman, Times, serif",
|
|
78
|
+
},
|
|
79
|
+
"geo": {
|
|
80
|
+
"bgcolor": "white",
|
|
81
|
+
"lakecolor": "white",
|
|
82
|
+
"landcolor": "#d3efde",
|
|
83
|
+
"showlakes": True,
|
|
84
|
+
"showland": True,
|
|
85
|
+
"subunitcolor": "white",
|
|
86
|
+
},
|
|
87
|
+
"hoverlabel": {"align": "left"},
|
|
88
|
+
"hovermode": "closest",
|
|
89
|
+
"paper_bgcolor": "white",
|
|
90
|
+
"plot_bgcolor": "#eaeaf2",
|
|
91
|
+
"polar": {
|
|
92
|
+
"angularaxis": {
|
|
93
|
+
"gridcolor": "white",
|
|
94
|
+
"linecolor": "white",
|
|
95
|
+
"showgrid": True,
|
|
96
|
+
"ticks": "",
|
|
97
|
+
},
|
|
98
|
+
"bgcolor": "#eaeaf2",
|
|
99
|
+
"radialaxis": {
|
|
100
|
+
"gridcolor": "white",
|
|
101
|
+
"linecolor": "white",
|
|
102
|
+
"showgrid": True,
|
|
103
|
+
"ticks": "",
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
"scene": {
|
|
107
|
+
"xaxis": {
|
|
108
|
+
"backgroundcolor": "#eaeaf2",
|
|
109
|
+
"gridcolor": "white",
|
|
110
|
+
"gridwidth": 2,
|
|
111
|
+
"linecolor": "white",
|
|
112
|
+
"showbackground": True,
|
|
113
|
+
"showgrid": True,
|
|
114
|
+
"ticks": "",
|
|
115
|
+
"zerolinecolor": "white",
|
|
116
|
+
},
|
|
117
|
+
"yaxis": {
|
|
118
|
+
"backgroundcolor": "#eaeaf2",
|
|
119
|
+
"gridcolor": "white",
|
|
120
|
+
"gridwidth": 2,
|
|
121
|
+
"linecolor": "white",
|
|
122
|
+
"showbackground": True,
|
|
123
|
+
"showgrid": True,
|
|
124
|
+
"ticks": "",
|
|
125
|
+
"zerolinecolor": "white",
|
|
126
|
+
},
|
|
127
|
+
"zaxis": {
|
|
128
|
+
"backgroundcolor": "#eaeaf2",
|
|
129
|
+
"gridcolor": "white",
|
|
130
|
+
"gridwidth": 2,
|
|
131
|
+
"linecolor": "white",
|
|
132
|
+
"showbackground": True,
|
|
133
|
+
"showgrid": True,
|
|
134
|
+
"ticks": "",
|
|
135
|
+
"zerolinecolor": "white",
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
"shapedefaults": {
|
|
139
|
+
"fillcolor": "#4367a7",
|
|
140
|
+
"line": {"width": 3},
|
|
141
|
+
"opacity": 0.5,
|
|
142
|
+
},
|
|
143
|
+
"ternary": {
|
|
144
|
+
"aaxis": {
|
|
145
|
+
"gridcolor": "white",
|
|
146
|
+
"linecolor": "white",
|
|
147
|
+
"showgrid": True,
|
|
148
|
+
"ticks": "",
|
|
149
|
+
},
|
|
150
|
+
"baxis": {
|
|
151
|
+
"gridcolor": "white",
|
|
152
|
+
"linecolor": "white",
|
|
153
|
+
"showgrid": True,
|
|
154
|
+
"ticks": "",
|
|
155
|
+
},
|
|
156
|
+
"bgcolor": "#eaeaf2",
|
|
157
|
+
"caxis": {
|
|
158
|
+
"gridcolor": "white",
|
|
159
|
+
"linecolor": "white",
|
|
160
|
+
"showgrid": True,
|
|
161
|
+
"ticks": "",
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
"xaxis": {
|
|
165
|
+
"automargin": True,
|
|
166
|
+
"gridcolor": "white",
|
|
167
|
+
"linecolor": "white",
|
|
168
|
+
"showgrid": True,
|
|
169
|
+
"ticks": "",
|
|
170
|
+
"title": {"standoff": 15},
|
|
171
|
+
"zerolinecolor": "white",
|
|
172
|
+
},
|
|
173
|
+
"yaxis": {
|
|
174
|
+
"automargin": True,
|
|
175
|
+
"gridcolor": "white",
|
|
176
|
+
"linecolor": "white",
|
|
177
|
+
"showgrid": True,
|
|
178
|
+
"ticks": "",
|
|
179
|
+
"title": {"standoff": 15},
|
|
180
|
+
"zerolinecolor": "white",
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from macrotrace.models.mt.observation import MTObservation
|
|
2
|
+
from macrotrace.models.mt.series_metadata import MTSeriesMetadata
|
|
3
|
+
from macrotrace.models.mt.time_series import MTTimeSeries
|
|
4
|
+
from macrotrace.models.mt.analysis import VintageComparison
|
|
5
|
+
from macrotrace.models.mt.plotter import MTTimeSeriesPlotter
|
|
6
|
+
from macrotrace.models.db import (
|
|
7
|
+
LOCAL_DATABASE,
|
|
8
|
+
Dataset,
|
|
9
|
+
DatasetDimension,
|
|
10
|
+
Release,
|
|
11
|
+
ReleaseDimension,
|
|
12
|
+
Series,
|
|
13
|
+
SeriesDimensionFilter,
|
|
14
|
+
Observation,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"MTObservation",
|
|
19
|
+
"MTSeriesMetadata",
|
|
20
|
+
"MTTimeSeries",
|
|
21
|
+
"VintageComparison",
|
|
22
|
+
"MTTimeSeriesPlotter",
|
|
23
|
+
"LOCAL_DATABASE",
|
|
24
|
+
"Dataset",
|
|
25
|
+
"DatasetDimension",
|
|
26
|
+
"Release",
|
|
27
|
+
"ReleaseDimension",
|
|
28
|
+
"Series",
|
|
29
|
+
"SeriesDimensionFilter",
|
|
30
|
+
"Observation",
|
|
31
|
+
]
|
macrotrace/models/db.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from peewee import (
|
|
3
|
+
SQL,
|
|
4
|
+
DateTimeField,
|
|
5
|
+
FloatField,
|
|
6
|
+
ForeignKeyField,
|
|
7
|
+
Model,
|
|
8
|
+
SqliteDatabase,
|
|
9
|
+
TextField,
|
|
10
|
+
)
|
|
11
|
+
from playhouse.sqlite_ext import JSONField
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
LOCAL_DATABASE = SqliteDatabase(
|
|
15
|
+
None,
|
|
16
|
+
pragmas=(
|
|
17
|
+
("cache_size", -1024 * 32), # 32MB page-cache.
|
|
18
|
+
("journal_mode", "wal"),
|
|
19
|
+
("foreign_keys", 1),
|
|
20
|
+
),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_valid_dateoffset(value: str) -> bool:
|
|
25
|
+
"""
|
|
26
|
+
Checks if a string is a valid pandas date offset.
|
|
27
|
+
We are using this to ensure that frequency fields conform to pandas offset strings.
|
|
28
|
+
Certain export and processing functions depend on this format such as to_darts_timeseries.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
value (str): The string to check.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
bool: True if the string is a valid pandas date offset, False otherwise.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
pd.tseries.frequencies.to_offset(value)
|
|
38
|
+
return True
|
|
39
|
+
except (ValueError, TypeError):
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class StrictDateTimeField(DateTimeField):
|
|
44
|
+
"""DateTimeField that enforces timezone-aware datetime objects in ISO 8601 format."""
|
|
45
|
+
|
|
46
|
+
def db_value(self, value):
|
|
47
|
+
if value is None:
|
|
48
|
+
return None
|
|
49
|
+
if not isinstance(value, datetime.datetime):
|
|
50
|
+
raise ValueError(f"Value must be a datetime object, got {type(value)}")
|
|
51
|
+
if value.tzinfo is None:
|
|
52
|
+
raise ValueError("Datetime must be timezone-aware (include tzinfo).")
|
|
53
|
+
return value.isoformat()
|
|
54
|
+
|
|
55
|
+
def python_value(self, value):
|
|
56
|
+
if value is None:
|
|
57
|
+
return None
|
|
58
|
+
if isinstance(value, datetime.datetime):
|
|
59
|
+
return value
|
|
60
|
+
try:
|
|
61
|
+
# Parse ISO 8601 format
|
|
62
|
+
parsed = datetime.datetime.fromisoformat(value)
|
|
63
|
+
if parsed.tzinfo is None:
|
|
64
|
+
raise ValueError("Parsed datetime is missing timezone information.")
|
|
65
|
+
return parsed
|
|
66
|
+
except (ValueError, AttributeError) as e:
|
|
67
|
+
raise ValueError(
|
|
68
|
+
f"Invalid datetime format. Expected ISO 8601 with timezone: {e}"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class FrequencyField(TextField):
|
|
73
|
+
def db_value(self, value):
|
|
74
|
+
if value is not None and not is_valid_dateoffset(value):
|
|
75
|
+
raise ValueError(f"Invalid pandas frequency offset: {value}")
|
|
76
|
+
return super().db_value(value)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class DataBaseModel(Model):
|
|
80
|
+
class Meta:
|
|
81
|
+
database = LOCAL_DATABASE
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Dataset(DataBaseModel):
|
|
85
|
+
"""Identity of a dataset; versioning lives in DatasetVersion."""
|
|
86
|
+
|
|
87
|
+
source = TextField()
|
|
88
|
+
dataset_id = TextField()
|
|
89
|
+
|
|
90
|
+
class Meta:
|
|
91
|
+
constraints = [SQL("UNIQUE(source, dataset_id)")]
|
|
92
|
+
|
|
93
|
+
def __repr__(self):
|
|
94
|
+
return f"Dataset(source={self.source}, dataset_id={self.dataset_id})"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class DatasetDimension(DataBaseModel):
|
|
98
|
+
"""
|
|
99
|
+
Identity of a dataset dimension.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
dataset = ForeignKeyField(
|
|
103
|
+
Dataset,
|
|
104
|
+
backref="dimensions",
|
|
105
|
+
on_delete="CASCADE",
|
|
106
|
+
)
|
|
107
|
+
# This is the ID used by the source to identify the dimension
|
|
108
|
+
# This should NOT change as the dimension is versioned via valid_from and valid_to
|
|
109
|
+
dataset_dimension_id = TextField()
|
|
110
|
+
title = TextField()
|
|
111
|
+
type = TextField(
|
|
112
|
+
choices=[
|
|
113
|
+
("text", "text"),
|
|
114
|
+
("numeric", "numeric"),
|
|
115
|
+
("boolean", "boolean"),
|
|
116
|
+
]
|
|
117
|
+
)
|
|
118
|
+
frequency = FrequencyField(null=True)
|
|
119
|
+
description = TextField(null=True)
|
|
120
|
+
units = TextField(null=True)
|
|
121
|
+
seasonal_adjustment = TextField(null=True)
|
|
122
|
+
# Validity period for this dimension definition, null valid_to means currently valid
|
|
123
|
+
valid_from = StrictDateTimeField()
|
|
124
|
+
valid_to = StrictDateTimeField(null=True)
|
|
125
|
+
created_at = StrictDateTimeField(
|
|
126
|
+
default=datetime.datetime.now(tz=datetime.timezone.utc)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
class Meta:
|
|
130
|
+
constraints = [
|
|
131
|
+
SQL("UNIQUE(dataset_id, dataset_dimension_id, valid_from)"),
|
|
132
|
+
SQL("CHECK(type IN ('text', 'numeric', 'boolean'))"),
|
|
133
|
+
SQL("CHECK(valid_to IS NULL OR valid_to > valid_from)"),
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
def __repr__(self):
|
|
137
|
+
return (
|
|
138
|
+
f"DatasetDimension(dataset={self.dataset.dataset_id}, title={self.title})"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class Release(DataBaseModel):
|
|
143
|
+
"""
|
|
144
|
+
A release of data in a dataset at a point in time.
|
|
145
|
+
It is not versioned itself as releases are theoretically immutable once created.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
dataset = ForeignKeyField(
|
|
149
|
+
Dataset,
|
|
150
|
+
backref="releases",
|
|
151
|
+
on_delete="CASCADE",
|
|
152
|
+
)
|
|
153
|
+
release_date = StrictDateTimeField()
|
|
154
|
+
additional_metadata = JSONField(null=True)
|
|
155
|
+
created_at = StrictDateTimeField(
|
|
156
|
+
default=datetime.datetime.now(tz=datetime.timezone.utc)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
class Meta:
|
|
160
|
+
constraints = [SQL("UNIQUE(dataset_id, release_date)")]
|
|
161
|
+
|
|
162
|
+
def __repr__(self):
|
|
163
|
+
return (
|
|
164
|
+
f"Release(dataset={self.dataset.dataset_id}, "
|
|
165
|
+
f"release_date={self.release_date})"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class ReleaseDimension(DataBaseModel):
|
|
170
|
+
"""
|
|
171
|
+
Association table for many-to-many relationship between Release and DatasetDimension.
|
|
172
|
+
Tracks which dimensions are included in each release.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
release = ForeignKeyField(
|
|
176
|
+
Release,
|
|
177
|
+
backref="release_dimensions",
|
|
178
|
+
on_delete="CASCADE",
|
|
179
|
+
)
|
|
180
|
+
dimension = ForeignKeyField(
|
|
181
|
+
DatasetDimension,
|
|
182
|
+
backref="release_dimensions",
|
|
183
|
+
on_delete="CASCADE",
|
|
184
|
+
)
|
|
185
|
+
created_at = StrictDateTimeField(
|
|
186
|
+
default=datetime.datetime.now(tz=datetime.timezone.utc)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
class Meta:
|
|
190
|
+
constraints = [SQL("UNIQUE(release_id, dimension_id)")]
|
|
191
|
+
|
|
192
|
+
def __repr__(self):
|
|
193
|
+
return (
|
|
194
|
+
f"ReleaseDimension(release={self.release.release_date}, "
|
|
195
|
+
f"dimension={self.dimension.title})"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class Series(DataBaseModel):
|
|
200
|
+
dataset = ForeignKeyField(Dataset, backref="series", on_delete="CASCADE")
|
|
201
|
+
series_key = JSONField()
|
|
202
|
+
created_at = StrictDateTimeField(
|
|
203
|
+
default=datetime.datetime.now(tz=datetime.timezone.utc)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def __repr__(self):
|
|
207
|
+
return (
|
|
208
|
+
f"Series(dataset = {self.dataset.dataset_id}, series_key={self.series_key})"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class SeriesDimensionFilter(DataBaseModel):
|
|
213
|
+
series = ForeignKeyField(
|
|
214
|
+
Series,
|
|
215
|
+
backref="dimension_selections",
|
|
216
|
+
on_delete="CASCADE",
|
|
217
|
+
)
|
|
218
|
+
dimension = ForeignKeyField(
|
|
219
|
+
DatasetDimension,
|
|
220
|
+
on_delete="CASCADE",
|
|
221
|
+
)
|
|
222
|
+
# For numeric or boolean dimensions
|
|
223
|
+
# Store as text, cast as needed based on dimension.type
|
|
224
|
+
value = TextField()
|
|
225
|
+
|
|
226
|
+
class Meta:
|
|
227
|
+
constraints = [
|
|
228
|
+
SQL("UNIQUE(series_id, dimension_id, value)"),
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
def __repr__(self):
|
|
232
|
+
return (
|
|
233
|
+
f"SeriesDimensionFilter(series={self.series.series_key}, "
|
|
234
|
+
f"dimension={self.dimension.title}, value={self.value})"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class Observation(DataBaseModel):
|
|
239
|
+
series = ForeignKeyField(
|
|
240
|
+
Series,
|
|
241
|
+
backref="observations",
|
|
242
|
+
on_delete="CASCADE",
|
|
243
|
+
)
|
|
244
|
+
release = ForeignKeyField(
|
|
245
|
+
Release,
|
|
246
|
+
backref="observations",
|
|
247
|
+
on_delete="CASCADE",
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
observation_timestamp = StrictDateTimeField()
|
|
251
|
+
value = FloatField(null=True) # null if the observation is missing
|
|
252
|
+
created_at = StrictDateTimeField(
|
|
253
|
+
default=datetime.datetime.now(tz=datetime.timezone.utc)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
class Meta:
|
|
257
|
+
constraints = [SQL("UNIQUE(release_id, observation_timestamp)")]
|
|
258
|
+
|
|
259
|
+
def __repr__(self):
|
|
260
|
+
return (
|
|
261
|
+
f"Observation(series={self.series.series_key}, "
|
|
262
|
+
f"release={self.release.release_date}, "
|
|
263
|
+
f"observation_timestamp={self.observation_timestamp}, "
|
|
264
|
+
f"value={self.value})"
|
|
265
|
+
)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""MacroTrace models for time series data."""
|
|
2
|
+
|
|
3
|
+
from macrotrace.models.mt.time_series import MTTimeSeries
|
|
4
|
+
from macrotrace.models.mt.analysis import VintageComparison
|
|
5
|
+
from macrotrace.models.mt.series_metadata import MTSeriesMetadata
|
|
6
|
+
from macrotrace.models.mt.observation import MTObservation
|
|
7
|
+
|
|
8
|
+
__all__ = ["MTTimeSeries", "VintageComparison", "MTSeriesMetadata", "MTObservation"]
|