dsgrid-toolkit 0.3.3__cp313-cp313-win_amd64.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.
- build_backend.py +93 -0
- dsgrid/__init__.py +22 -0
- dsgrid/api/__init__.py +0 -0
- dsgrid/api/api_manager.py +179 -0
- dsgrid/api/app.py +419 -0
- dsgrid/api/models.py +60 -0
- dsgrid/api/response_models.py +116 -0
- dsgrid/apps/__init__.py +0 -0
- dsgrid/apps/project_viewer/app.py +216 -0
- dsgrid/apps/registration_gui.py +444 -0
- dsgrid/chronify.py +32 -0
- dsgrid/cli/__init__.py +0 -0
- dsgrid/cli/common.py +120 -0
- dsgrid/cli/config.py +176 -0
- dsgrid/cli/download.py +13 -0
- dsgrid/cli/dsgrid.py +157 -0
- dsgrid/cli/dsgrid_admin.py +92 -0
- dsgrid/cli/install_notebooks.py +62 -0
- dsgrid/cli/query.py +729 -0
- dsgrid/cli/registry.py +1862 -0
- dsgrid/cloud/__init__.py +0 -0
- dsgrid/cloud/cloud_storage_interface.py +140 -0
- dsgrid/cloud/factory.py +31 -0
- dsgrid/cloud/fake_storage_interface.py +37 -0
- dsgrid/cloud/s3_storage_interface.py +156 -0
- dsgrid/common.py +36 -0
- dsgrid/config/__init__.py +0 -0
- dsgrid/config/annual_time_dimension_config.py +194 -0
- dsgrid/config/common.py +142 -0
- dsgrid/config/config_base.py +148 -0
- dsgrid/config/dataset_config.py +907 -0
- dsgrid/config/dataset_schema_handler_factory.py +46 -0
- dsgrid/config/date_time_dimension_config.py +136 -0
- dsgrid/config/dimension_config.py +54 -0
- dsgrid/config/dimension_config_factory.py +65 -0
- dsgrid/config/dimension_mapping_base.py +350 -0
- dsgrid/config/dimension_mappings_config.py +48 -0
- dsgrid/config/dimensions.py +1025 -0
- dsgrid/config/dimensions_config.py +71 -0
- dsgrid/config/file_schema.py +190 -0
- dsgrid/config/index_time_dimension_config.py +80 -0
- dsgrid/config/input_dataset_requirements.py +31 -0
- dsgrid/config/mapping_tables.py +209 -0
- dsgrid/config/noop_time_dimension_config.py +42 -0
- dsgrid/config/project_config.py +1462 -0
- dsgrid/config/registration_models.py +188 -0
- dsgrid/config/representative_period_time_dimension_config.py +194 -0
- dsgrid/config/simple_models.py +49 -0
- dsgrid/config/supplemental_dimension.py +29 -0
- dsgrid/config/time_dimension_base_config.py +192 -0
- dsgrid/data_models.py +155 -0
- dsgrid/dataset/__init__.py +0 -0
- dsgrid/dataset/dataset.py +123 -0
- dsgrid/dataset/dataset_expression_handler.py +86 -0
- dsgrid/dataset/dataset_mapping_manager.py +121 -0
- dsgrid/dataset/dataset_schema_handler_base.py +945 -0
- dsgrid/dataset/dataset_schema_handler_one_table.py +209 -0
- dsgrid/dataset/dataset_schema_handler_two_table.py +322 -0
- dsgrid/dataset/growth_rates.py +162 -0
- dsgrid/dataset/models.py +51 -0
- dsgrid/dataset/table_format_handler_base.py +257 -0
- dsgrid/dataset/table_format_handler_factory.py +17 -0
- dsgrid/dataset/unpivoted_table.py +121 -0
- dsgrid/dimension/__init__.py +0 -0
- dsgrid/dimension/base_models.py +230 -0
- dsgrid/dimension/dimension_filters.py +308 -0
- dsgrid/dimension/standard.py +252 -0
- dsgrid/dimension/time.py +352 -0
- dsgrid/dimension/time_utils.py +103 -0
- dsgrid/dsgrid_rc.py +88 -0
- dsgrid/exceptions.py +105 -0
- dsgrid/filesystem/__init__.py +0 -0
- dsgrid/filesystem/cloud_filesystem.py +32 -0
- dsgrid/filesystem/factory.py +32 -0
- dsgrid/filesystem/filesystem_interface.py +136 -0
- dsgrid/filesystem/local_filesystem.py +74 -0
- dsgrid/filesystem/s3_filesystem.py +118 -0
- dsgrid/loggers.py +132 -0
- dsgrid/minimal_patterns.cp313-win_amd64.pyd +0 -0
- dsgrid/notebooks/connect_to_dsgrid_registry.ipynb +949 -0
- dsgrid/notebooks/registration.ipynb +48 -0
- dsgrid/notebooks/start_notebook.sh +11 -0
- dsgrid/project.py +451 -0
- dsgrid/query/__init__.py +0 -0
- dsgrid/query/dataset_mapping_plan.py +142 -0
- dsgrid/query/derived_dataset.py +388 -0
- dsgrid/query/models.py +728 -0
- dsgrid/query/query_context.py +287 -0
- dsgrid/query/query_submitter.py +994 -0
- dsgrid/query/report_factory.py +19 -0
- dsgrid/query/report_peak_load.py +70 -0
- dsgrid/query/reports_base.py +20 -0
- dsgrid/registry/__init__.py +0 -0
- dsgrid/registry/bulk_register.py +165 -0
- dsgrid/registry/common.py +287 -0
- dsgrid/registry/config_update_checker_base.py +63 -0
- dsgrid/registry/data_store_factory.py +34 -0
- dsgrid/registry/data_store_interface.py +74 -0
- dsgrid/registry/dataset_config_generator.py +158 -0
- dsgrid/registry/dataset_registry_manager.py +950 -0
- dsgrid/registry/dataset_update_checker.py +16 -0
- dsgrid/registry/dimension_mapping_registry_manager.py +575 -0
- dsgrid/registry/dimension_mapping_update_checker.py +16 -0
- dsgrid/registry/dimension_registry_manager.py +413 -0
- dsgrid/registry/dimension_update_checker.py +16 -0
- dsgrid/registry/duckdb_data_store.py +207 -0
- dsgrid/registry/filesystem_data_store.py +150 -0
- dsgrid/registry/filter_registry_manager.py +123 -0
- dsgrid/registry/project_config_generator.py +57 -0
- dsgrid/registry/project_registry_manager.py +1623 -0
- dsgrid/registry/project_update_checker.py +48 -0
- dsgrid/registry/registration_context.py +223 -0
- dsgrid/registry/registry_auto_updater.py +316 -0
- dsgrid/registry/registry_database.py +667 -0
- dsgrid/registry/registry_interface.py +446 -0
- dsgrid/registry/registry_manager.py +558 -0
- dsgrid/registry/registry_manager_base.py +367 -0
- dsgrid/registry/versioning.py +92 -0
- dsgrid/rust_ext/__init__.py +14 -0
- dsgrid/rust_ext/find_minimal_patterns.py +129 -0
- dsgrid/spark/__init__.py +0 -0
- dsgrid/spark/functions.py +589 -0
- dsgrid/spark/types.py +110 -0
- dsgrid/tests/__init__.py +0 -0
- dsgrid/tests/common.py +140 -0
- dsgrid/tests/make_us_data_registry.py +265 -0
- dsgrid/tests/register_derived_datasets.py +103 -0
- dsgrid/tests/utils.py +25 -0
- dsgrid/time/__init__.py +0 -0
- dsgrid/time/time_conversions.py +80 -0
- dsgrid/time/types.py +67 -0
- dsgrid/units/__init__.py +0 -0
- dsgrid/units/constants.py +113 -0
- dsgrid/units/convert.py +71 -0
- dsgrid/units/energy.py +145 -0
- dsgrid/units/power.py +87 -0
- dsgrid/utils/__init__.py +0 -0
- dsgrid/utils/dataset.py +830 -0
- dsgrid/utils/files.py +179 -0
- dsgrid/utils/filters.py +125 -0
- dsgrid/utils/id_remappings.py +100 -0
- dsgrid/utils/py_expression_eval/LICENSE +19 -0
- dsgrid/utils/py_expression_eval/README.md +8 -0
- dsgrid/utils/py_expression_eval/__init__.py +847 -0
- dsgrid/utils/py_expression_eval/tests.py +283 -0
- dsgrid/utils/run_command.py +70 -0
- dsgrid/utils/scratch_dir_context.py +65 -0
- dsgrid/utils/spark.py +918 -0
- dsgrid/utils/spark_partition.py +98 -0
- dsgrid/utils/timing.py +239 -0
- dsgrid/utils/utilities.py +221 -0
- dsgrid/utils/versioning.py +36 -0
- dsgrid_toolkit-0.3.3.dist-info/METADATA +193 -0
- dsgrid_toolkit-0.3.3.dist-info/RECORD +157 -0
- dsgrid_toolkit-0.3.3.dist-info/WHEEL +4 -0
- dsgrid_toolkit-0.3.3.dist-info/entry_points.txt +4 -0
- dsgrid_toolkit-0.3.3.dist-info/licenses/LICENSE +29 -0
build_backend.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Custom build backend that wraps maturin but allows fallback when Rust is unavailable.
|
|
2
|
+
|
|
3
|
+
This allows `pip install -e .` to succeed even without a Rust toolchain installed,
|
|
4
|
+
while still building the Rust extension when building wheels for distribution.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
import warnings
|
|
10
|
+
|
|
11
|
+
# Check if Rust toolchain is available
|
|
12
|
+
RUST_AVAILABLE = shutil.which("cargo") is not None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _warn_no_rust():
|
|
16
|
+
"""Warn that Rust extension will not be built."""
|
|
17
|
+
msg = (
|
|
18
|
+
"Rust toolchain (cargo) not found. The dsgrid Rust extension (minimal_patterns) "
|
|
19
|
+
"will not be built. Some functionality will be unavailable. "
|
|
20
|
+
"To build the Rust extension, install Rust from https://rustup.rs/"
|
|
21
|
+
)
|
|
22
|
+
# Use warnings module - this is typically shown by pip
|
|
23
|
+
warnings.warn(msg, UserWarning, stacklevel=2)
|
|
24
|
+
# Also print directly to stderr for visibility
|
|
25
|
+
print(f"\n{'='*70}", file=sys.stderr)
|
|
26
|
+
print("WARNING: Rust toolchain not found.", file=sys.stderr)
|
|
27
|
+
print("Building dsgrid without Rust extension (minimal_patterns).", file=sys.stderr)
|
|
28
|
+
print("Some functionality will be unavailable.", file=sys.stderr)
|
|
29
|
+
print(f"{'='*70}\n", file=sys.stderr)
|
|
30
|
+
# Flush to ensure it appears
|
|
31
|
+
sys.stderr.flush()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# For wheel builds, use maturin if Rust is available, otherwise fall back to setuptools
|
|
35
|
+
def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
|
|
36
|
+
"""Build wheel - uses Rust if available, otherwise falls back to setuptools."""
|
|
37
|
+
if RUST_AVAILABLE:
|
|
38
|
+
import maturin
|
|
39
|
+
|
|
40
|
+
return maturin.build_wheel(wheel_directory, config_settings, metadata_directory)
|
|
41
|
+
else:
|
|
42
|
+
_warn_no_rust()
|
|
43
|
+
# Fall back to setuptools for wheel build without Rust extension
|
|
44
|
+
import setuptools.build_meta
|
|
45
|
+
|
|
46
|
+
return setuptools.build_meta.build_wheel(
|
|
47
|
+
wheel_directory, config_settings, metadata_directory
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def build_sdist(sdist_directory, config_settings=None):
|
|
52
|
+
"""Build source distribution."""
|
|
53
|
+
import maturin
|
|
54
|
+
|
|
55
|
+
return maturin.build_sdist(sdist_directory, config_settings)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_requires_for_build_wheel(config_settings=None):
|
|
59
|
+
"""Get requirements for building wheel."""
|
|
60
|
+
if RUST_AVAILABLE:
|
|
61
|
+
return ["maturin>=1.0,<2.0"]
|
|
62
|
+
else:
|
|
63
|
+
return ["setuptools>=61.0"]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_requires_for_build_sdist(config_settings=None):
|
|
67
|
+
"""Get requirements for building sdist."""
|
|
68
|
+
return ["maturin>=1.0,<2.0"]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# For editable installs, allow fallback without Rust
|
|
72
|
+
def build_editable(wheel_directory, config_settings=None, metadata_directory=None):
|
|
73
|
+
"""Build editable install - can work without Rust."""
|
|
74
|
+
if RUST_AVAILABLE:
|
|
75
|
+
import maturin
|
|
76
|
+
|
|
77
|
+
return maturin.build_editable(wheel_directory, config_settings, metadata_directory)
|
|
78
|
+
else:
|
|
79
|
+
_warn_no_rust()
|
|
80
|
+
# Fall back to setuptools for editable install without Rust extension
|
|
81
|
+
import setuptools.build_meta
|
|
82
|
+
|
|
83
|
+
return setuptools.build_meta.build_editable(
|
|
84
|
+
wheel_directory, config_settings, metadata_directory
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_requires_for_build_editable(config_settings=None):
|
|
89
|
+
"""Get requirements for editable install."""
|
|
90
|
+
if RUST_AVAILABLE:
|
|
91
|
+
return ["maturin>=1.0,<2.0"]
|
|
92
|
+
else:
|
|
93
|
+
return ["setuptools>=61.0"]
|
dsgrid/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
import warnings
|
|
3
|
+
|
|
4
|
+
from dsgrid.dsgrid_rc import DsgridRuntimeConfig
|
|
5
|
+
from dsgrid.utils.timing import timer_stats_collector # noqa: F401
|
|
6
|
+
|
|
7
|
+
__title__ = "dsgrid"
|
|
8
|
+
__description__ = (
|
|
9
|
+
"Python API for registring and accessing demand-side grid model (dsgrid) datasets"
|
|
10
|
+
)
|
|
11
|
+
__url__ = "https://github.com/dsgrid/dsgrid"
|
|
12
|
+
__version__ = "0.3.3"
|
|
13
|
+
__author__ = "NREL"
|
|
14
|
+
__maintainer_email__ = "elaine.hale@nrel.gov"
|
|
15
|
+
__license__ = "BSD-3"
|
|
16
|
+
__copyright__ = "Copyright {}, The Alliance for Sustainable Energy, LLC".format(
|
|
17
|
+
dt.date.today().year
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
warnings.filterwarnings("ignore", module="duckdb_engine")
|
|
21
|
+
|
|
22
|
+
runtime_config = DsgridRuntimeConfig.load()
|
dsgrid/api/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from dsgrid.exceptions import DSGValueNotStored
|
|
7
|
+
from dsgrid.registry.registry_manager import RegistryManager
|
|
8
|
+
from dsgrid.utils.files import load_data
|
|
9
|
+
from .models import StoreModel, AsyncTaskModel, AsyncTaskStatus, AsyncTaskType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
MAX_CONCURRENT_ASYNC_TASKS = 4
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ApiManager:
|
|
18
|
+
"""Manages API requests"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
home_dir: str | Path,
|
|
23
|
+
registry_manager: RegistryManager,
|
|
24
|
+
max_concurrent_async_tasks=MAX_CONCURRENT_ASYNC_TASKS,
|
|
25
|
+
):
|
|
26
|
+
self._home_dir = Path(home_dir)
|
|
27
|
+
self._store = Store.load(self._home_dir)
|
|
28
|
+
self._lock = threading.RLock()
|
|
29
|
+
self._max_concurrent_async_tasks = max_concurrent_async_tasks
|
|
30
|
+
self._cached_projects = {}
|
|
31
|
+
self._registry_mgr = registry_manager
|
|
32
|
+
|
|
33
|
+
def can_start_new_async_task(self):
|
|
34
|
+
self._lock.acquire()
|
|
35
|
+
try:
|
|
36
|
+
return len(self._store.data.outstanding_async_tasks) < self._max_concurrent_async_tasks
|
|
37
|
+
finally:
|
|
38
|
+
self._lock.release()
|
|
39
|
+
|
|
40
|
+
def initialize_async_task(self, task_type: AsyncTaskType) -> int:
|
|
41
|
+
self._lock.acquire()
|
|
42
|
+
try:
|
|
43
|
+
num_outstanding = len(self._store.data.outstanding_async_tasks)
|
|
44
|
+
# TODO: implement queueing so that we don't return an error
|
|
45
|
+
if num_outstanding > self._max_concurrent_async_tasks:
|
|
46
|
+
msg = f"Too many async tasks are already running: {num_outstanding}"
|
|
47
|
+
raise Exception(msg)
|
|
48
|
+
async_task_id = self._get_next_async_task_id()
|
|
49
|
+
task = AsyncTaskModel(
|
|
50
|
+
async_task_id=async_task_id,
|
|
51
|
+
task_type=task_type,
|
|
52
|
+
status=AsyncTaskStatus.IN_PROGRESS,
|
|
53
|
+
start_time=datetime.now(),
|
|
54
|
+
)
|
|
55
|
+
self._store.data.async_tasks[async_task_id] = task
|
|
56
|
+
self._store.data.outstanding_async_tasks.add(async_task_id)
|
|
57
|
+
self._store.persist()
|
|
58
|
+
finally:
|
|
59
|
+
self._lock.release()
|
|
60
|
+
|
|
61
|
+
logger.info("Initialized async_task_id=%s", async_task_id)
|
|
62
|
+
return async_task_id
|
|
63
|
+
|
|
64
|
+
def clear_completed_async_tasks(self):
|
|
65
|
+
self._lock.acquire()
|
|
66
|
+
try:
|
|
67
|
+
to_remove = [
|
|
68
|
+
x.async_task_id
|
|
69
|
+
for x in self._store.data.async_tasks
|
|
70
|
+
if x.status == AsyncTaskStatus.COMPLETE
|
|
71
|
+
]
|
|
72
|
+
for async_task_id in to_remove:
|
|
73
|
+
self._store.data.async_tasks.pop(async_task_id)
|
|
74
|
+
self._store.persist()
|
|
75
|
+
logger.info("Cleared %d completed tasks", len(to_remove))
|
|
76
|
+
finally:
|
|
77
|
+
self._lock.release()
|
|
78
|
+
|
|
79
|
+
def get_async_task_status(self, async_task_id):
|
|
80
|
+
"""Return the status of the async ID."""
|
|
81
|
+
self._lock.acquire()
|
|
82
|
+
try:
|
|
83
|
+
return self._store.data.async_tasks[async_task_id]
|
|
84
|
+
finally:
|
|
85
|
+
self._lock.release()
|
|
86
|
+
|
|
87
|
+
def complete_async_task(self, async_task_id, return_code: int, result=None):
|
|
88
|
+
"""Complete an asynchronous operation."""
|
|
89
|
+
self._lock.acquire()
|
|
90
|
+
try:
|
|
91
|
+
task = self._store.data.async_tasks[async_task_id]
|
|
92
|
+
task.status = AsyncTaskStatus.COMPLETE
|
|
93
|
+
task.return_code = return_code
|
|
94
|
+
task.completion_time = datetime.now()
|
|
95
|
+
self._store.data.outstanding_async_tasks.remove(async_task_id)
|
|
96
|
+
if result is not None:
|
|
97
|
+
task.result = result
|
|
98
|
+
self._store.persist()
|
|
99
|
+
finally:
|
|
100
|
+
self._lock.release()
|
|
101
|
+
|
|
102
|
+
logger.info("Completed async_task_id=%s", async_task_id)
|
|
103
|
+
|
|
104
|
+
def list_async_tasks(self, async_task_ids=None, status=None) -> list[AsyncTaskModel]:
|
|
105
|
+
"""Return async tasks.
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
async_task_ids : list | None
|
|
110
|
+
IDs of tasks for which to return status. If not set, return all statuses.
|
|
111
|
+
status : AsyncTaskStatus | None
|
|
112
|
+
If set, filter tasks by this status.
|
|
113
|
+
|
|
114
|
+
"""
|
|
115
|
+
self._lock.acquire()
|
|
116
|
+
try:
|
|
117
|
+
if async_task_ids is not None:
|
|
118
|
+
diff = set(async_task_ids).difference(self._store.data.async_tasks.keys())
|
|
119
|
+
if diff:
|
|
120
|
+
msg = f"async_task_ids={diff} are not stored"
|
|
121
|
+
raise DSGValueNotStored(msg)
|
|
122
|
+
tasks = (
|
|
123
|
+
self._store.data.async_tasks.keys() if async_task_ids is None else async_task_ids
|
|
124
|
+
)
|
|
125
|
+
return [
|
|
126
|
+
self._store.data.async_tasks[x]
|
|
127
|
+
for x in tasks
|
|
128
|
+
if status is None or self._store.data.async_tasks[x].status == status
|
|
129
|
+
]
|
|
130
|
+
finally:
|
|
131
|
+
self._lock.release()
|
|
132
|
+
|
|
133
|
+
def _get_next_async_task_id(self) -> int:
|
|
134
|
+
self._lock.acquire()
|
|
135
|
+
try:
|
|
136
|
+
next_id = self._store.data.next_async_task_id
|
|
137
|
+
self._store.data.next_async_task_id += 1
|
|
138
|
+
self._store.persist()
|
|
139
|
+
finally:
|
|
140
|
+
self._lock.release()
|
|
141
|
+
|
|
142
|
+
return next_id
|
|
143
|
+
|
|
144
|
+
def get_project(self, project_id):
|
|
145
|
+
"""Load a Project and cache it for future calls.
|
|
146
|
+
Loading is slow and the Project isn't being changed by this API.
|
|
147
|
+
"""
|
|
148
|
+
self._lock.acquire()
|
|
149
|
+
try:
|
|
150
|
+
project = self._cached_projects.get(project_id)
|
|
151
|
+
if project is not None:
|
|
152
|
+
return project
|
|
153
|
+
project = self._registry_mgr.project_manager.load_project(project_id)
|
|
154
|
+
self._cached_projects[project_id] = project
|
|
155
|
+
return project
|
|
156
|
+
finally:
|
|
157
|
+
self._lock.release()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class Store:
|
|
161
|
+
STORE_FILENAME = "api_server_store.json"
|
|
162
|
+
|
|
163
|
+
def __init__(self, store_file: Path, data: StoreModel):
|
|
164
|
+
self._store_file = store_file
|
|
165
|
+
self.data = data
|
|
166
|
+
|
|
167
|
+
@classmethod
|
|
168
|
+
def load(cls, path: Path):
|
|
169
|
+
# TODO: use MongoDB or some other db
|
|
170
|
+
store_file = path / cls.STORE_FILENAME
|
|
171
|
+
if store_file.exists():
|
|
172
|
+
logger.info("Load from existing store: %s", store_file)
|
|
173
|
+
store_data = load_data(store_file)
|
|
174
|
+
return cls(store_file, StoreModel(**store_data))
|
|
175
|
+
logger.info("Create new store: %s", store_file)
|
|
176
|
+
return cls(store_file, StoreModel())
|
|
177
|
+
|
|
178
|
+
def persist(self):
|
|
179
|
+
self._store_file.write_text(self.data.model_dump_json(indent=2))
|