e-data 2.0.0.dev121__py3-none-any.whl → 2.0.0.dev126__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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: e-data
3
- Version: 2.0.0.dev121
3
+ Version: 2.0.0.dev126
4
4
  Summary: Python library for managing spanish energy data from various web providers
5
5
  Author-email: VMG <vmayorg@outlook.es>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -1,13 +1,13 @@
1
- e_data-2.0.0.dev121.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
1
+ e_data-2.0.0.dev126.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
2
2
  edata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  edata/cli.py,sha256=0pmlaaXPVfbxPVHMovsuK9qf8PGXl38AV3gtIsk95O4,4010
4
4
  edata/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  edata/core/const.py,sha256=X9J2f7lt9jXJnghqz0JFi-J7USa3xlYvbuiki70eHNE,43
6
6
  edata/core/utils.py,sha256=P4vEPVnymvBp8W2rwwc_XMvd8_6Vx6N0GKnVx0WxE0I,1938
7
7
  edata/database/__init__.py,sha256=HP_pDbGLzeMY9Ufm_Dps5cxzW_3YTonT8w8dstG558c,46
8
- edata/database/controller.py,sha256=_KQJALolxUTf5G405USCoSDxYebg2819aYYgiTZF1kU,16358
8
+ edata/database/controller.py,sha256=T3fsW-2Y3NTz9eOVVQjHvYqdMHUg0QmcJseLXALF_tQ,16748
9
9
  edata/database/models.py,sha256=4rlRpPErULqYr6QJxZar1NBpJPWTcHftDN1MB3bwGyA,5096
10
- edata/database/queries.py,sha256=kGoTWs0VJ4KjMJqBbhQGUeio55sZAysKABYGBUUGBE8,6687
10
+ edata/database/queries.py,sha256=rYdJ1X_yEY9FDU4NnEFjvanOcz7glZiQ4JrKCzXWXUM,7048
11
11
  edata/database/utils.py,sha256=V-FJfyAh8YJBGHB85BbWtG3Ilq09pM9Gl-L86TffB4g,1062
12
12
  edata/models/__init__.py,sha256=nMPPPkGjUsF51PBFlChgHyb5C3aQiccTRiVHUdJjNj4,153
13
13
  edata/models/bill.py,sha256=-qZ4l9f8BnPFlykFRoNYnmRe6-ELWmuNh6usks_k4X0,2674
@@ -16,13 +16,13 @@ edata/models/supply.py,sha256=qM0Q22-7PfBN93-Tu3ljAE58p5gfob0YWw-o5FccjrI,878
16
16
  edata/providers/__init__.py,sha256=5xaRKPttYM6EpVE1I_vhUAF25L9pEM_qK_5Ix6xnQe0,104
17
17
  edata/providers/datadis.py,sha256=a084ArY83ncvrMQvMSD2BY1rHlhp42dwJJkU8avmleo,20137
18
18
  edata/providers/redata.py,sha256=uvxO2TTtSNSfPRWHZifLjbaP7zAr6O7CZ1oRQFNbIhQ,3122
19
- edata/services/bill_service.py,sha256=bjBIL1jl0Lcb2TG5VLKZgqT55PXdMPtEQW6fIPjWrVk,13411
20
- edata/services/data_service.py,sha256=65w-fVllJY8GFM9eUZ7uVF_XK-QgKeYl4wfrqBFpXjE,16772
19
+ edata/services/bill_service.py,sha256=cZkf-CVA-1I1l1vasB8waVGbS4FkODL_W9x9H_DoXG0,14632
20
+ edata/services/data_service.py,sha256=vLm2LuWTiJgWXYOQUjwaTvkEWTo0gCt6nCkUUtVIIQ0,17120
21
21
  edata/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  edata/tests/test_datadis_connector.py,sha256=O3mjI5uIOgFz3PzQsApcG6J2yXd8rlEtj8By-9dsDbg,9270
23
23
  edata/tests/test_redata_connector.py,sha256=SBvxsma5t-qvz2drxNisNM2P1STTurqEs1TSDgwaPJ8,508
