e-data 2.0.0.dev121__py3-none-any.whl → 2.0.0.dev125__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.dev125
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.dev125.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=1oIsQFlepCvpcR1T2WRhgjPafRCoXtvKFcL6eU1EM18,16945
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=K8Jx_LpFAqenupaF0I58whpo5OGUaSWBhN4MwNA2TGc,6179
25
+ e_data-2.0.0.dev125.dist-info/METADATA,sha256=jaRZ4ufDfQhJinChybMh0kJEEYCaIdlG-n3b89hd_T0,48075
26
+ e_data-2.0.0.dev125.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
27
+ e_data-2.0.0.dev125.dist-info/top_level.txt,sha256=Ez-fReWtUVTMcFuH0dzCnoKIgMBHgjLaBuba8QiKC7g,6
28
+ e_data-2.0.0.dev125.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
 
@@ -154,3 +154,60 @@ async def test_bill_service(
154
154
  assert len(monthly_stats) > 0
155
155
 
156
156
  assert monthly_stats == snapshot
157
+
158
+
159
+ @pytest.mark.asyncio
160
+ async def test_simulate_does_not_persist(
161
+ populated_data_service, snapshot: SnapshotAssertion, storage_dir
162
+ ):
163
+ bs = BillService("ESXXXXXXXXXXXXXXXXTEST", storage_path=storage_dir)
164
+
165
+ before = len(await bs.get_bills())
166
+
167
+ rules = BillingRules(
168
+ p1_kwh_eur=0.30,
169
+ p2_kwh_eur=0.30,
170
+ p3_kwh_eur=0.30,
171
+ p1_kw_year_eur=20,
172
+ p2_kw_year_eur=10,
173
+ )
174
+ simulated = await bs.simulate(billing_rules=rules, is_pvpc=False)
175
+
176
+ after = len(await bs.get_bills())
177
+
178
+ assert len(simulated) > 0
179
+ assert before == after
180
+
181
+ summary = {
182
+ "count": len(simulated),
183
+ "value_eur": round(sum(x.value_eur for x in simulated), 6),
184
+ "energy_term": round(sum(x.energy_term for x in simulated), 6),
185
+ "power_term": round(sum(x.power_term for x in simulated), 6),
186
+ "first": simulated[0],
187
+ "last": simulated[-1],
188
+ }
189
+ assert summary == snapshot
190
+
191
+
192
+ @pytest.mark.asyncio
193
+ async def test_clear_bills(populated_data_service, energy, storage_dir):
194
+ bs = BillService("ESXXXXXXXXXXXXXXXXTEST", storage_path=storage_dir)
195
+
196
+ all_energy = sorted(energy, key=lambda x: x.datetime)
197
+ rules = BillingRules(
198
+ p1_kwh_eur=0.20,
199
+ p2_kwh_eur=0.20,
200
+ p3_kwh_eur=0.20,
201
+ p1_kw_year_eur=20,
202
+ p2_kw_year_eur=10,
203
+ )
204
+ await bs.update(
205
+ all_energy[0].datetime, all_energy[-1].datetime, rules, is_pvpc=False
206
+ )
207
+ assert len(await bs.get_bills()) > 0
208
+
209
+ await bs.clear_bills()
210
+
211
+ assert len(await bs.get_bills("hour")) == 0
212
+ assert len(await bs.get_bills(type_="day")) == 0
213
+ assert len(await bs.get_bills(type_="month")) == 0