e-data 2.0.2.dev136__py3-none-any.whl → 2.0.2.dev138__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.
- {e_data-2.0.2.dev136.dist-info → e_data-2.0.2.dev138.dist-info}/METADATA +3 -4
- {e_data-2.0.2.dev136.dist-info → e_data-2.0.2.dev138.dist-info}/RECORD +13 -13
- edata/database/controller.py +241 -171
- edata/database/models.py +21 -20
- edata/providers/datadis.py +63 -47
- edata/tests/test_controller.py +33 -14
- edata/tests/test_datadis_connector.py +18 -13
- edata/tests/test_incremental.py +2 -10
- edata/tests/test_migrations.py +2 -10
- edata/tests/test_services.py +5 -5
- {e_data-2.0.2.dev136.dist-info → e_data-2.0.2.dev138.dist-info}/WHEEL +0 -0
- {e_data-2.0.2.dev136.dist-info → e_data-2.0.2.dev138.dist-info}/licenses/LICENSE +0 -0
- {e_data-2.0.2.dev136.dist-info → e_data-2.0.2.dev138.dist-info}/top_level.txt +0 -0
edata/database/controller.py
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import functools
|
|
2
3
|
import logging
|
|
3
4
|
import os
|
|
4
5
|
import typing
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
5
7
|
from datetime import datetime
|
|
6
8
|
|
|
7
|
-
from sqlalchemy import Select, Table, event, insert, or_
|
|
9
|
+
from sqlalchemy import Engine, Select, Table, create_engine, event, insert, or_
|
|
8
10
|
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
|
|
9
11
|
from sqlalchemy.exc import IntegrityError
|
|
10
|
-
from
|
|
11
|
-
from sqlmodel import SQLModel, UniqueConstraint
|
|
12
|
-
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
12
|
+
from sqlmodel import Session, SQLModel, UniqueConstraint
|
|
13
13
|
from sqlmodel.sql.expression import SelectOfScalar
|
|
14
14
|
|
|
15
15
|
import edata.database.queries as q
|
|
@@ -28,6 +28,7 @@ from edata.models.bill import Bill, EnergyPrice
|
|
|
28
28
|
_LOGGER = logging.getLogger(__name__)
|
|
29
29
|
|
|
30
30
|
T = typing.TypeVar("T", bound=SQLModel)
|
|
31
|
+
R = typing.TypeVar("R")
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
def _set_sqlite_pragmas(dbapi_connection, connection_record) -> None:
|
|
@@ -77,53 +78,91 @@ def _conflict_columns(table: Table) -> list[str]:
|
|
|
77
78
|
return [c.name for c in table.primary_key.columns]
|
|
78
79
|
|
|
79
80
|
|
|
81
|
+
def _in_db_thread(
|
|
82
|
+
fn: typing.Callable[..., R],
|
|
83
|
+
) -> typing.Callable[..., typing.Coroutine[typing.Any, typing.Any, R]]:
|
|
84
|
+
"""Turn a blocking ``EdataDB`` method into a coroutine run on its DB thread."""
|
|
85
|
+
|
|
86
|
+
@functools.wraps(fn)
|
|
87
|
+
async def wrapper(self: "EdataDB", *args: typing.Any, **kwargs: typing.Any) -> R:
|
|
88
|
+
loop = asyncio.get_running_loop()
|
|
89
|
+
return await loop.run_in_executor(
|
|
90
|
+
self._executor, functools.partial(fn, self, *args, **kwargs)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return wrapper
|
|
94
|
+
|
|
95
|
+
|
|
80
96
|
class EdataDB:
|
|
97
|
+
"""SQLite store.
|
|
98
|
+
|
|
99
|
+
The public API is async, but every database operation (SQLAlchemy, pydantic
|
|
100
|
+
(de)serialization and SQLite itself) runs on a single dedicated thread so a
|
|
101
|
+
large import never stalls the caller's event loop (e.g. Home Assistant's).
|
|
102
|
+
One thread also serializes access, which suits SQLite's single writer.
|
|
103
|
+
"""
|
|
81
104
|
|
|
82
105
|
_instance = None
|
|
83
|
-
_engine:
|
|
106
|
+
_engine: Engine | None = None
|
|
84
107
|
_db_url: str | None = None
|
|
85
108
|
|
|
86
109
|
def __new__(cls, sqlite_path: str):
|
|
87
|
-
db_url = f"sqlite
|
|
110
|
+
db_url = f"sqlite:////{os.path.abspath(sqlite_path)}"
|
|
88
111
|
if cls._instance is None:
|
|
89
112
|
cls._instance = super().__new__(cls)
|
|
90
113
|
cls._db_url = db_url
|
|
91
114
|
# Ensure parent directory exists before the first connection is opened.
|
|
92
115
|
dir_path = os.path.dirname(os.path.abspath(sqlite_path))
|
|
93
116
|
os.makedirs(dir_path, exist_ok=True)
|
|
94
|
-
cls._engine =
|
|
95
|
-
event.listen(cls._engine
|
|
117
|
+
cls._engine = create_engine(db_url)
|
|
118
|
+
event.listen(cls._engine, "connect", _set_sqlite_pragmas)
|
|
96
119
|
cls._instance._tables_initialized = False
|
|
97
|
-
cls._instance.
|
|
120
|
+
cls._instance._executor = ThreadPoolExecutor(
|
|
121
|
+
max_workers=1, thread_name_prefix="edata-db"
|
|
122
|
+
)
|
|
98
123
|
elif db_url != cls._db_url:
|
|
99
124
|
raise ValueError("EdataDB already initialized with a different db_url")
|
|
100
125
|
return cls._instance
|
|
101
126
|
|
|
127
|
+
@classmethod
|
|
128
|
+
def reset(cls) -> None:
|
|
129
|
+
"""Close the shared instance (its DB thread and engine) and forget it.
|
|
130
|
+
|
|
131
|
+
Waits for queued database work to finish first. A later ``EdataDB(...)``
|
|
132
|
+
starts afresh, possibly on another path.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
if cls._instance is not None:
|
|
136
|
+
cls._instance._executor.shutdown(wait=True)
|
|
137
|
+
if cls._engine is not None:
|
|
138
|
+
cls._engine.dispose()
|
|
139
|
+
cls._instance = None
|
|
140
|
+
cls._engine = None
|
|
141
|
+
cls._db_url = None
|
|
142
|
+
|
|
102
143
|
@property
|
|
103
|
-
def engine(self) ->
|
|
104
|
-
"""Return the
|
|
144
|
+
def engine(self) -> Engine | None:
|
|
145
|
+
"""Return the database engine."""
|
|
105
146
|
|
|
106
147
|
return self._engine
|
|
107
148
|
|
|
108
|
-
|
|
109
|
-
"""Create tables if not already
|
|
149
|
+
def _ensure_tables(self) -> None:
|
|
150
|
+
"""Create tables and missing indexes if not already done (lazy init).
|
|
110
151
|
|
|
111
|
-
|
|
152
|
+
Only ever runs on the database thread, so concurrent first calls are
|
|
153
|
+
serialized and cannot race to create the same index.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
if self._tables_initialized or not self.engine:
|
|
112
157
|
return
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
async with self.engine.begin() as conn:
|
|
120
|
-
await conn.run_sync(SQLModel.metadata.create_all)
|
|
121
|
-
await conn.run_sync(_create_missing_indexes)
|
|
122
|
-
self._tables_initialized = True
|
|
123
|
-
|
|
124
|
-
async def _add_one(
|
|
158
|
+
with self.engine.begin() as conn:
|
|
159
|
+
SQLModel.metadata.create_all(conn)
|
|
160
|
+
_create_missing_indexes(conn)
|
|
161
|
+
self._tables_initialized = True
|
|
162
|
+
|
|
163
|
+
def _add_one(
|
|
125
164
|
self,
|
|
126
|
-
session:
|
|
165
|
+
session: Session,
|
|
127
166
|
record: T,
|
|
128
167
|
commit: bool = True,
|
|
129
168
|
) -> T:
|
|
@@ -131,15 +170,15 @@ class EdataDB:
|
|
|
131
170
|
|
|
132
171
|
session.add(record)
|
|
133
172
|
if commit:
|
|
134
|
-
|
|
135
|
-
|
|
173
|
+
session.commit()
|
|
174
|
+
session.refresh(record)
|
|
136
175
|
else:
|
|
137
|
-
|
|
176
|
+
session.flush()
|
|
138
177
|
return record
|
|
139
178
|
|
|
140
|
-
|
|
179
|
+
def _update_one(
|
|
141
180
|
self,
|
|
142
|
-
session:
|
|
181
|
+
session: Session,
|
|
143
182
|
query: SelectOfScalar,
|
|
144
183
|
data: typing.Any,
|
|
145
184
|
commit: bool = True,
|
|
@@ -147,7 +186,7 @@ class EdataDB:
|
|
|
147
186
|
) -> T | None: # type: ignore
|
|
148
187
|
"""Update a single record in the database."""
|
|
149
188
|
|
|
150
|
-
result =
|
|
189
|
+
result = session.exec(query)
|
|
151
190
|
existing = result.first()
|
|
152
191
|
if existing and getattr(existing, "data") == data and not overrides:
|
|
153
192
|
return existing
|
|
@@ -156,15 +195,15 @@ class EdataDB:
|
|
|
156
195
|
for key, value in overrides.items():
|
|
157
196
|
setattr(existing, key, value)
|
|
158
197
|
if commit:
|
|
159
|
-
|
|
160
|
-
|
|
198
|
+
session.commit()
|
|
199
|
+
session.refresh(existing)
|
|
161
200
|
else:
|
|
162
|
-
|
|
201
|
+
session.flush()
|
|
163
202
|
return existing
|
|
164
203
|
|
|
165
|
-
|
|
204
|
+
def _add_or_update_one(
|
|
166
205
|
self,
|
|
167
|
-
session:
|
|
206
|
+
session: Session,
|
|
168
207
|
query: SelectOfScalar,
|
|
169
208
|
record: T,
|
|
170
209
|
commit: bool = True,
|
|
@@ -173,12 +212,12 @@ class EdataDB:
|
|
|
173
212
|
"""Add a single record into the database and fallback to update safely."""
|
|
174
213
|
|
|
175
214
|
try:
|
|
176
|
-
|
|
215
|
+
with session.begin_nested():
|
|
177
216
|
session.add(record)
|
|
178
|
-
|
|
217
|
+
session.flush()
|
|
179
218
|
if commit:
|
|
180
|
-
|
|
181
|
-
|
|
219
|
+
session.commit()
|
|
220
|
+
session.refresh(record)
|
|
182
221
|
return record
|
|
183
222
|
except IntegrityError:
|
|
184
223
|
new_data = getattr(record, "data")
|
|
@@ -188,13 +227,13 @@ class EdataDB:
|
|
|
188
227
|
override_dict = {
|
|
189
228
|
x: record_json[x] for x in record.model_dump() if x in override
|
|
190
229
|
}
|
|
191
|
-
return
|
|
230
|
+
return self._update_one(
|
|
192
231
|
session, query, new_data, commit=commit, overrides=override_dict
|
|
193
232
|
)
|
|
194
233
|
|
|
195
|
-
|
|
234
|
+
def _add_or_update_many(
|
|
196
235
|
self,
|
|
197
|
-
session:
|
|
236
|
+
session: Session,
|
|
198
237
|
model: type[SQLModel],
|
|
199
238
|
rows: list[dict[str, typing.Any]],
|
|
200
239
|
override: list[str] | None = None,
|
|
@@ -232,166 +271,183 @@ class EdataDB:
|
|
|
232
271
|
},
|
|
233
272
|
where=or_(*(table.c[c].is_distinct_from(stmt.excluded[c]) for c in updated)),
|
|
234
273
|
)
|
|
235
|
-
connection =
|
|
236
|
-
|
|
237
|
-
|
|
274
|
+
connection = session.connection()
|
|
275
|
+
connection.execute(stmt, rows)
|
|
276
|
+
session.commit()
|
|
238
277
|
|
|
239
|
-
|
|
278
|
+
@_in_db_thread
|
|
279
|
+
def get_supply(self, cups: str) -> SupplyModel | None:
|
|
240
280
|
"""Get a supply record by cups."""
|
|
241
281
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
result =
|
|
282
|
+
self._ensure_tables()
|
|
283
|
+
with Session(self.engine) as session:
|
|
284
|
+
result = session.exec(q.get_supply(cups))
|
|
245
285
|
return result.first()
|
|
246
286
|
|
|
247
|
-
|
|
287
|
+
@_in_db_thread
|
|
288
|
+
def get_contract(
|
|
248
289
|
self, cups: str, date_start: datetime | None = None
|
|
249
290
|
) -> ContractModel | None:
|
|
250
291
|
"""Get a contract record by cups."""
|
|
251
292
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
result =
|
|
293
|
+
self._ensure_tables()
|
|
294
|
+
with Session(self.engine) as session:
|
|
295
|
+
result = session.exec(q.get_contract(cups, date_start))
|
|
255
296
|
return result.first()
|
|
256
297
|
|
|
257
|
-
|
|
298
|
+
@_in_db_thread
|
|
299
|
+
def get_last_energy(self, cups: str) -> EnergyModel | None:
|
|
258
300
|
"""Get the most recent Energy record by cups."""
|
|
259
301
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
result =
|
|
302
|
+
self._ensure_tables()
|
|
303
|
+
with Session(self.engine) as session:
|
|
304
|
+
result = session.exec(q.get_last_energy(cups))
|
|
263
305
|
return result.first()
|
|
264
306
|
|
|
265
|
-
|
|
307
|
+
@_in_db_thread
|
|
308
|
+
def get_last_power(self, cups: str) -> PowerModel | None:
|
|
266
309
|
"""Get the most recent power record by cups."""
|
|
267
310
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
result =
|
|
311
|
+
self._ensure_tables()
|
|
312
|
+
with Session(self.engine) as session:
|
|
313
|
+
result = session.exec(q.get_last_power(cups))
|
|
271
314
|
return result.first()
|
|
272
315
|
|
|
273
|
-
|
|
316
|
+
@_in_db_thread
|
|
317
|
+
def get_last_pvpc(self) -> PVPCModel | None:
|
|
274
318
|
"""Get the most recent pvpc."""
|
|
275
319
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
result =
|
|
320
|
+
self._ensure_tables()
|
|
321
|
+
with Session(self.engine) as session:
|
|
322
|
+
result = session.exec(q.get_last_pvpc())
|
|
279
323
|
return result.first()
|
|
280
324
|
|
|
281
|
-
|
|
325
|
+
@_in_db_thread
|
|
326
|
+
def get_last_bill(self, cups: str) -> BillModel | None:
|
|
282
327
|
"""Get the most recent bill record by cups."""
|
|
283
328
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
result =
|
|
329
|
+
self._ensure_tables()
|
|
330
|
+
with Session(self.engine) as session:
|
|
331
|
+
result = session.exec(q.get_last_bill(cups))
|
|
287
332
|
return result.first()
|
|
288
333
|
|
|
289
|
-
|
|
334
|
+
@_in_db_thread
|
|
335
|
+
def get_last_complete_statistic(
|
|
290
336
|
self, cups: str, type_: typing.Literal["day", "month"]
|
|
291
337
|
) -> StatisticsModel | None:
|
|
292
338
|
"""Get the most recent complete statistics record by cups and type."""
|
|
293
339
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
result =
|
|
340
|
+
self._ensure_tables()
|
|
341
|
+
with Session(self.engine) as session:
|
|
342
|
+
result = session.exec(q.get_last_complete_statistic(cups, type_))
|
|
297
343
|
return result.first()
|
|
298
344
|
|
|
299
|
-
|
|
345
|
+
@_in_db_thread
|
|
346
|
+
def get_last_complete_bill(
|
|
300
347
|
self, cups: str, type_: typing.Literal["hour", "day", "month"]
|
|
301
348
|
) -> BillModel | None:
|
|
302
349
|
"""Get the most recent complete bill record by cups and type."""
|
|
303
350
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
result =
|
|
351
|
+
self._ensure_tables()
|
|
352
|
+
with Session(self.engine) as session:
|
|
353
|
+
result = session.exec(q.get_last_complete_bill(cups, type_))
|
|
307
354
|
return result.first()
|
|
308
355
|
|
|
309
|
-
|
|
356
|
+
@_in_db_thread
|
|
357
|
+
def add_contract(self, cups: str, contract: Contract) -> ContractModel | None:
|
|
310
358
|
"""Add or update a contract record."""
|
|
311
359
|
|
|
312
|
-
|
|
360
|
+
self._ensure_tables()
|
|
313
361
|
record = ContractModel(cups=cups, date_start=contract.date_start, data=contract)
|
|
314
|
-
|
|
315
|
-
return
|
|
362
|
+
with Session(self.engine) as session:
|
|
363
|
+
return self._add_or_update_one(
|
|
316
364
|
session, q.get_contract(cups, contract.date_start), record
|
|
317
365
|
)
|
|
318
366
|
|
|
319
|
-
|
|
367
|
+
@_in_db_thread
|
|
368
|
+
def add_supply(self, supply: Supply) -> SupplyModel | None:
|
|
320
369
|
"""Add or update a supply record."""
|
|
321
370
|
|
|
322
|
-
|
|
371
|
+
self._ensure_tables()
|
|
323
372
|
record = SupplyModel(cups=supply.cups, data=supply)
|
|
324
|
-
|
|
325
|
-
return
|
|
373
|
+
with Session(self.engine) as session:
|
|
374
|
+
return self._add_or_update_one(
|
|
326
375
|
session, q.get_supply(supply.cups), record
|
|
327
376
|
)
|
|
328
377
|
|
|
329
|
-
|
|
378
|
+
@_in_db_thread
|
|
379
|
+
def add_energy(self, cups: str, energy: Energy) -> EnergyModel | None:
|
|
330
380
|
"""Add or update an energy record."""
|
|
331
381
|
|
|
332
|
-
|
|
382
|
+
self._ensure_tables()
|
|
333
383
|
record = EnergyModel(
|
|
334
384
|
cups=cups, delta_h=energy.delta_h, datetime=energy.datetime, data=energy
|
|
335
385
|
)
|
|
336
|
-
|
|
337
|
-
return
|
|
386
|
+
with Session(self.engine) as session:
|
|
387
|
+
return self._add_or_update_one(
|
|
338
388
|
session, q.get_energy(cups, energy.datetime), record
|
|
339
389
|
)
|
|
340
390
|
|
|
341
|
-
|
|
391
|
+
@_in_db_thread
|
|
392
|
+
def add_energy_list(self, cups: str, energy: list[Energy]) -> None:
|
|
342
393
|
"""Add or update a list of energy records."""
|
|
343
394
|
|
|
344
|
-
|
|
345
|
-
|
|
395
|
+
self._ensure_tables()
|
|
396
|
+
with Session(self.engine) as session:
|
|
346
397
|
unique_map = {item.datetime: item for item in energy}
|
|
347
398
|
unique = list(unique_map.values())
|
|
348
399
|
rows = [
|
|
349
400
|
{"cups": cups, "delta_h": x.delta_h, "datetime": x.datetime, "data": x}
|
|
350
401
|
for x in unique
|
|
351
402
|
]
|
|
352
|
-
|
|
403
|
+
self._add_or_update_many(session, EnergyModel, rows)
|
|
353
404
|
|
|
354
|
-
|
|
405
|
+
@_in_db_thread
|
|
406
|
+
def add_power(self, cups: str, power: Power) -> PowerModel | None:
|
|
355
407
|
"""Add or update a power record for a given CUPS and Power instance."""
|
|
356
408
|
|
|
357
|
-
|
|
409
|
+
self._ensure_tables()
|
|
358
410
|
record = PowerModel(cups=cups, datetime=power.datetime, data=power)
|
|
359
|
-
|
|
360
|
-
return
|
|
411
|
+
with Session(self.engine) as session:
|
|
412
|
+
return self._add_or_update_one(
|
|
361
413
|
session, q.get_power(cups, power.datetime), record
|
|
362
414
|
)
|
|
363
415
|
|
|
364
|
-
|
|
416
|
+
@_in_db_thread
|
|
417
|
+
def add_power_list(self, cups: str, power: list[Power]) -> None:
|
|
365
418
|
"""Add or update a list of power records."""
|
|
366
419
|
|
|
367
|
-
|
|
368
|
-
|
|
420
|
+
self._ensure_tables()
|
|
421
|
+
with Session(self.engine) as session:
|
|
369
422
|
unique_map = {item.datetime: item for item in power}
|
|
370
423
|
unique = list(unique_map.values())
|
|
371
424
|
rows = [{"cups": cups, "datetime": x.datetime, "data": x} for x in unique]
|
|
372
|
-
|
|
425
|
+
self._add_or_update_many(session, PowerModel, rows)
|
|
373
426
|
|
|
374
|
-
|
|
427
|
+
@_in_db_thread
|
|
428
|
+
def add_pvpc(self, pvpc: EnergyPrice) -> PVPCModel | None:
|
|
375
429
|
"""Add or update a pvpc record."""
|
|
376
430
|
|
|
377
|
-
|
|
431
|
+
self._ensure_tables()
|
|
378
432
|
record = PVPCModel(datetime=pvpc.datetime, data=pvpc)
|
|
379
|
-
|
|
380
|
-
return
|
|
433
|
+
with Session(self.engine) as session:
|
|
434
|
+
return self._add_or_update_one(
|
|
381
435
|
session, q.get_pvpc(pvpc.datetime), record
|
|
382
436
|
)
|
|
383
437
|
|
|
384
|
-
|
|
438
|
+
@_in_db_thread
|
|
439
|
+
def add_pvpc_list(self, pvpc: list[EnergyPrice]) -> None:
|
|
385
440
|
"""Add or update a list of pvpc records."""
|
|
386
441
|
|
|
387
|
-
|
|
388
|
-
|
|
442
|
+
self._ensure_tables()
|
|
443
|
+
with Session(self.engine) as session:
|
|
389
444
|
unique_map = {item.datetime: item for item in pvpc}
|
|
390
445
|
unique = list(unique_map.values())
|
|
391
446
|
rows = [{"datetime": x.datetime, "data": x} for x in unique]
|
|
392
|
-
|
|
447
|
+
self._add_or_update_many(session, PVPCModel, rows)
|
|
393
448
|
|
|
394
|
-
|
|
449
|
+
@_in_db_thread
|
|
450
|
+
def add_statistics(
|
|
395
451
|
self,
|
|
396
452
|
cups: str,
|
|
397
453
|
type_: typing.Literal["day", "month"],
|
|
@@ -400,19 +456,20 @@ class EdataDB:
|
|
|
400
456
|
) -> StatisticsModel | None:
|
|
401
457
|
"""Add or update a statistics record."""
|
|
402
458
|
|
|
403
|
-
|
|
459
|
+
self._ensure_tables()
|
|
404
460
|
record = StatisticsModel(
|
|
405
461
|
cups=cups, datetime=data.datetime, type=type_, data=data, complete=complete
|
|
406
462
|
)
|
|
407
|
-
|
|
408
|
-
return
|
|
463
|
+
with Session(self.engine) as session:
|
|
464
|
+
return self._add_or_update_one(
|
|
409
465
|
session,
|
|
410
466
|
q.get_statistics(cups, type_, data.datetime),
|
|
411
467
|
record,
|
|
412
468
|
override=["complete"],
|
|
413
469
|
)
|
|
414
470
|
|
|
415
|
-
|
|
471
|
+
@_in_db_thread
|
|
472
|
+
def add_statistics_list(
|
|
416
473
|
self,
|
|
417
474
|
cups: str,
|
|
418
475
|
type_: typing.Literal["day", "month"],
|
|
@@ -421,8 +478,8 @@ class EdataDB:
|
|
|
421
478
|
) -> None:
|
|
422
479
|
"""Add or update a list of statistics records."""
|
|
423
480
|
|
|
424
|
-
|
|
425
|
-
|
|
481
|
+
self._ensure_tables()
|
|
482
|
+
with Session(self.engine) as session:
|
|
426
483
|
unique_map = {item.datetime: item for item in statistics}
|
|
427
484
|
rows = [
|
|
428
485
|
{
|
|
@@ -434,11 +491,12 @@ class EdataDB:
|
|
|
434
491
|
}
|
|
435
492
|
for x in unique_map.values()
|
|
436
493
|
]
|
|
437
|
-
|
|
494
|
+
self._add_or_update_many(
|
|
438
495
|
session, StatisticsModel, rows, override=["complete"]
|
|
439
496
|
)
|
|
440
497
|
|
|
441
|
-
|
|
498
|
+
@_in_db_thread
|
|
499
|
+
def add_bill(
|
|
442
500
|
self,
|
|
443
501
|
cups: str,
|
|
444
502
|
type_: typing.Literal["hour", "day", "month"],
|
|
@@ -448,7 +506,7 @@ class EdataDB:
|
|
|
448
506
|
) -> BillModel | None:
|
|
449
507
|
"""Add or update a bill record."""
|
|
450
508
|
|
|
451
|
-
|
|
509
|
+
self._ensure_tables()
|
|
452
510
|
record = BillModel(
|
|
453
511
|
cups=cups,
|
|
454
512
|
datetime=data.datetime,
|
|
@@ -457,15 +515,16 @@ class EdataDB:
|
|
|
457
515
|
confhash=confhash,
|
|
458
516
|
data=data,
|
|
459
517
|
)
|
|
460
|
-
|
|
461
|
-
return
|
|
518
|
+
with Session(self.engine) as session:
|
|
519
|
+
return self._add_or_update_one(
|
|
462
520
|
session,
|
|
463
521
|
q.get_bill(cups, type_, data.datetime),
|
|
464
522
|
record,
|
|
465
523
|
override=["complete", "confhash"],
|
|
466
524
|
)
|
|
467
525
|
|
|
468
|
-
|
|
526
|
+
@_in_db_thread
|
|
527
|
+
def add_bill_list(
|
|
469
528
|
self,
|
|
470
529
|
cups: str,
|
|
471
530
|
type_: typing.Literal["hour", "day", "month"],
|
|
@@ -475,8 +534,8 @@ class EdataDB:
|
|
|
475
534
|
) -> None:
|
|
476
535
|
"""Add or update a list of bill records."""
|
|
477
536
|
|
|
478
|
-
|
|
479
|
-
|
|
537
|
+
self._ensure_tables()
|
|
538
|
+
with Session(self.engine) as session:
|
|
480
539
|
unique_map = {item.datetime: item for item in bill}
|
|
481
540
|
unique = list(unique_map.values())
|
|
482
541
|
rows = [
|
|
@@ -490,51 +549,55 @@ class EdataDB:
|
|
|
490
549
|
}
|
|
491
550
|
for x in unique
|
|
492
551
|
]
|
|
493
|
-
|
|
552
|
+
self._add_or_update_many(
|
|
494
553
|
session, BillModel, rows, override=["complete", "confhash"]
|
|
495
554
|
)
|
|
496
555
|
|
|
497
|
-
|
|
556
|
+
@_in_db_thread
|
|
557
|
+
def clear_bills(self, cups: str, since: datetime | None = None) -> None:
|
|
498
558
|
"""Delete bill records for a cups, optionally only from a datetime onwards."""
|
|
499
559
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
560
|
+
self._ensure_tables()
|
|
561
|
+
with Session(self.engine) as session:
|
|
562
|
+
session.exec(q.delete_bill(cups, since)) # type: ignore[call-overload]
|
|
563
|
+
session.commit()
|
|
504
564
|
|
|
505
|
-
|
|
565
|
+
@_in_db_thread
|
|
566
|
+
def list_supplies(self) -> typing.Sequence[SupplyModel]:
|
|
506
567
|
"""List all supply records."""
|
|
507
568
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
result =
|
|
569
|
+
self._ensure_tables()
|
|
570
|
+
with Session(self.engine) as session:
|
|
571
|
+
result = session.exec(q.list_supply())
|
|
511
572
|
return result.all()
|
|
512
573
|
|
|
513
|
-
|
|
574
|
+
@_in_db_thread
|
|
575
|
+
def list_contracts(
|
|
514
576
|
self, cups: str | None = None
|
|
515
577
|
) -> typing.Sequence[ContractModel]:
|
|
516
578
|
"""List all contract records."""
|
|
517
579
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
result =
|
|
580
|
+
self._ensure_tables()
|
|
581
|
+
with Session(self.engine) as session:
|
|
582
|
+
result = session.exec(q.list_contract(cups))
|
|
521
583
|
return result.all()
|
|
522
584
|
|
|
523
|
-
|
|
585
|
+
def _list_data(self, query: SelectOfScalar, model: type[SQLModel]) -> list:
|
|
524
586
|
"""Return only the ``data`` payload of the rows selected by ``query``.
|
|
525
587
|
|
|
526
588
|
Skips building an ORM instance per row, which nearly halves the cost of
|
|
527
589
|
reading a month of hourly records.
|
|
528
590
|
"""
|
|
529
591
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
result =
|
|
592
|
+
self._ensure_tables()
|
|
593
|
+
with Session(self.engine) as session:
|
|
594
|
+
result = session.exec(
|
|
533
595
|
query.with_only_columns(model.data) # type: ignore[attr-defined]
|
|
534
596
|
)
|
|
535
597
|
return list(result.all())
|
|
536
598
|
|
|
537
|
-
|
|
599
|
+
@_in_db_thread
|
|
600
|
+
def list_energy_data(
|
|
538
601
|
self,
|
|
539
602
|
cups: str,
|
|
540
603
|
date_from: datetime | None = None,
|
|
@@ -542,20 +605,22 @@ class EdataDB:
|
|
|
542
605
|
) -> list[Energy]:
|
|
543
606
|
"""List the energy data (without row metadata)."""
|
|
544
607
|
|
|
545
|
-
return
|
|
608
|
+
return self._list_data(
|
|
546
609
|
q.list_energy(cups, date_from, date_to), EnergyModel
|
|
547
610
|
)
|
|
548
611
|
|
|
549
|
-
|
|
612
|
+
@_in_db_thread
|
|
613
|
+
def list_pvpc_data(
|
|
550
614
|
self,
|
|
551
615
|
date_from: datetime | None = None,
|
|
552
616
|
date_to: datetime | None = None,
|
|
553
617
|
) -> list[EnergyPrice]:
|
|
554
618
|
"""List the pvpc data (without row metadata)."""
|
|
555
619
|
|
|
556
|
-
return
|
|
620
|
+
return self._list_data(q.list_pvpc(date_from, date_to), PVPCModel)
|
|
557
621
|
|
|
558
|
-
|
|
622
|
+
@_in_db_thread
|
|
623
|
+
def list_bill_data(
|
|
559
624
|
self,
|
|
560
625
|
cups: str,
|
|
561
626
|
type_: typing.Literal["hour", "day", "month"],
|
|
@@ -564,11 +629,12 @@ class EdataDB:
|
|
|
564
629
|
) -> list[Bill]:
|
|
565
630
|
"""List the bill data (without row metadata)."""
|
|
566
631
|
|
|
567
|
-
return
|
|
632
|
+
return self._list_data(
|
|
568
633
|
q.list_bill(cups, type_, date_from, date_to), BillModel
|
|
569
634
|
)
|
|
570
635
|
|
|
571
|
-
|
|
636
|
+
@_in_db_thread
|
|
637
|
+
def list_energy(
|
|
572
638
|
self,
|
|
573
639
|
cups: str,
|
|
574
640
|
date_from: datetime | None = None,
|
|
@@ -576,12 +642,13 @@ class EdataDB:
|
|
|
576
642
|
) -> typing.Sequence[EnergyModel]:
|
|
577
643
|
"""List energy records."""
|
|
578
644
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
result =
|
|
645
|
+
self._ensure_tables()
|
|
646
|
+
with Session(self.engine) as session:
|
|
647
|
+
result = session.exec(q.list_energy(cups, date_from, date_to))
|
|
582
648
|
return result.all()
|
|
583
649
|
|
|
584
|
-
|
|
650
|
+
@_in_db_thread
|
|
651
|
+
def list_power(
|
|
585
652
|
self,
|
|
586
653
|
cups: str,
|
|
587
654
|
date_from: datetime | None = None,
|
|
@@ -589,24 +656,26 @@ class EdataDB:
|
|
|
589
656
|
) -> typing.Sequence[PowerModel]:
|
|
590
657
|
"""List power records."""
|
|
591
658
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
result =
|
|
659
|
+
self._ensure_tables()
|
|
660
|
+
with Session(self.engine) as session:
|
|
661
|
+
result = session.exec(q.list_power(cups, date_from, date_to))
|
|
595
662
|
return result.all()
|
|
596
663
|
|
|
597
|
-
|
|
664
|
+
@_in_db_thread
|
|
665
|
+
def list_pvpc(
|
|
598
666
|
self,
|
|
599
667
|
date_from: datetime | None = None,
|
|
600
668
|
date_to: datetime | None = None,
|
|
601
669
|
) -> typing.Sequence[PVPCModel]:
|
|
602
670
|
"""List pvpc records."""
|
|
603
671
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
result =
|
|
672
|
+
self._ensure_tables()
|
|
673
|
+
with Session(self.engine) as session:
|
|
674
|
+
result = session.exec(q.list_pvpc(date_from, date_to))
|
|
607
675
|
return result.all()
|
|
608
676
|
|
|
609
|
-
|
|
677
|
+
@_in_db_thread
|
|
678
|
+
def list_statistics(
|
|
610
679
|
self,
|
|
611
680
|
cups: str,
|
|
612
681
|
type_: typing.Literal["day", "month"],
|
|
@@ -616,14 +685,15 @@ class EdataDB:
|
|
|
616
685
|
) -> typing.Sequence[StatisticsModel]:
|
|
617
686
|
"""List statistics records filtered by type ('day' or 'month') and date range."""
|
|
618
687
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
result =
|
|
688
|
+
self._ensure_tables()
|
|
689
|
+
with Session(self.engine) as session:
|
|
690
|
+
result = session.exec(
|
|
622
691
|
q.list_statistics(cups, type_, date_from, date_to, complete)
|
|
623
692
|
)
|
|
624
693
|
return result.all()
|
|
625
694
|
|
|
626
|
-
|
|
695
|
+
@_in_db_thread
|
|
696
|
+
def list_bill(
|
|
627
697
|
self,
|
|
628
698
|
cups: str,
|
|
629
699
|
type_: typing.Literal["hour", "day", "month"],
|
|
@@ -633,9 +703,9 @@ class EdataDB:
|
|
|
633
703
|
) -> typing.Sequence[BillModel]:
|
|
634
704
|
"""List bill records filtered by type ('hour', 'day' or 'month') and date range."""
|
|
635
705
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
result =
|
|
706
|
+
self._ensure_tables()
|
|
707
|
+
with Session(self.engine) as session:
|
|
708
|
+
result = session.exec(
|
|
639
709
|
q.list_bill(cups, type_, date_from, date_to, complete)
|
|
640
710
|
)
|
|
641
711
|
return result.all()
|