24
- edata/tests/test_services.py,sha256=XiwppLWqxm5vjgRtNmBeDFUaV5vVcQxerGgjnBJgmmk,4521
25
- e_data-2.0.0.dev121.dist-info/METADATA,sha256=hhW170QW1yry_sAqkesWyOXXXKjbZm3ZvdvYgafEatg,48075
26
- e_data-2.0.0.dev121.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
27
- e_data-2.0.0.dev121.dist-info/top_level.txt,sha256=Ez-fReWtUVTMcFuH0dzCnoKIgMBHgjLaBuba8QiKC7g,6
28
- e_data-2.0.0.dev121.dist-info/RECORD,,
24
+ edata/tests/test_services.py,sha256=xR_BxNttWzoEGXh02W6Iyy_QQ9odzlLXj7X9nVXd5Co,6889
25
+ e_data-2.0.0.dev126.dist-info/METADATA,sha256=lYEx8a9kPp2tiOPuEVT8OmBggtsEjgluiJACOuFO4K8,48075
26
+ e_data-2.0.0.dev126.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
27
+ e_data-2.0.0.dev126.dist-info/top_level.txt,sha256=Ez-fReWtUVTMcFuH0dzCnoKIgMBHgjLaBuba8QiKC7g,6
28
+ e_data-2.0.0.dev126.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.1)
2
+ Generator: setuptools (83.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -374,6 +374,14 @@ class EdataDB:
374
374
  session, queries, items, override=["complete", "confhash"]
375
375
  )
376
376
 
377
+ async def clear_bills(self, cups: str, since: datetime | None = None) -> None:
378
+ """Delete bill records for a cups, optionally only from a datetime onwards."""
379
+
380
+ await self._ensure_tables()
381
+ async with AsyncSession(self.engine) as session:
382
+ await session.exec(q.delete_bill(cups, since)) # type: ignore[call-overload]
383
+ await session.commit()
384
+
377
385
  async def list_supplies(self) -> typing.Sequence[SupplyModel]:
378
386
  """List all supply records."""
379
387
 
edata/database/queries.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import typing
2
2
  from datetime import datetime
3
3
 
4
+ from sqlalchemy import Delete, delete
4
5
  from sqlmodel import asc, desc, select
5
6
  from sqlmodel.sql.expression import SelectOfScalar
6
7
 
