Skip to content

Commit efca00c

Browse files
authored
[DEV-4393] Updated EwbDataFilePaths to support variants. (#206)
Signed-off-by: Anthony Charlton <anthony.charlton@zepben.com>
1 parent a920cef commit efca00c

File tree

6 files changed

+448
-429
lines changed

6 files changed

+448
-429
lines changed

changelog.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Zepben Python SDK
22
## [1.1.0] - UNRELEASED
33
### Breaking Changes
4-
* None.
4+
* Updated `EwbDataFilePaths` to be an abstract class that supports variants. Added `LocalEwbDataFilePaths` which is a local file system implementation of
5+
`EwbDataFilePaths`, and should be used in place of the old `EwbDataFilePaths`.
56

67
### New Features
78
* None.
@@ -176,7 +177,7 @@
176177
* `RegulatingControl.ratedCurrent`
177178
* `Sensor.relayFunctions`
178179
* `UsagePoint.approvedInverterCapacity`
179-
* using `EquipmentTreeBuilder` more then once per interpreter will no longer cause the `roots` to contain more objects then it should due to `_roots` being a
180+
* using `EquipmentTreeBuilder` more then once per interpreter will no longer cause the `roots` to contain more objects then it should due to `_roots` being a
180181
class var
181182
* Errors when initiating gRPC connections will now properly be propagated to users.
182183

src/zepben/ewb/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@
352352

353353
from zepben.ewb.database.paths.database_type import *
354354
from zepben.ewb.database.paths.ewb_data_file_paths import *
355+
from zepben.ewb.database.paths.local_ewb_data_file_paths import *
355356

356357
from zepben.ewb.database.sql.column import *
357358
from zepben.ewb.database.sqlite.tables.sqlite_table import *
Lines changed: 122 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -1,237 +1,192 @@
1-
# Copyright 2024 Zeppelin Bend Pty Ltd
1+
# Copyright 2025 Zeppelin Bend Pty Ltd
22
# This Source Code Form is subject to the terms of the Mozilla Public
33
# License, v. 2.0. If a copy of the MPL was not distributed with this
44
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
55

66
__all__ = ['EwbDataFilePaths']
77

8+
from abc import ABC, abstractmethod
89
from datetime import date, timedelta
910
from pathlib import Path
10-
from typing import Callable, Iterator, Optional, List
11+
from typing import Optional, List, Generator
1112

1213
from zepben.ewb import require
1314
from zepben.ewb.database.paths.database_type import DatabaseType
1415

1516

16-
class EwbDataFilePaths:
17+
class EwbDataFilePaths(ABC):
1718
"""Provides paths to all the various data files / folders used by EWB."""
1819

19-
def __init__(self, base_dir: Path,
20-
create_path: bool = False,
21-
create_directories_func: Callable[[Path], None] = lambda it: it.mkdir(parents=True),
22-
is_directory: Callable[[Path], bool] = Path.is_dir,
23-
exists: Callable[[Path], bool] = Path.exists,
24-
list_files: Callable[[Path], Iterator[Path]] = Path.iterdir):
25-
"""
26-
:param base_dir: The root directory of the EWB data structure.
27-
:param create_path: Create the root directory (and any missing parent folders) if it does not exist.
28-
"""
29-
self.create_directories_func = create_directories_func
30-
self.is_directory = is_directory
31-
self.exists = exists
32-
self.list_files = list_files
33-
self._base_dir = base_dir
34-
35-
if create_path:
36-
self.create_directories_func(base_dir)
37-
38-
require(self.is_directory(base_dir), lambda: f"base_dir must be a directory")
39-
40-
@property
41-
def base_dir(self):
42-
"""The root directory of the EWB data structure."""
43-
return self._base_dir
44-
45-
def customer(self, database_date: date) -> Path:
46-
"""
47-
Determine the path to the "customers" database for the specified date.
48-
49-
:param database_date: The :class:`date` to use for the "customers" database.
50-
:return: The :class:`path` to the "customers" database for the specified date.
51-
"""
52-
return self._to_dated_path(database_date, DatabaseType.CUSTOMER.file_descriptor)
53-
54-
def diagram(self, database_date: date) -> Path:
55-
"""
56-
Determine the path to the "diagrams" database for the specified date.
57-
58-
:param database_date: The :class:`date` to use for the "diagrams" database.
59-
:return: The :class:`path` to the "diagrams" database for the specified date.
60-
"""
61-
return self._to_dated_path(database_date, DatabaseType.DIAGRAM.file_descriptor)
62-
63-
def measurement(self, database_date: date) -> Path:
64-
"""
65-
Determine the path to the "measurements" database for the specified date.
66-
67-
:param database_date: The :class:`date` to use for the "measurements" database.
68-
:return: The :class:`path` to the "measurements" database for the specified date.
69-
"""
70-
return self._to_dated_path(database_date, DatabaseType.MEASUREMENT.file_descriptor)
71-
72-
def network_model(self, database_date: date) -> Path:
73-
"""
74-
Determine the path to the "network model" database for the specified date.
75-
76-
:param database_date: The :class:`date` to use for the "network model" database.
77-
:return: The :class:`path` to the "network model" database for the specified date.
78-
"""
79-
return self._to_dated_path(database_date, DatabaseType.NETWORK_MODEL.file_descriptor)
80-
81-
def tile_cache(self, database_date: date) -> Path:
82-
"""
83-
Determine the path to the "tile cache" database for the specified date.
84-
85-
:param database_date: The :class:`date` to use for the "tile cache" database.
86-
:return: The :class:`path` to the "tile cache" database for the specified date.
87-
"""
88-
return self._to_dated_path(database_date, DatabaseType.TILE_CACHE.file_descriptor)
89-
90-
def energy_reading(self, database_date: date) -> Path:
91-
"""
92-
Determine the path to the "energy readings" database for the specified date.
93-
94-
:param database_date: The :class:`date` to use for the "energy readings" database.
95-
:return: The :class:`path` to the "energy readings" database for the specified date.
96-
"""
97-
return self._to_dated_path(database_date, DatabaseType.ENERGY_READING.file_descriptor)
98-
99-
def energy_readings_index(self) -> Path:
100-
"""
101-
Determine the path to the "energy readings index" database.
102-
103-
:return: The :class:`path` to the "energy readings index" database.
104-
"""
105-
return self._base_dir.joinpath(f"{DatabaseType.ENERGY_READINGS_INDEX.file_descriptor}.sqlite")
20+
VARIANTS_PATH: str = "variants"
21+
"""
22+
The folder containing the variants. Will be placed under the dated folder alongside the network model database.
23+
"""
10624

107-
def load_aggregator_meters_by_date(self) -> Path:
25+
def resolve(self, database_type: DatabaseType, database_date: Optional[date] = None, variant: Optional[str] = None) -> Path:
10826
"""
109-
Determine the path to the "load aggregator meters-by-date" database.
27+
Resolves the :class:`Path` to the database file for the specified :class:`DatabaseType`, within the specified `database_date`
28+
and optional `variant` when `DatabaseType.per_date` is set to true.
11029
111-
:return: The :class:`path` to the "load aggregator meters-by-date" database.
112-
"""
113-
return self._base_dir.joinpath(f"{DatabaseType.LOAD_AGGREGATOR_METERS_BY_DATE.file_descriptor}.sqlite")
114-
115-
def weather_reading(self) -> Path:
116-
"""
117-
Determine the path to the "weather readings" database.
30+
:param database_type: The :class:`DatabaseType` to use for the database :class:`Path`.
31+
:param database_date: The :class:`date` to use for the database :class:`Path`. Required when `database_type.per_date` is true, otherwise must be `None`.
32+
:param variant: The optional name of the variant containing the database.
11833
119-
:return: The :class:`path` to the "weather readings" database.
34+
:return: The :class:`Path` to the :class:`DatabaseType` database file.
12035
"""
121-
return self._base_dir.joinpath(f"{DatabaseType.WEATHER_READING.file_descriptor}.sqlite")
122-
123-
def results_cache(self) -> Path:
124-
"""
125-
Determine the path to the "results cache" database.
126-
127-
:return: The :class:`path` to the "results cache" database.
128-
"""
129-
return self._base_dir.joinpath(f"{DatabaseType.RESULTS_CACHE.file_descriptor}.sqlite")
36+
if database_date is not None:
37+
require(database_type.per_date, lambda: "database_type must have its per_date set to True to use this method with a database_date.")
38+
if variant is not None:
39+
return self.resolve_database(self._to_dated_variant_path(database_type, database_date, variant))
40+
else:
41+
return self.resolve_database(self._to_dated_path(database_type, database_date))
42+
else:
43+
require(not database_type.per_date, lambda: "database_type must have its per_date set to False to use this method without a database_date.")
44+
return self.resolve_database(Path(self._database_name(database_type)))
13045

46+
@abstractmethod
13147
def create_directories(self, database_date: date) -> Path:
13248
"""
13349
Create the directories required to have a valid path for the specified date.
13450
13551
:param database_date: The :class:`date` required in the path.
136-
:return: The :class:`path` to the directory for the `database_date`.
137-
"""
138-
date_path = self._base_dir.joinpath(str(database_date))
139-
if self.exists(date_path):
140-
return date_path
141-
else:
142-
self.create_directories_func(date_path)
143-
return date_path
144-
145-
def _to_dated_path(self, database_date: date, file: str) -> Path:
146-
return self._base_dir.joinpath(str(database_date), f"{database_date}-{file}.sqlite")
147-
148-
def _check_exists(self, database_type: DatabaseType, database_date: date) -> bool:
52+
:return: The :class:`Path` to the directory for the `database_date`.
14953
"""
150-
Check if a database of the specified type and date exists.
54+
raise NotImplemented
15155

152-
:param database_type: The type of database to search for.
153-
:param database_date: The date to check.
154-
:return: `True` if a database of the specified `database_type` and `database_date` exists in the date path.
155-
"""
156-
if not database_type.per_date:
157-
raise ValueError("INTERNAL ERROR: Should only be calling `checkExists` for `perDate` files.")
158-
159-
if database_type == DatabaseType.CUSTOMER:
160-
model_path = self.customer(database_date)
161-
elif database_type == DatabaseType.DIAGRAM:
162-
model_path = self.diagram(database_date)
163-
elif database_type == DatabaseType.MEASUREMENT:
164-
model_path = self.measurement(database_date)
165-
elif database_type == DatabaseType.NETWORK_MODEL:
166-
model_path = self.network_model(database_date)
167-
elif database_type == DatabaseType.TILE_CACHE:
168-
model_path = self.tile_cache(database_date)
169-
elif database_type == DatabaseType.ENERGY_READING:
170-
model_path = self.energy_reading(database_date)
171-
else:
172-
raise ValueError(
173-
"INTERNAL ERROR: Should only be calling `check_exists` for `per_date` files, which should all be covered above, so go ahead and add it.")
174-
return self.exists(model_path)
175-
176-
def find_closest(self, database_type: DatabaseType, max_days_to_search: int = 999, target_date: date = date.today(), search_forwards: bool = False) -> \
177-
Optional[date]:
56+
def find_closest(
57+
self,
58+
database_type: DatabaseType,
59+
max_days_to_search: int = 999999,
60+
target_date: date = date.today(),
61+
search_forwards: bool = False
62+
) -> Optional[date]:
17863
"""
17964
Find the closest date with a usable database of the specified type.
18065
18166
:param database_type: The type of database to search for.
18267
:param max_days_to_search: The maximum number of days to search for a valid database.
183-
:param target_date: The target :class:`date`. Defaults to today.
184-
:param search_forwards: Indicates the search should also look forwards in time from `start_date` for a valid file. Defaults to reverse search only.
185-
:return: The closest :class:`date` to `database_date` with a valid database of `database_type` within the search parameters, or `None` if no valid database was found.
68+
:param target_date: The target date. Defaults to today.
69+
:param search_forwards: Indicates the search should also look forwards in time from `target_date` for a valid file. Defaults to reverse search only.
70+
71+
:return: The closest :class:`date` to `target_date` with a valid database of `database_type` within the search parameters, or null if no valid database
72+
was found.
18673
"""
18774
if not database_type.per_date:
18875
return None
18976

190-
if self._check_exists(database_type, target_date):
77+
descendants = list(self.enumerate_descendants())
78+
if self._check_exists(descendants, database_type, target_date):
19179
return target_date
19280

19381
offset = 1
194-
19582
while offset <= max_days_to_search:
19683
offset_days = timedelta(offset)
19784
try:
19885
previous_date = target_date - offset_days
199-
if self._check_exists(database_type, previous_date):
86+
if self._check_exists(descendants, database_type, previous_date):
20087
return previous_date
20188
except OverflowError:
20289
pass
20390

20491
if search_forwards:
20592
try:
20693
forward_date = target_date + offset_days
207-
if self._check_exists(database_type, forward_date):
94+
if self._check_exists(descendants, database_type, forward_date):
20895
return forward_date
20996
except OverflowError:
21097
pass
98+
21199
offset += 1
100+
212101
return None
213102

214-
def _get_available_dates_for(self, database_type: DatabaseType) -> List[date]:
103+
def get_available_dates_for(self, database_type: DatabaseType) -> List[date]:
104+
"""
105+
Find available databases specified by :class:`DatabaseType` in data path.
106+
107+
:param database_type: The type of database to search for.
108+
109+
:return: list of :class:`date`'s for which this specified :class:`DatabaseType` databases exist in the data path.
110+
"""
215111
if not database_type.per_date:
216112
raise ValueError(
217-
"INTERNAL ERROR: Should only be calling `_get_available_dates_for` for `per_date` files.")
113+
"INTERNAL ERROR: Should only be calling `get_available_dates_for` for `per_date` files, "
114+
"which should all be covered above, so go ahead and add it."
115+
)
218116

