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.
@@ -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 sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
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: AsyncEngine | None = None
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+aiosqlite:////{os.path.abspath(sqlite_path)}"
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 = create_async_engine(db_url, future=True)
95
- event.listen(cls._engine.sync_engine, "connect", _set_sqlite_pragmas)
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._tables_lock = asyncio.Lock()
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) -> AsyncEngine | None:
104
- """Return the async database engine."""
144
+ def engine(self) -> Engine | None:
145
+ """Return the database engine."""
105
146
 
106
147
  return self._engine
107
148
 
108
- async def _ensure_tables(self) -> None:
109
- """Create tables if not already created (lazy init)."""
149
+ def _ensure_tables(self) -> None:
150
+ """Create tables and missing indexes if not already done (lazy init).
110
151
 
111
- if self._tables_initialized:
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
- # concurrent first calls (e.g. several websocket requests right after
114
- # startup) would otherwise all see the index missing and race to create
115
- # it, failing with "index ... already exists"
116
- async with self._tables_lock:
117
- if self._tables_initialized or not self.engine:
118
- return
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: AsyncSession,
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
- await session.commit()
135
- await session.refresh(record)
173
+ session.commit()
174
+ session.refresh(record)
136
175
  else:
137
- await session.flush()
176
+ session.flush()
138
177
  return record
139
178
 
140
- async def _update_one(
179
+ def _update_one(
141
180
  self,
142
- session: AsyncSession,
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 = await session.exec(query)
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
- await session.commit()
160
- await session.refresh(existing)
198
+ session.commit()
199
+ session.refresh(existing)
161
200
  else:
162
- await session.flush()
201
+ session.flush()
163
202
  return existing
164
203
 
165
- async def _add_or_update_one(
204
+ def _add_or_update_one(
166
205
  self,
167
- session: AsyncSession,
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
- async with session.begin_nested():
215
+ with session.begin_nested():
177
216
  session.add(record)
178
- await session.flush()
217
+ session.flush()
179
218
  if commit:
180
- await session.commit()
181
- await session.refresh(record)
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 await self._update_one(
230
+ return self._update_one(
192
231
  session, query, new_data, commit=commit, overrides=override_dict
193
232
  )
194
233
 
195
- async def _add_or_update_many(
234
+ def _add_or_update_many(
196
235
  self,
197
- session: AsyncSession,
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 = await session.connection()
236
- await connection.execute(stmt, rows)
237
- await session.commit()
274
+ connection = session.connection()
275
+ connection.execute(stmt, rows)
276
+ session.commit()
238
277
 
239
- async def get_supply(self, cups: str) -> SupplyModel | None:
278
+ @_in_db_thread
279
+ def get_supply(self, cups: str) -> SupplyModel | None:
240
280
  """Get a supply record by cups."""
241
281
 
242
- await self._ensure_tables()
243
- async with AsyncSession(self.engine) as session:
244
- result = await session.exec(q.get_supply(cups))
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
- async def get_contract(
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
- await self._ensure_tables()
253
- async with AsyncSession(self.engine) as session:
254
- result = await session.exec(q.get_contract(cups, date_start))
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
- async def get_last_energy(self, cups: str) -> EnergyModel | None:
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
- await self._ensure_tables()
261
- async with AsyncSession(self.engine) as session:
262
- result = await session.exec(q.get_last_energy(cups))
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
- async def get_last_power(self, cups: str) -> PowerModel | None:
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
- await self._ensure_tables()
269
- async with AsyncSession(self.engine) as session:
270
- result = await session.exec(q.get_last_power(cups))
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
- async def get_last_pvpc(self) -> PVPCModel | None:
316
+ @_in_db_thread
317
+ def get_last_pvpc(self) -> PVPCModel | None:
274
318
  """Get the most recent pvpc."""
275
319
 
276
- await self._ensure_tables()
277
- async with AsyncSession(self.engine) as session:
278
- result = await session.exec(q.get_last_pvpc())
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
- async def get_last_bill(self, cups: str) -> BillModel | None:
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
- await self._ensure_tables()
285
- async with AsyncSession(self.engine) as session:
286
- result = await session.exec(q.get_last_bill(cups))
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
- async def get_last_complete_statistic(
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
- await self._ensure_tables()
295
- async with AsyncSession(self.engine) as session:
296
- result = await session.exec(q.get_last_complete_statistic(cups, type_))
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
- async def get_last_complete_bill(
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
- await self._ensure_tables()
305
- async with AsyncSession(self.engine) as session:
306
- result = await session.exec(q.get_last_complete_bill(cups, type_))
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
- async def add_contract(self, cups: str, contract: Contract) -> ContractModel | None:
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
- await self._ensure_tables()
360
+ self._ensure_tables()
313
361
  record = ContractModel(cups=cups, date_start=contract.date_start, data=contract)
314
- async with AsyncSession(self.engine) as session:
315
- return await self._add_or_update_one(
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
- async def add_supply(self, supply: Supply) -> SupplyModel | None:
367
+ @_in_db_thread
368
+ def add_supply(self, supply: Supply) -> SupplyModel | None:
320
369
  """Add or update a supply record."""
321
370
 
322
- await self._ensure_tables()
371
+ self._ensure_tables()
323
372
  record = SupplyModel(cups=supply.cups, data=supply)
324
- async with AsyncSession(self.engine) as session:
325
- return await self._add_or_update_one(
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
- async def add_energy(self, cups: str, energy: Energy) -> EnergyModel | None:
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
- await self._ensure_tables()
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
- async with AsyncSession(self.engine) as session:
337
- return await self._add_or_update_one(
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
- async def add_energy_list(self, cups: str, energy: list[Energy]) -> None:
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
- await self._ensure_tables()
345
- async with AsyncSession(self.engine) as session:
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
- await self._add_or_update_many(session, EnergyModel, rows)
403
+ self._add_or_update_many(session, EnergyModel, rows)
353
404
 
354
- async def add_power(self, cups: str, power: Power) -> PowerModel | None:
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
- await self._ensure_tables()
409
+ self._ensure_tables()
358
410
  record = PowerModel(cups=cups, datetime=power.datetime, data=power)
359
- async with AsyncSession(self.engine) as session:
360
- return await self._add_or_update_one(
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
- async def add_power_list(self, cups: str, power: list[Power]) -> None:
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
- await self._ensure_tables()
368
- async with AsyncSession(self.engine) as session:
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
- await self._add_or_update_many(session, PowerModel, rows)
425
+ self._add_or_update_many(session, PowerModel, rows)
373
426
 
374
- async def add_pvpc(self, pvpc: EnergyPrice) -> PVPCModel | None:
427
+ @_in_db_thread
428
+ def add_pvpc(self, pvpc: EnergyPrice) -> PVPCModel | None:
375
429
  """Add or update a pvpc record."""
376
430
 
377
- await self._ensure_tables()
431
+ self._ensure_tables()
378
432
  record = PVPCModel(datetime=pvpc.datetime, data=pvpc)
379
- async with AsyncSession(self.engine) as session:
380
- return await self._add_or_update_one(
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
- async def add_pvpc_list(self, pvpc: list[EnergyPrice]) -> None:
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
- await self._ensure_tables()
388
- async with AsyncSession(self.engine) as session:
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
- await self._add_or_update_many(session, PVPCModel, rows)
447
+ self._add_or_update_many(session, PVPCModel, rows)
393
448
 
394
- async def add_statistics(
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
- await self._ensure_tables()
459
+ self._ensure_tables()
404
460
  record = StatisticsModel(
405
461
  cups=cups, datetime=data.datetime, type=type_, data=data, complete=complete
406
462
  )
407
- async with AsyncSession(self.engine) as session:
408
- return await self._add_or_update_one(
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
- async def add_statistics_list(
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
- await self._ensure_tables()
425
- async with AsyncSession(self.engine) as session:
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
- await self._add_or_update_many(
494
+ self._add_or_update_many(
438
495
  session, StatisticsModel, rows, override=["complete"]
439
496
  )
440
497
 
441
- async def add_bill(
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
- await self._ensure_tables()
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
- async with AsyncSession(self.engine) as session:
461
- return await self._add_or_update_one(
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
- async def add_bill_list(
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
- await self._ensure_tables()
479
- async with AsyncSession(self.engine) as session:
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
- await self._add_or_update_many(
552
+ self._add_or_update_many(
494
553
  session, BillModel, rows, override=["complete", "confhash"]
495
554
  )
496
555
 
497
- async def clear_bills(self, cups: str, since: datetime | None = None) -> None:
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
- await self._ensure_tables()
501
- async with AsyncSession(self.engine) as session:
502
- await session.exec(q.delete_bill(cups, since)) # type: ignore[call-overload]
503
- await session.commit()
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
- async def list_supplies(self) -> typing.Sequence[SupplyModel]:
565
+ @_in_db_thread
566
+ def list_supplies(self) -> typing.Sequence[SupplyModel]:
506
567
  """List all supply records."""
507
568
 
508
- await self._ensure_tables()
509
- async with AsyncSession(self.engine) as session:
510
- result = await session.exec(q.list_supply())
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
- async def list_contracts(
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
- await self._ensure_tables()
519
- async with AsyncSession(self.engine) as session:
520
- result = await session.exec(q.list_contract(cups))
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
- async def _list_data(self, query: SelectOfScalar, model: type[SQLModel]) -> list:
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
- await self._ensure_tables()
531
- async with AsyncSession(self.engine) as session:
532
- result = await session.exec(
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
- async def list_energy_data(
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 await self._list_data(
608
+ return self._list_data(
546
609
  q.list_energy(cups, date_from, date_to), EnergyModel
547
610
  )
548
611
 
549
- async def list_pvpc_data(
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 await self._list_data(q.list_pvpc(date_from, date_to), PVPCModel)
620
+ return self._list_data(q.list_pvpc(date_from, date_to), PVPCModel)
557
621
 
558
- async def list_bill_data(
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 await self._list_data(
632
+ return self._list_data(
568
633
  q.list_bill(cups, type_, date_from, date_to), BillModel
569
634
  )
570
635
 
571
- async def list_energy(
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
- await self._ensure_tables()
580
- async with AsyncSession(self.engine) as session:
581
- result = await session.exec(q.list_energy(cups, date_from, date_to))
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
- async def list_power(
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
- await self._ensure_tables()
593
- async with AsyncSession(self.engine) as session:
594
- result = await session.exec(q.list_power(cups, date_from, date_to))
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
- async def list_pvpc(
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
- await self._ensure_tables()
605
- async with AsyncSession(self.engine) as session:
606
- result = await session.exec(q.list_pvpc(date_from, date_to))
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
- async def list_statistics(
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
- await self._ensure_tables()
620
- async with AsyncSession(self.engine) as session:
621
- result = await session.exec(
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
- async def list_bill(
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
- await self._ensure_tables()
637
- async with AsyncSession(self.engine) as session:
638
- result = await session.exec(
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()