@@ -184,6 +185,15 @@ def get_last_bill(
184
185
  return query
185
186
 
186
187
 
188
+ def delete_bill(cups: str, date_from: datetime | None = None) -> Delete:
189
+ """Query that deletes bill records, optionally from a datetime onwards."""
190
+
191
+ query = delete(BillModel).where(BillModel.cups == cups)
192
+ if date_from is not None:
193
+ query = query.where(BillModel.datetime >= date_from)
194
+ return query
195
+
196
+
187
197
  # Queries for "pvpc" table
188
198
  def get_pvpc(datetime_: datetime) -> SelectOfScalar[PVPCModel]:
189
199
  """Query that selects a PVPC record."""
@@ -137,6 +137,39 @@ class BillService:
137
137
  await self.update_statistics(start, end)
138
138
  await self.fix_missing_statistics()
139
139
 
140
+ async def clear_bills(self, since: datetime | None = None) -> None:
141
+ """Delete stored bills for this cups, optionally only from a datetime onwards."""
142
+
143
+ await self.db.clear_bills(self._cups, since)
144
+
145
+ async def simulate(
146
+ self,
147
+ billing_rules: BillingRules | PVPCBillingRules | None = None,
148
+ is_pvpc: bool = True,
149
+ start: datetime | None = None,
150
+ end: datetime | None = None,
151
+ ) -> list[Bill]:
152
+ """Compile hourly bills for a date range without persisting them."""
153
+
154
+ if not billing_rules:
155
+ _LOGGER.info("%s non explicit billing rules, assuming PVPC", self._scups)
156
+ billing_rules = PVPCBillingRules()
157
+ is_pvpc = True
158
+
159
+ contracts = await self._get_contracts()
160
+ energy = await self._get_energy(start, end)
161
+
162
+ if is_pvpc:
163
+ pvpc = await self._get_pvpc(start, end)
164
+ billing_rules = PVPCBillingRules(**billing_rules.model_dump())
165
+ return await asyncio.to_thread(
166
+ self.simulate_pvpc, contracts, energy, pvpc, billing_rules
167
+ )
168
+
169
+ return await asyncio.to_thread(
170
+ self.simulate_custom, contracts, energy, billing_rules
171
+ )
172
+
140
173
  async def update_statistics(self, start: datetime, end: datetime):
141
174
  """Update the statistics during a period."""
142
175
 
@@ -109,6 +109,11 @@ class DataService:
109
109
  data = await self.db.list_statistics(self._cups, type_, start, end, complete)
110
110
  return [x.data for x in data]
111
111
 
112
+ async def get_last_energy_dt(self) -> datetime | None:
113
+ """Return the timestamp of the latest energy record."""
114
+
115
+ return await self._get_last_energy_dt()
116
+
112
117
  async def fix_missing_statistics(self) -> None:
113
118
  """Recompile statistics to fix missing data."""
114
119
 
@@ -311,7 +316,9 @@ class DataService:
311
316
  # end date out of bounds
312
317
  return False
313
318
  if _pvpc_dt := await self._get_last_pvpc_dt():
314
- start = _pvpc_dt + timedelta(hours=1)
319
+ # keep within the fetchable window even if the last stored price is
320
+ # older than a month, otherwise REData rejects the range with a 400
321
+ start = max(_pvpc_dt + timedelta(hours=1), min_date)
315
322
  if start >= end:
316
323
  _LOGGER.info("%s pvpc prices are already synced", self._scups)
317
324
  # data is already synced
@@ -1,3 +1,4 @@
1
+ from datetime import datetime, timedelta
1
2
  import json
2
3
  import os
3
4
  from tempfile import gettempdir
@@ -7,6 +8,7 @@ import pytest
7
8
  import pytest_asyncio
8
9
  from syrupy.assertion import SnapshotAssertion
9
10
 
11
+ from edata.core.utils import get_day
10
12
  from edata.models.bill import BillingRules
11
13
  from edata.models.data import Energy, Power
12
14
  from edata.models.supply import Contract, Supply
@@ -154,3 +156,79 @@ async def test_bill_service(
154
156
  assert len(monthly_stats) > 0
155
157
 
156
158
  assert monthly_stats == snapshot
159
+
160
+
161
+ @pytest.mark.asyncio
162
+ async def test_simulate_does_not_persist(
163
+ populated_data_service, snapshot: SnapshotAssertion, storage_dir
164
+ ):
165
+ bs = BillService("ESXXXXXXXXXXXXXXXXTEST", storage_path=storage_dir)
166
+
167
+ before = len(await bs.get_bills())
168
+
169
+ rules = BillingRules(
170
+ p1_kwh_eur=0.30,
171
+ p2_kwh_eur=0.30,
172
+ p3_kwh_eur=0.30,
173
+ p1_kw_year_eur=20,
174
+ p2_kw_year_eur=10,
175
+ )
176
+ simulated = await bs.simulate(billing_rules=rules, is_pvpc=False)
177
+
178
+ after = len(await bs.get_bills())
179
+
180
+ assert len(simulated) > 0
181
+ assert before == after
182
+
183
+ summary = {
184
+ "count": len(simulated),
185
+ "value_eur": round(sum(x.value_eur for x in simulated), 6),
186
+ "energy_term": round(sum(x.energy_term for x in simulated), 6),
187
+ "power_term": round(sum(x.power_term for x in simulated), 6),
188
+ "first": simulated[0],
189
+ "last": simulated[-1],
190
+ }
191
+ assert summary == snapshot
192
+
193
+
194
+ @pytest.mark.asyncio
195
+ async def test_clear_bills(populated_data_service, energy, storage_dir):
196
+ bs = BillService("ESXXXXXXXXXXXXXXXXTEST", storage_path=storage_dir)
197
+
198
+ all_energy = sorted(energy, key=lambda x: x.datetime)
199
+ rules = BillingRules(
200
+ p1_kwh_eur=0.20,
201
+ p2_kwh_eur=0.20,
202
+ p3_kwh_eur=0.20,
203
+ p1_kw_year_eur=20,
204
+ p2_kw_year_eur=10,
205
+ )
206
+ await bs.update(
207
+ all_energy[0].datetime, all_energy[-1].datetime, rules, is_pvpc=False
208
+ )
209
+ assert len(await bs.get_bills()) > 0
210
+
211
+ await bs.clear_bills()
212
+
213
+ assert len(await bs.get_bills("hour")) == 0
214
+ assert len(await bs.get_bills(type_="day")) == 0
215
+ assert len(await bs.get_bills(type_="month")) == 0
216
+
217
+
218
+ @pytest.mark.asyncio
219
+ async def test_update_pvpc_clamps_range_to_min_date(populated_data_service):
220
+ ds = populated_data_service
221
+ now = datetime.now()
222
+ stale_dt = now - timedelta(days=90)
223
+
224
+ with (
225
+ patch.object(ds, "_get_last_pvpc_dt", AsyncMock(return_value=stale_dt)),
226
+ patch.object(
227
+ ds.redata, "async_get_realtime_prices", AsyncMock(return_value=[])
228
+ ) as mock_fetch,
229
+ ):
230
+ await ds.update_pvpc(now - timedelta(days=365), now)
231
+
232
+ mock_fetch.assert_awaited_once()
233
+ called_start = mock_fetch.await_args.args[0]
234
+ assert called_start >= get_day(now) - timedelta(days=28)