219117
to_return = list()
220118

221-
for file in self.list_files(self._base_dir):
222-
if self.is_directory(file):
119+
for it in self.enumerate_descendants():
120+
if it.name.endswith(self._database_name(database_type)):
223121
try:
224-
database_date = date.fromisoformat(file.name)
225-
if self.exists(self._to_dated_path(database_date, database_type.file_descriptor)):
226-
to_return.append(database_date)
122+
to_return.append(date.fromisoformat(it.parent.name))
227123
except ValueError:
228124
pass
125+
126+
return sorted(to_return)
127+
128+
def get_available_variants_for(self, target_date: date = date.today()) -> List[str]:
129+
"""
130+
Find available variants for the specified `target_date` in data path.
131+
132+
:param target_date: The target date. Defaults to today.
133+
134+
:return: list of variant names that exist in the data path for the specified `target_date`.
135+
"""
136+
to_return = list()
137+
138+
for it in self.enumerate_descendants():
139+
try:
140+
if (str(it.parent.name).lower() == self.VARIANTS_PATH) and (str(it.parent.parent.name) == str(target_date)):
141+
to_return.append(str(it.name))
142+
except ValueError:
143+
pass
144+
229145
return sorted(to_return)
230146

231-
def get_network_model_databases(self) -> List[date]:
147+
@abstractmethod
148+
def enumerate_descendants(self) -> Generator[Path, None, None]:
149+
"""
150+
Lists the child items of source location.
151+
152+
:return: generator of child items.
153+
"""
154+
raise NotImplemented
155+
156+
@abstractmethod
157+
def resolve_database(self, path: Path) -> Path:
232158
"""
233-
Find available network-model databases in data path.
159+
Resolves the database in the specified source :class:`Path`.
234160
235-
:return: A list of :class:`date`'s for which network-model databases exist in the data path.
161+
:param path: :class:`Path` to the source database file.
162+
:return: :class:`Path` to the local database file.
236163
"""
237-
return self._get_available_dates_for(DatabaseType.NETWORK_MODEL)
164+
raise NotImplemented
165+
166+
def _check_exists(self, descendants: List[Path], database_type: DatabaseType, database_date: date) -> bool:
167+
"""
168+
Check if a database :class:`Path` of the specified :class:`DatabaseType` and :class:`date` exists.
169+
170+
:param descendants: A list of :class:`Path` representing the descendant paths.
171+
:param database_type: The type of database to search for.
172+
:param database_date: The date to check.
173+
174+
:return: True if a database of the specified `database_type` and `database_date` exits in the date path.
175+
"""
176+
for cp in descendants:
177+
if cp.is_relative_to(self._to_dated_path(database_type, database_date)):
178+
return True
179+
180+
return False
181+
182+
def _to_dated_path(self, database_type: DatabaseType, database_date: date) -> Path:
183+
date_str = str(database_date)
184+
return Path(date_str).joinpath(f"{date_str}-{self._database_name(database_type)}")
185+
186+
def _to_dated_variant_path(self, database_type: DatabaseType, database_date: date, variant: str) -> Path:
187+
date_str = str(database_date)
188+
return Path(date_str).joinpath(self.VARIANTS_PATH, variant, f"{date_str}-{self._database_name(database_type)}")
189+
190+
@staticmethod
191+
def _database_name(database_type: DatabaseType) -> str:
192+
return f"{database_type.file_descriptor}.sqlite"

0 commit comments

Comments
 (0)