e-data 2.0.0.dev112__py3-none-any.whl → 2.0.0.dev120__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.0.dev112.dist-info → e_data-2.0.0.dev120.dist-info}/METADATA +3 -3
- e_data-2.0.0.dev120.dist-info/RECORD +28 -0
- {e_data-2.0.0.dev112.dist-info → e_data-2.0.0.dev120.dist-info}/WHEEL +1 -1
- edata/cli.py +8 -8
- edata/core/utils.py +1 -1
- edata/database/controller.py +17 -15
- edata/database/queries.py +3 -1
- edata/database/utils.py +3 -4
- edata/models/bill.py +5 -5
- edata/models/data.py +66 -14
- edata/models/supply.py +14 -5
- edata/providers/datadis.py +44 -46
- edata/providers/redata.py +2 -2
- edata/services/bill_service.py +15 -14
- edata/services/data_service.py +35 -34
- edata/tests/test_datadis_connector.py +85 -75
- e_data-2.0.0.dev112.dist-info/RECORD +0 -28
- {e_data-2.0.0.dev112.dist-info → e_data-2.0.0.dev120.dist-info}/licenses/LICENSE +0 -0
- {e_data-2.0.0.dev112.dist-info → e_data-2.0.0.dev120.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: e-data
|
|
3
|
-
Version: 2.0.0.
|
|
3
|
+
Version: 2.0.0.dev120
|
|
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
|
|
@@ -823,7 +823,7 @@ async def main():
|
|
|
823
823
|
# Mostrar resumen
|
|
824
824
|
total_cost = sum(b.value_eur for b in bills)
|
|
825
825
|
print(f"Coste total: {total_cost:.2f} €")
|
|
826
|
-
print(f"Consumo total: {sum(e.
|
|
826
|
+
print(f"Consumo total: {sum(e.value_kwh for e in energy):.2f} kWh")
|
|
827
827
|
|
|
828
828
|
# Ejecutar
|
|
829
829
|
asyncio.run(main())
|
|
@@ -841,7 +841,7 @@ python -m edata.cli show-supplies <username>
|
|
|
841
841
|
python -m edata.cli download-all --cups ES0000000000000000XX <username>
|
|
842
842
|
|
|
843
843
|
# Actualizar facturación con tarifa fija
|
|
844
|
-
python -m edata.cli update-bill \
|
|
844
|
+
python -m edata.cli update-custom-bill \
|
|
845
845
|
--cups ES0000000000000000XX \
|
|
846
846
|
--p1-kw-year-eur 30.67 \
|
|
847
847
|
--p2-kw-year-eur 1.42 \
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
e_data-2.0.0.dev120.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
2
|
+
edata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
edata/cli.py,sha256=0pmlaaXPVfbxPVHMovsuK9qf8PGXl38AV3gtIsk95O4,4010
|
|
4
|
+
edata/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
edata/core/const.py,sha256=X9J2f7lt9jXJnghqz0JFi-J7USa3xlYvbuiki70eHNE,43
|
|
6
|
+
edata/core/utils.py,sha256=P4vEPVnymvBp8W2rwwc_XMvd8_6Vx6N0GKnVx0WxE0I,1938
|
|
7
|
+
edata/database/__init__.py,sha256=HP_pDbGLzeMY9Ufm_Dps5cxzW_3YTonT8w8dstG558c,46
|
|
8
|
+
edata/database/controller.py,sha256=_KQJALolxUTf5G405USCoSDxYebg2819aYYgiTZF1kU,16358
|
|
9
|
+
edata/database/models.py,sha256=4rlRpPErULqYr6QJxZar1NBpJPWTcHftDN1MB3bwGyA,5096
|
|
10
|
+
edata/database/queries.py,sha256=kGoTWs0VJ4KjMJqBbhQGUeio55sZAysKABYGBUUGBE8,6687
|
|
11
|
+
edata/database/utils.py,sha256=V-FJfyAh8YJBGHB85BbWtG3Ilq09pM9Gl-L86TffB4g,1062
|
|
12
|
+
edata/models/__init__.py,sha256=nMPPPkGjUsF51PBFlChgHyb5C3aQiccTRiVHUdJjNj4,153
|
|
13
|
+
edata/models/bill.py,sha256=-qZ4l9f8BnPFlykFRoNYnmRe6-ELWmuNh6usks_k4X0,2674
|
|
14
|
+
edata/models/data.py,sha256=O1aj0H_G92mnJjm6mYhC8fnl8ZOoatGiokRf8b7ziD8,2388
|
|
15
|
+
edata/models/supply.py,sha256=qM0Q22-7PfBN93-Tu3ljAE58p5gfob0YWw-o5FccjrI,878
|
|
16
|
+
edata/providers/__init__.py,sha256=5xaRKPttYM6EpVE1I_vhUAF25L9pEM_qK_5Ix6xnQe0,104
|
|
17
|
+
edata/providers/datadis.py,sha256=ChV7IDO2Rc9YxpwQ-b9Scq527i_z2z2UiQa52mxZYmA,20143
|
|
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
|
|
21
|
+
edata/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
edata/tests/test_datadis_connector.py,sha256=O3mjI5uIOgFz3PzQsApcG6J2yXd8rlEtj8By-9dsDbg,9270
|
|
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.dev120.dist-info/METADATA,sha256=YZ4hqEryOAJoiurPpRmvZ-CQ2F11Qxu4RS8XwOk5_u8,48075
|
|
26
|
+
e_data-2.0.0.dev120.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
27
|
+
e_data-2.0.0.dev120.dist-info/top_level.txt,sha256=Ez-fReWtUVTMcFuH0dzCnoKIgMBHgjLaBuba8QiKC7g,6
|
|
28
|
+
e_data-2.0.0.dev120.dist-info/RECORD,,
|
edata/cli.py
CHANGED
|
@@ -17,7 +17,7 @@ app = typer.Typer()
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
@app.command()
|
|
20
|
-
def show_supplies(username: str):
|
|
20
|
+
def show_supplies(username: str) -> None:
|
|
21
21
|
"""Show supplies and contracts for a given datadis user."""
|
|
22
22
|
|
|
23
23
|
password = getpass()
|
|
@@ -28,7 +28,7 @@ def show_supplies(username: str):
|
|
|
28
28
|
typer.echo(supply.model_dump_json())
|
|
29
29
|
if supplies:
|
|
30
30
|
cups = supplies[0].cups
|
|
31
|
-
distributor = supplies[0].
|
|
31
|
+
distributor = supplies[0].distributor_code
|
|
32
32
|
contracts = connector.get_contract_detail(cups, distributor)
|
|
33
33
|
typer.echo("\nContracts:")
|
|
34
34
|
for contract in contracts:
|
|
@@ -41,7 +41,7 @@ async def _download_all(
|
|
|
41
41
|
nif: str,
|
|
42
42
|
cups: str,
|
|
43
43
|
authorized_nif: str | None = None,
|
|
44
|
-
):
|
|
44
|
+
) -> None:
|
|
45
45
|
"""Download all data for a given datadis account and CUPS."""
|
|
46
46
|
|
|
47
47
|
password = getpass()
|
|
@@ -63,7 +63,7 @@ def download_all(
|
|
|
63
63
|
nif: str,
|
|
64
64
|
cups: Annotated[str, typer.Option(help="The identifier of the Supply")],
|
|
65
65
|
authorized_nif: str | None = None,
|
|
66
|
-
):
|
|
66
|
+
) -> None:
|
|
67
67
|
"""Download all data for a given datadis account and CUPS."""
|
|
68
68
|
|
|
69
69
|
asyncio.run(_download_all(nif, cups, authorized_nif))
|
|
@@ -77,7 +77,7 @@ async def _update_custom_bill(
|
|
|
77
77
|
p2_kwh_eur: float,
|
|
78
78
|
p3_kwh_eur: float,
|
|
79
79
|
meter_month_eur: float,
|
|
80
|
-
):
|
|
80
|
+
) -> None:
|
|
81
81
|
"""Download all data for a given datadis account and CUPS."""
|
|
82
82
|
|
|
83
83
|
bs = BillService(cups, storage_path="./edata_cli")
|
|
@@ -105,7 +105,7 @@ def update_custom_bill(
|
|
|
105
105
|
p2_kwh_eur: Annotated[float, typer.Option(help="Price per kWh at P2 tariff")],
|
|
106
106
|
p3_kwh_eur: Annotated[float, typer.Option(help="Price per kWh at P3 tariff")],
|
|
107
107
|
meter_month_eur: Annotated[float, typer.Option(help="Monthly cost of the meter")],
|
|
108
|
-
):
|
|
108
|
+
) -> None:
|
|
109
109
|
"""Download all data for a given datadis account and CUPS."""
|
|
110
110
|
|
|
111
111
|
asyncio.run(
|
|
@@ -121,7 +121,7 @@ def update_custom_bill(
|
|
|
121
121
|
)
|
|
122
122
|
|
|
123
123
|
|
|
124
|
-
async def _update_pvpc_bill(cups: str):
|
|
124
|
+
async def _update_pvpc_bill(cups: str) -> None:
|
|
125
125
|
"""Download all data for a given datadis account and CUPS."""
|
|
126
126
|
|
|
127
127
|
bs = BillService(cups, storage_path="./edata_cli")
|
|
@@ -131,7 +131,7 @@ async def _update_pvpc_bill(cups: str):
|
|
|
131
131
|
@app.command()
|
|
132
132
|
def update_pvpc_bill(
|
|
133
133
|
cups: Annotated[str, typer.Option(help="The identifier of the Supply")],
|
|
134
|
-
):
|
|
134
|
+
) -> None:
|
|
135
135
|
"""Download all data for a given datadis account and CUPS."""
|
|
136
136
|
|
|
137
137
|
asyncio.run(
|
edata/core/utils.py
CHANGED
|
@@ -43,7 +43,7 @@ def get_tariff(dt: datetime) -> int:
|
|
|
43
43
|
return 0
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
def get_contract_for_dt(contracts: list[Contract], date: datetime):
|
|
46
|
+
def get_contract_for_dt(contracts: list[Contract], date: datetime) -> Contract | None:
|
|
47
47
|
"""Return the active contract for a provided datetime."""
|
|
48
48
|
|
|
49
49
|
for contract in contracts:
|
edata/database/controller.py
CHANGED
|
@@ -49,12 +49,12 @@ class EdataDB:
|
|
|
49
49
|
return cls._instance
|
|
50
50
|
|
|
51
51
|
@property
|
|
52
|
-
def engine(self):
|
|
52
|
+
def engine(self) -> AsyncEngine | None:
|
|
53
53
|
"""Return the async database engine."""
|
|
54
54
|
|
|
55
55
|
return self._engine
|
|
56
56
|
|
|
57
|
-
async def _ensure_tables(self):
|
|
57
|
+
async def _ensure_tables(self) -> None:
|
|
58
58
|
"""Create tables if not already created (lazy init)."""
|
|
59
59
|
|
|
60
60
|
if self._tables_initialized:
|
|
@@ -84,11 +84,11 @@ class EdataDB:
|
|
|
84
84
|
self,
|
|
85
85
|
session: AsyncSession,
|
|
86
86
|
query: SelectOfScalar,
|
|
87
|
-
data,
|
|
87
|
+
data: typing.Any,
|
|
88
88
|
commit: bool = True,
|
|
89
89
|
overrides: dict[str, typing.Any] | None = None,
|
|
90
90
|
) -> T | None: # type: ignore
|
|
91
|
-
"""
|
|
91
|
+
"""Update a single record in the database."""
|
|
92
92
|
|
|
93
93
|
result = await session.exec(query)
|
|
94
94
|
existing = result.first()
|
|
@@ -240,7 +240,7 @@ class EdataDB:
|
|
|
240
240
|
session, q.get_energy(cups, energy.datetime), record
|
|
241
241
|
)
|
|
242
242
|
|
|
243
|
-
async def add_energy_list(self, cups: str, energy: list[Energy]):
|
|
243
|
+
async def add_energy_list(self, cups: str, energy: list[Energy]) -> None:
|
|
244
244
|
"""Add or update a list of energy records."""
|
|
245
245
|
|
|
246
246
|
await self._ensure_tables()
|
|
@@ -264,7 +264,7 @@ class EdataDB:
|
|
|
264
264
|
session, q.get_power(cups, power.datetime), record
|
|
265
265
|
)
|
|
266
266
|
|
|
267
|
-
async def add_power_list(self, cups: str, power: list[Power]):
|
|
267
|
+
async def add_power_list(self, cups: str, power: list[Power]) -> None:
|
|
268
268
|
"""Add or update a list of power records."""
|
|
269
269
|
|
|
270
270
|
await self._ensure_tables()
|
|
@@ -285,7 +285,7 @@ class EdataDB:
|
|
|
285
285
|
session, q.get_pvpc(pvpc.datetime), record
|
|
286
286
|
)
|
|
287
287
|
|
|
288
|
-
async def add_pvpc_list(self, pvpc: list[EnergyPrice]):
|
|
288
|
+
async def add_pvpc_list(self, pvpc: list[EnergyPrice]) -> None:
|
|
289
289
|
"""Add or update a list of pvpc records."""
|
|
290
290
|
|
|
291
291
|
await self._ensure_tables()
|
|
@@ -351,7 +351,7 @@ class EdataDB:
|
|
|
351
351
|
confhash: str,
|
|
352
352
|
complete: bool,
|
|
353
353
|
bill: list[Bill],
|
|
354
|
-
):
|
|
354
|
+
) -> None:
|
|
355
355
|
"""Add or update a list of bill records."""
|
|
356
356
|
|
|
357
357
|
await self._ensure_tables()
|
|
@@ -374,7 +374,7 @@ class EdataDB:
|
|
|
374
374
|
session, queries, items, override=["complete", "confhash"]
|
|
375
375
|
)
|
|
376
376
|
|
|
377
|
-
async def list_supplies(self):
|
|
377
|
+
async def list_supplies(self) -> typing.Sequence[SupplyModel]:
|
|
378
378
|
"""List all supply records."""
|
|
379
379
|
|
|
380
380
|
await self._ensure_tables()
|
|
@@ -382,7 +382,9 @@ class EdataDB:
|
|
|
382
382
|
result = await session.exec(q.list_supply())
|
|
383
383
|
return result.all()
|
|
384
384
|
|
|
385
|
-
async def list_contracts(
|
|
385
|
+
async def list_contracts(
|
|
386
|
+
self, cups: str | None = None
|
|
387
|
+
) -> typing.Sequence[ContractModel]:
|
|
386
388
|
"""List all contract records."""
|
|
387
389
|
|
|
388
390
|
await self._ensure_tables()
|
|
@@ -395,7 +397,7 @@ class EdataDB:
|
|
|
395
397
|
cups: str,
|
|
396
398
|
date_from: datetime | None = None,
|
|
397
399
|
date_to: datetime | None = None,
|
|
398
|
-
):
|
|
400
|
+
) -> typing.Sequence[EnergyModel]:
|
|
399
401
|
"""List energy records."""
|
|
400
402
|
|
|
401
403
|
await self._ensure_tables()
|
|
@@ -408,7 +410,7 @@ class EdataDB:
|
|
|
408
410
|
cups: str,
|
|
409
411
|
date_from: datetime | None = None,
|
|
410
412
|
date_to: datetime | None = None,
|
|
411
|
-
):
|
|
413
|
+
) -> typing.Sequence[PowerModel]:
|
|
412
414
|
"""List power records."""
|
|
413
415
|
|
|
414
416
|
await self._ensure_tables()
|
|
@@ -420,7 +422,7 @@ class EdataDB:
|
|
|
420
422
|
self,
|
|
421
423
|
date_from: datetime | None = None,
|
|
422
424
|
date_to: datetime | None = None,
|
|
423
|
-
):
|
|
425
|
+
) -> typing.Sequence[PVPCModel]:
|
|
424
426
|
"""List pvpc records."""
|
|
425
427
|
|
|
426
428
|
await self._ensure_tables()
|
|
@@ -435,7 +437,7 @@ class EdataDB:
|
|
|
435
437
|
date_from: datetime | None = None,
|
|
436
438
|
date_to: datetime | None = None,
|
|
437
439
|
complete: bool | None = None,
|
|
438
|
-
):
|
|
440
|
+
) -> typing.Sequence[StatisticsModel]:
|
|
439
441
|
"""List statistics records filtered by type ('day' or 'month') and date range."""
|
|
440
442
|
|
|
441
443
|
await self._ensure_tables()
|
|
@@ -452,7 +454,7 @@ class EdataDB:
|
|
|
452
454
|
date_from: datetime | None = None,
|
|
453
455
|
date_to: datetime | None = None,
|
|
454
456
|
complete: bool | None = None,
|
|
455
|
-
):
|
|
457
|
+
) -> typing.Sequence[BillModel]:
|
|
456
458
|
"""List bill records filtered by type ('hour', 'day' or 'month') and date range."""
|
|
457
459
|
|
|
458
460
|
await self._ensure_tables()
|
edata/database/queries.py
CHANGED
|
@@ -215,7 +215,9 @@ def list_pvpc(
|
|
|
215
215
|
return query
|
|
216
216
|
|
|
217
217
|
|
|
218
|
-
def get_bill(
|
|
218
|
+
def get_bill(
|
|
219
|
+
cups: str, type_: typing.Literal["hour", "day", "month"], datetime_: datetime | None
|
|
220
|
+
) -> SelectOfScalar[BillModel]:
|
|
219
221
|
"""Query that selects a bill."""
|
|
220
222
|
|
|
221
223
|
query = select(BillModel).where(BillModel.cups == cups)
|
edata/database/utils.py
CHANGED
|
@@ -7,10 +7,7 @@ T = TypeVar("T", bound=BaseModel)
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class PydanticJSON(TypeDecorator):
|
|
10
|
-
"""
|
|
11
|
-
Tipo de SQLAlchemy para guardar modelos Pydantic como JSON.
|
|
12
|
-
Automáticamente serializa al guardar y deserializa al leer.
|
|
13
|
-
"""
|
|
10
|
+
"""SQLAlchemy type to guard Pydantic models as JSON."""
|
|
14
11
|
|
|
15
12
|
impl = JSON
|
|
16
13
|
cache_ok = True
|
|
@@ -20,6 +17,7 @@ class PydanticJSON(TypeDecorator):
|
|
|
20
17
|
self.pydantic_model = pydantic_model
|
|
21
18
|
|
|
22
19
|
def process_bind_param(self, value: T | None, dialect: Any) -> Any:
|
|
20
|
+
"""Process binding parameter."""
|
|
23
21
|
# Python -> Base de Datos
|
|
24
22
|
if value is None:
|
|
25
23
|
return None
|
|
@@ -27,6 +25,7 @@ class PydanticJSON(TypeDecorator):
|
|
|
27
25
|
return value.model_dump(mode="json")
|
|
28
26
|
|
|
29
27
|
def process_result_value(self, value: Any, dialect: Any) -> T | None:
|
|
28
|
+
"""Process result value."""
|
|
30
29
|
# Base de Datos -> Python
|
|
31
30
|
if value is None:
|
|
32
31
|
return None
|
edata/models/bill.py
CHANGED
|
@@ -6,15 +6,15 @@ from pydantic import BaseModel, Field
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class EnergyPrice(BaseModel):
|
|
9
|
-
"""
|
|
9
|
+
"""Represent pricing data."""
|
|
10
10
|
|
|
11
11
|
datetime: datetime
|
|
12
|
-
|
|
12
|
+
value_eur_kwh: float
|
|
13
13
|
delta_h: float
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class Bill(BaseModel):
|
|
17
|
-
"""
|
|
17
|
+
"""Represent a bill during a period."""
|
|
18
18
|
|
|
19
19
|
datetime: datetime
|
|
20
20
|
delta_h: float
|
|
@@ -26,7 +26,7 @@ class Bill(BaseModel):
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
class BillingRules(BaseModel):
|
|
29
|
-
"""
|
|
29
|
+
"""Represent a generic billing rule."""
|
|
30
30
|
|
|
31
31
|
p1_kw_year_eur: float
|
|
32
32
|
p2_kw_year_eur: float
|
|
@@ -50,7 +50,7 @@ class BillingRules(BaseModel):
|
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
class PVPCBillingRules(BillingRules):
|
|
53
|
-
"""
|
|
53
|
+
"""Represent a PVPC billing rule."""
|
|
54
54
|
|
|
55
55
|
p1_kw_year_eur: float = Field(default=30.67266)
|
|
56
56
|
p2_kw_year_eur: float = Field(default=1.4243591)
|
edata/models/data.py
CHANGED
|
@@ -6,32 +6,84 @@ from pydantic import BaseModel, Field
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class Energy(BaseModel):
|
|
9
|
-
"""
|
|
9
|
+
"""Represent energy consumption and/or surplus measurements."""
|
|
10
10
|
|
|
11
11
|
datetime: datetime
|
|
12
12
|
delta_h: float
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
consumption_kwh: float
|
|
14
|
+
surplus_kwh: float = Field(0)
|
|
15
|
+
generation_kwh: float = Field(0)
|
|
16
|
+
selfconsumption_kwh: float = Field(0)
|
|
15
17
|
real: bool
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class Power(BaseModel):
|
|
19
|
-
"""
|
|
21
|
+
"""Represent power measurements."""
|
|
20
22
|
|
|
21
23
|
datetime: datetime
|
|
22
|
-
|
|
24
|
+
value_kw: float
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
class Statistics(BaseModel):
|
|
26
|
-
"""
|
|
28
|
+
"""Represent aggregated energy/surplus data."""
|
|
27
29
|
|
|
28
30
|
datetime: datetime
|
|
29
31
|
delta_h: float = Field(0)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
32
|
+
consumption_kwh: float = Field(0)
|
|
33
|
+
consumption_by_tariff: list[float] = Field(default_factory=lambda: [0.0, 0.0, 0.0])
|
|
34
|
+
surplus_kwh: float = Field(0)
|
|
35
|
+
surplus_by_tariff: list[float] = Field(default_factory=lambda: [0.0, 0.0, 0.0])
|
|
36
|
+
generation_kwh: float = Field(0)
|
|
37
|
+
generation_by_tariff: list[float] = Field(default_factory=lambda: [0.0, 0.0, 0.0])
|
|
38
|
+
selfconsumption_kwh: float = Field(0)
|
|
39
|
+
selfconsumption_by_tariff: list[float] = Field(
|
|
40
|
+
default_factory=lambda: [0.0, 0.0, 0.0]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def consumption_p1_kwh(self) -> float:
|
|
45
|
+
return self.consumption_by_tariff[0]
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def consumption_p2_kwh(self) -> float:
|
|
49
|
+
return self.consumption_by_tariff[1]
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def consumption_p3_kwh(self) -> float:
|
|
53
|
+
return self.consumption_by_tariff[2]
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def surplus_p1_kwh(self) -> float:
|
|
57
|
+
return self.surplus_by_tariff[0]
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def surplus_p2_kwh(self) -> float:
|
|
61
|
+
return self.surplus_by_tariff[1]
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def surplus_p3_kwh(self) -> float:
|
|
65
|
+
return self.surplus_by_tariff[2]
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def generation_p1_kwh(self) -> float:
|
|
69
|
+
return self.generation_by_tariff[0]
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def generation_p2_kwh(self) -> float:
|
|
73
|
+
return self.generation_by_tariff[1]
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def generation_p3_kwh(self) -> float:
|
|
77
|
+
return self.generation_by_tariff[2]
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def selfconsumption_p1_kwh(self) -> float:
|
|
81
|
+
return self.selfconsumption_by_tariff[0]
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def selfconsumption_p2_kwh(self) -> float:
|
|
85
|
+
return self.selfconsumption_by_tariff[1]
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def selfconsumption_p3_kwh(self) -> float:
|
|
89
|
+
return self.selfconsumption_by_tariff[2]
|
edata/models/supply.py
CHANGED
|
@@ -16,8 +16,8 @@ class Supply(BaseModel):
|
|
|
16
16
|
province: str | None
|
|
17
17
|
municipality: str | None
|
|
18
18
|
distributor: str | None
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
point_type: int
|
|
20
|
+
distributor_code: str
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class Contract(BaseModel):
|
|
@@ -26,6 +26,15 @@ class Contract(BaseModel):
|
|
|
26
26
|
date_start: datetime
|
|
27
27
|
date_end: datetime
|
|
28
28
|
marketer: str
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
distributor_code: str
|
|
30
|
+
power: list[float]
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def power_p1(self) -> float | None:
|
|
34
|
+
"""Return power P1."""
|
|
35
|
+
return self.power[0] if len(self.power) > 0 else None
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def power_p2(self) -> float | None:
|
|
39
|
+
"""Return power P2."""
|
|
40
|
+
return self.power[1] if len(self.power) > 1 else None
|
edata/providers/datadis.py
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
"""Datadis API connector.
|
|
2
|
-
|
|
3
|
-
To fetch data from datadis.es private API.
|
|
4
|
-
There a few issues that are workarounded:
|
|
5
|
-
- You have to wait 24h between two identical requests.
|
|
6
|
-
- Datadis server does not like ranges greater than 1 month.
|
|
7
|
-
"""
|
|
1
|
+
"""Datadis API connector."""
|
|
8
2
|
|
|
9
3
|
import asyncio
|
|
10
4
|
import contextlib
|
|
@@ -28,7 +22,7 @@ TOKEN_USERNAME = "username"
|
|
|
28
22
|
TOKEN_PASSWD = "password"
|
|
29
23
|
|
|
30
24
|
# Supplies-related constants
|
|
31
|
-
URL_GET_SUPPLIES = "https://datadis.es/api-private/api/get-supplies"
|
|
25
|
+
URL_GET_SUPPLIES = "https://datadis.es/api-private/api/get-supplies-v2"
|
|
32
26
|
GET_SUPPLIES_MANDATORY_FIELDS = [
|
|
33
27
|
"cups",
|
|
34
28
|
"validDateFrom",
|
|
@@ -38,7 +32,7 @@ GET_SUPPLIES_MANDATORY_FIELDS = [
|
|
|
38
32
|
]
|
|
39
33
|
|
|
40
34
|
# Contracts-related constants
|
|
41
|
-
URL_GET_CONTRACT_DETAIL = "https://datadis.es/api-private/api/get-contract-detail"
|
|
35
|
+
URL_GET_CONTRACT_DETAIL = "https://datadis.es/api-private/api/get-contract-detail-v2"
|
|
42
36
|
GET_CONTRACT_DETAIL_MANDATORY_FIELDS = [
|
|
43
37
|
"startDate",
|
|
44
38
|
"endDate",
|
|
@@ -47,7 +41,7 @@ GET_CONTRACT_DETAIL_MANDATORY_FIELDS = [
|
|
|
47
41
|
]
|
|
48
42
|
|
|
49
43
|
# Consumption-related constants
|
|
50
|
-
URL_GET_CONSUMPTION_DATA = "https://datadis.es/api-private/api/get-consumption-data"
|
|
44
|
+
URL_GET_CONSUMPTION_DATA = "https://datadis.es/api-private/api/get-consumption-data-v2"
|
|
51
45
|
GET_CONSUMPTION_DATA_MANDATORY_FIELDS = [
|
|
52
46
|
"time",
|
|
53
47
|
"date",
|
|
@@ -56,7 +50,7 @@ GET_CONSUMPTION_DATA_MANDATORY_FIELDS = [
|
|
|
56
50
|
]
|
|
57
51
|
|
|
58
52
|
# Maximeter-related constants
|
|
59
|
-
URL_GET_MAX_POWER = "https://datadis.es/api-private/api/get-max-power"
|
|
53
|
+
URL_GET_MAX_POWER = "https://datadis.es/api-private/api/get-max-power-v2"
|
|
60
54
|
GET_MAX_POWER_MANDATORY_FIELDS = ["time", "date", "maxPower"]
|
|
61
55
|
|
|
62
56
|
# Timing constants
|
|
@@ -68,7 +62,7 @@ QUERY_LIMIT = timedelta(hours=24) # a datadis limitation, again...
|
|
|
68
62
|
RECENT_CACHE_SUBDIR = "cache"
|
|
69
63
|
|
|
70
64
|
|
|
71
|
-
def migrate_storage(storage_dir):
|
|
65
|
+
def migrate_storage(storage_dir: str) -> None:
|
|
72
66
|
"""Migrate storage from older versions."""
|
|
73
67
|
with contextlib.suppress(FileNotFoundError):
|
|
74
68
|
os.remove(os.path.join(storage_dir, "edata_recent_queries.json"))
|
|
@@ -102,9 +96,8 @@ class DatadisConnector:
|
|
|
102
96
|
os.makedirs(self._recent_cache_dir, exist_ok=True)
|
|
103
97
|
self._cache = diskcache.Cache(self._recent_cache_dir)
|
|
104
98
|
|
|
105
|
-
def _get_hash(self, item: str):
|
|
99
|
+
def _get_hash(self, item: str) -> str:
|
|
106
100
|
"""Return a hash."""
|
|
107
|
-
|
|
108
101
|
return hashlib.md5(item.encode()).hexdigest()
|
|
109
102
|
|
|
110
103
|
def _set_cache(self, key: str, data: dict | None = None) -> None:
|
|
@@ -121,7 +114,7 @@ class DatadisConnector:
|
|
|
121
114
|
hash_query = self._get_hash(key)
|
|
122
115
|
return hash_query in self._cache
|
|
123
116
|
|
|
124
|
-
def _get_cache(self, key: str):
|
|
117
|
+
def _get_cache(self, key: str) -> dict | None:
|
|
125
118
|
"""Return cached response for a query (diskcache)."""
|
|
126
119
|
hash_query = self._get_hash(key)
|
|
127
120
|
try:
|
|
@@ -129,7 +122,7 @@ class DatadisConnector:
|
|
|
129
122
|
except Exception:
|
|
130
123
|
return None
|
|
131
124
|
|
|
132
|
-
async def _async_get_token(self):
|
|
125
|
+
async def _async_get_token(self) -> bool:
|
|
133
126
|
"""Private async method that fetches a new token if needed."""
|
|
134
127
|
_LOGGER.debug("No token found, fetching a new one")
|
|
135
128
|
is_valid_token = False
|
|
@@ -158,11 +151,11 @@ class DatadisConnector:
|
|
|
158
151
|
_LOGGER.error("Exception while retrieving token: %s", e)
|
|
159
152
|
return is_valid_token
|
|
160
153
|
|
|
161
|
-
async def async_login(self):
|
|
154
|
+
async def async_login(self) -> bool:
|
|
162
155
|
"""Test to login with provided credentials (async)."""
|
|
163
156
|
return await self._async_get_token()
|
|
164
157
|
|
|
165
|
-
def login(self):
|
|
158
|
+
def login(self) -> bool:
|
|
166
159
|
"""Test to login with provided credentials (sync wrapper)."""
|
|
167
160
|
return asyncio.run(self.async_login())
|
|
168
161
|
|
|
@@ -288,6 +281,7 @@ class DatadisConnector:
|
|
|
288
281
|
async def async_get_supplies(
|
|
289
282
|
self, authorized_nif: str | None = None
|
|
290
283
|
) -> list[Supply]:
|
|
284
|
+
"""Datadis 'get_supplies' query."""
|
|
291
285
|
data = {}
|
|
292
286
|
if authorized_nif is not None:
|
|
293
287
|
data["authorizedNif"] = authorized_nif
|
|
@@ -296,7 +290,7 @@ class DatadisConnector:
|
|
|
296
290
|
)
|
|
297
291
|
supplies = []
|
|
298
292
|
tomorrow_str = (datetime.today() + timedelta(days=1)).strftime("%Y/%m/%d")
|
|
299
|
-
for i in response:
|
|
293
|
+
for i in response.get("supplies", []):
|
|
300
294
|
if all(k in i for k in GET_SUPPLIES_MANDATORY_FIELDS):
|
|
301
295
|
supplies.append(
|
|
302
296
|
Supply(
|
|
@@ -322,8 +316,8 @@ class DatadisConnector:
|
|
|
322
316
|
province=i.get("province", None),
|
|
323
317
|
municipality=i.get("municipality", None),
|
|
324
318
|
distributor=i.get("distributor", None),
|
|
325
|
-
|
|
326
|
-
|
|
319
|
+
point_type=i["pointType"],
|
|
320
|
+
distributor_code=i["distributorCode"],
|
|
327
321
|
)
|
|
328
322
|
)
|
|
329
323
|
else:
|
|
@@ -333,13 +327,14 @@ class DatadisConnector:
|
|
|
333
327
|
)
|
|
334
328
|
return supplies
|
|
335
329
|
|
|
336
|
-
def get_supplies(self, authorized_nif: str | None = None):
|
|
330
|
+
def get_supplies(self, authorized_nif: str | None = None) -> list[Supply]:
|
|
337
331
|
"""Datadis 'get_supplies' query (sync wrapper)."""
|
|
338
332
|
return asyncio.run(self.async_get_supplies(authorized_nif=authorized_nif))
|
|
339
333
|
|
|
340
334
|
async def async_get_contract_detail(
|
|
341
335
|
self, cups: str, distributor_code: str, authorized_nif: str | None = None
|
|
342
336
|
) -> list[Contract]:
|
|
337
|
+
"""Datadis 'get_contract_detail' query."""
|
|
343
338
|
data = {"cups": cups, "distributorCode": distributor_code}
|
|
344
339
|
if authorized_nif is not None:
|
|
345
340
|
data["authorizedNif"] = authorized_nif
|
|
@@ -348,7 +343,7 @@ class DatadisConnector:
|
|
|
348
343
|
)
|
|
349
344
|
contracts = []
|
|
350
345
|
tomorrow_str = (datetime.today() + timedelta(days=1)).strftime("%Y/%m/%d")
|
|
351
|
-
for i in response:
|
|
346
|
+
for i in response.get("contract", []):
|
|
352
347
|
if all(k in i for k in GET_CONTRACT_DETAIL_MANDATORY_FIELDS):
|
|
353
348
|
contracts.append(
|
|
354
349
|
Contract(
|
|
@@ -361,17 +356,8 @@ class DatadisConnector:
|
|
|
361
356
|
"%Y/%m/%d",
|
|
362
357
|
),
|
|
363
358
|
marketer=i["marketer"],
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
i["contractedPowerkW"][0]
|
|
367
|
-
if isinstance(i["contractedPowerkW"], list)
|
|
368
|
-
else None
|
|
369
|
-
),
|
|
370
|
-
power_p2=(
|
|
371
|
-
i["contractedPowerkW"][1]
|
|
372
|
-
if (len(i["contractedPowerkW"]) > 1)
|
|
373
|
-
else None
|
|
374
|
-
),
|
|
359
|
+
distributor_code=distributor_code,
|
|
360
|
+
power=i["contractedPowerkW"],
|
|
375
361
|
)
|
|
376
362
|
)
|
|
377
363
|
else:
|
|
@@ -383,7 +369,7 @@ class DatadisConnector:
|
|
|
383
369
|
|
|
384
370
|
def get_contract_detail(
|
|
385
371
|
self, cups: str, distributor_code: str, authorized_nif: str | None = None
|
|
386
|
-
):
|
|
372
|
+
) -> list[Contract]:
|
|
387
373
|
"""Datadis get_contract_detail query (sync wrapper)."""
|
|
388
374
|
return asyncio.run(
|
|
389
375
|
self.async_get_contract_detail(cups, distributor_code, authorized_nif)
|
|
@@ -399,7 +385,7 @@ class DatadisConnector:
|
|
|
399
385
|
point_type: int,
|
|
400
386
|
authorized_nif: str | None = None,
|
|
401
387
|
) -> list[Energy]:
|
|
402
|
-
|
|
388
|
+
"""Datadis 'get_consumption_data' query."""
|
|
403
389
|
data = {
|
|
404
390
|
"cups": cups,
|
|
405
391
|
"distributorCode": distributor_code,
|
|
@@ -414,7 +400,7 @@ class DatadisConnector:
|
|
|
414
400
|
response = await self._async_get(URL_GET_CONSUMPTION_DATA, request_data=data)
|
|
415
401
|
|
|
416
402
|
consumptions = []
|
|
417
|
-
for i in response:
|
|
403
|
+
for i in response.get("timeCurve", []):
|
|
418
404
|
if "consumptionKWh" in i:
|
|
419
405
|
if all(k in i for k in GET_CONSUMPTION_DATA_MANDATORY_FIELDS):
|
|
420
406
|
hour = str(int(i["time"].split(":")[0]) - 1)
|
|
@@ -423,15 +409,26 @@ class DatadisConnector:
|
|
|
423
409
|
)
|
|
424
410
|
if not (start_date <= date_as_dt <= end_date):
|
|
425
411
|
continue # skip element if dt is out of range
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
412
|
+
|
|
413
|
+
# sanitize these values
|
|
414
|
+
_surplus_kwh = i.get("surplusEnergyKWh", 0)
|
|
415
|
+
if _surplus_kwh is None:
|
|
416
|
+
_surplus_kwh = 0
|
|
417
|
+
_generation_kwh = i.get("generationEnergyKWh", 0)
|
|
418
|
+
if _generation_kwh is None:
|
|
419
|
+
_generation_kwh = 0
|
|
420
|
+
_selfconsumption_kwh = i.get("selfConsumptionEnergyKWh", 0)
|
|
421
|
+
if _selfconsumption_kwh is None:
|
|
422
|
+
_selfconsumption_kwh = 0
|
|
423
|
+
|
|
429
424
|
consumptions.append(
|
|
430
425
|
Energy(
|
|
431
426
|
datetime=date_as_dt,
|
|
432
427
|
delta_h=1,
|
|
433
|
-
|
|
434
|
-
|
|
428
|
+
consumption_kwh=i["consumptionKWh"],
|
|
429
|
+
surplus_kwh=_surplus_kwh,
|
|
430
|
+
generation_kwh=_generation_kwh,
|
|
431
|
+
selfconsumption_kwh=_selfconsumption_kwh,
|
|
435
432
|
real=i["obtainMethod"] == "Real",
|
|
436
433
|
)
|
|
437
434
|
)
|
|
@@ -451,7 +448,7 @@ class DatadisConnector:
|
|
|
451
448
|
measurement_type: str,
|
|
452
449
|
point_type: int,
|
|
453
450
|
authorized_nif: str | None = None,
|
|
454
|
-
):
|
|
451
|
+
) -> list[Energy]:
|
|
455
452
|
"""Datadis get_consumption_data query (sync wrapper)."""
|
|
456
453
|
return asyncio.run(
|
|
457
454
|
self.async_get_consumption_data(
|
|
@@ -473,6 +470,7 @@ class DatadisConnector:
|
|
|
473
470
|
end_date: datetime,
|
|
474
471
|
authorized_nif: str | None = None,
|
|
475
472
|
) -> list[Power]:
|
|
473
|
+
"""Datadis 'get_max_power' query."""
|
|
476
474
|
data = {
|
|
477
475
|
"cups": cups,
|
|
478
476
|
"distributorCode": distributor_code,
|
|
@@ -483,14 +481,14 @@ class DatadisConnector:
|
|
|
483
481
|
data["authorizedNif"] = authorized_nif
|
|
484
482
|
response = await self._async_get(URL_GET_MAX_POWER, request_data=data)
|
|
485
483
|
maxpower_values = []
|
|
486
|
-
for i in response:
|
|
484
|
+
for i in response.get("maxPower", []):
|
|
487
485
|
if all(k in i for k in GET_MAX_POWER_MANDATORY_FIELDS):
|
|
488
486
|
maxpower_values.append(
|
|
489
487
|
Power(
|
|
490
488
|
datetime=datetime.strptime(
|
|
491
489
|
f"{i['date']} {i['time']}", "%Y/%m/%d %H:%M"
|
|
492
490
|
),
|
|
493
|
-
|
|
491
|
+
value_kw=i["maxPower"],
|
|
494
492
|
)
|
|
495
493
|
)
|
|
496
494
|
else:
|
|
@@ -507,7 +505,7 @@ class DatadisConnector:
|
|
|
507
505
|
start_date: datetime,
|
|
508
506
|
end_date: datetime,
|
|
509
507
|
authorized_nif: str | None = None,
|
|
510
|
-
):
|
|
508
|
+
) -> list[Power]:
|
|
511
509
|
"""Datadis get_max_power query (sync wrapper)."""
|
|
512
510
|
return asyncio.run(
|
|
513
511
|
self.async_get_max_power(
|
edata/providers/redata.py
CHANGED
|
@@ -62,7 +62,7 @@ class REDataConnector:
|
|
|
62
62
|
datetime=parser.parse(element["datetime"]).replace(
|
|
63
63
|
tzinfo=None
|
|
64
64
|
),
|
|
65
|
-
|
|
65
|
+
value_eur_kwh=element["value"] / 1000,
|
|
66
66
|
delta_h=1,
|
|
67
67
|
)
|
|
68
68
|
)
|
|
@@ -79,7 +79,7 @@ class REDataConnector:
|
|
|
79
79
|
|
|
80
80
|
def get_realtime_prices(
|
|
81
81
|
self, dt_from: dt.datetime, dt_to: dt.datetime, is_ceuta_melilla: bool = False
|
|
82
|
-
) -> list:
|
|
82
|
+
) -> list[EnergyPrice]:
|
|
83
83
|
"""GET query to fetch realtime pvpc prices, historical data is limited to current month (sync wrapper)"""
|
|
84
84
|
return asyncio.run(
|
|
85
85
|
self.async_get_realtime_prices(dt_from, dt_to, is_ceuta_melilla)
|
edata/services/bill_service.py
CHANGED
|
@@ -115,12 +115,12 @@ class BillService:
|
|
|
115
115
|
pvpc = await self._get_pvpc()
|
|
116
116
|
billing_rules = PVPCBillingRules(**billing_rules.model_dump())
|
|
117
117
|
bills = await asyncio.to_thread(
|
|
118
|
-
self.
|
|
118
|
+
self.simulate_pvpc, contracts, energy, pvpc, billing_rules
|
|
119
119
|
)
|
|
120
120
|
confighash = f"pvpc-{hash(billing_rules.model_dump_json())}"
|
|
121
121
|
else:
|
|
122
122
|
bills = await asyncio.to_thread(
|
|
123
|
-
self.
|
|
123
|
+
self.simulate_custom, contracts, energy, billing_rules
|
|
124
124
|
)
|
|
125
125
|
confighash = f"custom-{hash(billing_rules.model_dump_json())}"
|
|
126
126
|
|
|
@@ -143,7 +143,7 @@ class BillService:
|
|
|
143
143
|
await self._update_daily_statistics(start, end)
|
|
144
144
|
await self._update_monthly_statistics(start, end)
|
|
145
145
|
|
|
146
|
-
async def _update_daily_statistics(self, start: datetime, end: datetime):
|
|
146
|
+
async def _update_daily_statistics(self, start: datetime, end: datetime) -> None:
|
|
147
147
|
"""Update daily statistics within a date range."""
|
|
148
148
|
|
|
149
149
|
day_start = get_day(start)
|
|
@@ -171,7 +171,7 @@ class BillService:
|
|
|
171
171
|
stat.datetime.date(),
|
|
172
172
|
)
|
|
173
173
|
|
|
174
|
-
async def _update_monthly_statistics(self, start: datetime, end: datetime):
|
|
174
|
+
async def _update_monthly_statistics(self, start: datetime, end: datetime) -> None:
|
|
175
175
|
"""Update monthly statistics within a date range."""
|
|
176
176
|
|
|
177
177
|
month_start = get_month(start)
|
|
@@ -202,7 +202,7 @@ class BillService:
|
|
|
202
202
|
stat.datetime.date(),
|
|
203
203
|
)
|
|
204
204
|
|
|
205
|
-
async def _find_missing_stats(self):
|
|
205
|
+
async def _find_missing_stats(self) -> list[datetime]:
|
|
206
206
|
"""Return the list of days that are missing billing data."""
|
|
207
207
|
|
|
208
208
|
stats = await self.db.list_bill(self._cups, "day", complete=False)
|
|
@@ -247,7 +247,7 @@ class BillService:
|
|
|
247
247
|
|
|
248
248
|
return [agg_data[x] for x in agg_data]
|
|
249
249
|
|
|
250
|
-
async def fix_missing_statistics(self):
|
|
250
|
+
async def fix_missing_statistics(self) -> None:
|
|
251
251
|
"""Recompile statistics to fix missing data."""
|
|
252
252
|
|
|
253
253
|
missing = await self._find_missing_stats()
|
|
@@ -264,7 +264,7 @@ class BillService:
|
|
|
264
264
|
end = month + relativedelta.relativedelta(months=1) - timedelta(minutes=1)
|
|
265
265
|
await self._update_monthly_statistics(month, end)
|
|
266
266
|
|
|
267
|
-
def
|
|
267
|
+
def simulate_pvpc(
|
|
268
268
|
self,
|
|
269
269
|
contracts: list[Contract],
|
|
270
270
|
energy: list[Energy],
|
|
@@ -306,8 +306,8 @@ class BillService:
|
|
|
306
306
|
bill.energy_term = (
|
|
307
307
|
rules.electricity_tax
|
|
308
308
|
* rules.iva_tax
|
|
309
|
-
* p[dt].
|
|
310
|
-
* e[dt].
|
|
309
|
+
* p[dt].value_eur_kwh
|
|
310
|
+
* e[dt].consumption_kwh
|
|
311
311
|
)
|
|
312
312
|
bill.power_term = (
|
|
313
313
|
rules.electricity_tax
|
|
@@ -326,12 +326,12 @@ class BillService:
|
|
|
326
326
|
|
|
327
327
|
return [x for x in b.values()]
|
|
328
328
|
|
|
329
|
-
def
|
|
329
|
+
def simulate_custom(
|
|
330
330
|
self,
|
|
331
331
|
contracts: list[Contract],
|
|
332
332
|
energy: list[Energy],
|
|
333
333
|
rules: BillingRules,
|
|
334
|
-
):
|
|
334
|
+
) -> list[Bill]:
|
|
335
335
|
"""Compile bills from custom rules."""
|
|
336
336
|
|
|
337
337
|
e = {x.datetime: x for x in energy}
|
|
@@ -358,7 +358,7 @@ class BillService:
|
|
|
358
358
|
params = rules.model_dump()
|
|
359
359
|
params["p1_kw"] = p1_kw
|
|
360
360
|
params["p2_kw"] = p2_kw
|
|
361
|
-
params["kwh"] = e[dt].
|
|
361
|
+
params["kwh"] = e[dt].consumption_kwh
|
|
362
362
|
|
|
363
363
|
tariff = get_tariff(dt)
|
|
364
364
|
if tariff == 1:
|
|
@@ -388,25 +388,26 @@ class BillService:
|
|
|
388
388
|
return [x for x in b.values()]
|
|
389
389
|
|
|
390
390
|
async def _get_contracts(self) -> list[Contract]:
|
|
391
|
-
|
|
391
|
+
"""Get contracts."""
|
|
392
392
|
res = await self.db.list_contracts(self._cups)
|
|
393
393
|
return [x.data for x in res]
|
|
394
394
|
|
|
395
395
|
async def _get_energy(
|
|
396
396
|
self, start: datetime | None = None, end: datetime | None = None
|
|
397
397
|
) -> list[Energy]:
|
|
398
|
+
"""Get energy."""
|
|
398
399
|
res = await self.db.list_energy(self._cups, start, end)
|
|
399
400
|
return [x.data for x in res]
|
|
400
401
|
|
|
401
402
|
async def _get_pvpc(
|
|
402
403
|
self, start: datetime | None = None, end: datetime | None = None
|
|
403
404
|
) -> list[EnergyPrice]:
|
|
405
|
+
"""Get PVPC."""
|
|
404
406
|
res = await self.db.list_pvpc(start, end)
|
|
405
407
|
return [x.data for x in res]
|
|
406
408
|
|
|
407
409
|
async def _get_last_bill_dt(self) -> datetime | None:
|
|
408
410
|
"""Return the timestamp of the latest bill record."""
|
|
409
|
-
|
|
410
411
|
last_record = await self.db.get_last_bill(self._cups)
|
|
411
412
|
if last_record:
|
|
412
413
|
return last_record.datetime
|
edata/services/data_service.py
CHANGED
|
@@ -109,7 +109,7 @@ 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 fix_missing_statistics(self):
|
|
112
|
+
async def fix_missing_statistics(self) -> None:
|
|
113
113
|
"""Recompile statistics to fix missing data."""
|
|
114
114
|
|
|
115
115
|
missing = await self._find_missing_stats()
|
|
@@ -236,12 +236,12 @@ class DataService:
|
|
|
236
236
|
|
|
237
237
|
return True
|
|
238
238
|
|
|
239
|
-
async def login(self):
|
|
239
|
+
async def login(self) -> bool:
|
|
240
240
|
"""Test login at Datadis."""
|
|
241
241
|
|
|
242
242
|
return await self.datadis.async_login()
|
|
243
243
|
|
|
244
|
-
async def update_supplies(self):
|
|
244
|
+
async def update_supplies(self) -> None:
|
|
245
245
|
"""Update the list of supplies for the configured user."""
|
|
246
246
|
|
|
247
247
|
self._supplies = await self.datadis.async_get_supplies(self._authorized_nif)
|
|
@@ -255,7 +255,7 @@ class DataService:
|
|
|
255
255
|
supply = self._find_supply_for_cups(cups)
|
|
256
256
|
if supply:
|
|
257
257
|
self._contracts = await self.datadis.async_get_contract_detail(
|
|
258
|
-
cups, supply.
|
|
258
|
+
cups, supply.distributor_code, self._authorized_nif
|
|
259
259
|
)
|
|
260
260
|
for c in self._contracts:
|
|
261
261
|
await self.db.add_contract(self._cups, c)
|
|
@@ -263,7 +263,7 @@ class DataService:
|
|
|
263
263
|
_LOGGER.warning("Unable to fetch contract details for %s", self._scups)
|
|
264
264
|
return False
|
|
265
265
|
|
|
266
|
-
async def update_energy(self, start: datetime, end: datetime):
|
|
266
|
+
async def update_energy(self, start: datetime, end: datetime) -> bool:
|
|
267
267
|
"""Update the list of energy consumptions for the selected cups."""
|
|
268
268
|
|
|
269
269
|
cups = self._cups
|
|
@@ -271,11 +271,11 @@ class DataService:
|
|
|
271
271
|
if supply:
|
|
272
272
|
data = await self.datadis.async_get_consumption_data(
|
|
273
273
|
cups,
|
|
274
|
-
supply.
|
|
274
|
+
supply.distributor_code,
|
|
275
275
|
start,
|
|
276
276
|
end,
|
|
277
277
|
self._measurement_type,
|
|
278
|
-
supply.
|
|
278
|
+
supply.point_type,
|
|
279
279
|
self._authorized_nif,
|
|
280
280
|
)
|
|
281
281
|
await self.db.add_energy_list(self._cups, data)
|
|
@@ -283,14 +283,14 @@ class DataService:
|
|
|
283
283
|
_LOGGER.warning("Unable to fetch energy data for %s", self._scups)
|
|
284
284
|
return False
|
|
285
285
|
|
|
286
|
-
async def update_power(self, start: datetime, end: datetime):
|
|
286
|
+
async def update_power(self, start: datetime, end: datetime) -> bool:
|
|
287
287
|
"""Update the list of power peaks for the selected cups."""
|
|
288
288
|
cups = self._cups
|
|
289
289
|
supply = self._find_supply_for_cups(cups)
|
|
290
290
|
if supply:
|
|
291
291
|
data = await self.datadis.async_get_max_power(
|
|
292
292
|
cups,
|
|
293
|
-
supply.
|
|
293
|
+
supply.distributor_code,
|
|
294
294
|
start,
|
|
295
295
|
end,
|
|
296
296
|
self._authorized_nif,
|
|
@@ -300,7 +300,7 @@ class DataService:
|
|
|
300
300
|
_LOGGER.warning("Unable to fetch power data for %s", self._scups)
|
|
301
301
|
return False
|
|
302
302
|
|
|
303
|
-
async def update_pvpc(self, start: datetime, end: datetime):
|
|
303
|
+
async def update_pvpc(self, start: datetime, end: datetime) -> bool:
|
|
304
304
|
"""Update recent pvpc prices."""
|
|
305
305
|
|
|
306
306
|
cups = self._cups
|
|
@@ -324,13 +324,13 @@ class DataService:
|
|
|
324
324
|
_LOGGER.warning("%s unable to fetch pvpc prices", self._scups)
|
|
325
325
|
return False
|
|
326
326
|
|
|
327
|
-
async def update_statistics(self, start: datetime, end: datetime):
|
|
327
|
+
async def update_statistics(self, start: datetime, end: datetime) -> None:
|
|
328
328
|
"""Update the statistics during a period."""
|
|
329
329
|
|
|
330
330
|
await self._update_daily_statistics(start, end)
|
|
331
331
|
await self._update_monthly_statistics(start, end)
|
|
332
332
|
|
|
333
|
-
async def _update_daily_statistics(self, start: datetime, end: datetime):
|
|
333
|
+
async def _update_daily_statistics(self, start: datetime, end: datetime) -> None:
|
|
334
334
|
"""Update daily statistics within a date range."""
|
|
335
335
|
|
|
336
336
|
day_start = get_day(start)
|
|
@@ -358,7 +358,7 @@ class DataService:
|
|
|
358
358
|
stat.datetime.date(),
|
|
359
359
|
)
|
|
360
360
|
|
|
361
|
-
async def _update_monthly_statistics(self, start: datetime, end: datetime):
|
|
361
|
+
async def _update_monthly_statistics(self, start: datetime, end: datetime) -> None:
|
|
362
362
|
"""Update monthly statistics within a date range."""
|
|
363
363
|
|
|
364
364
|
month_start = get_month(start)
|
|
@@ -420,33 +420,34 @@ class DataService:
|
|
|
420
420
|
agg_data[agg_dt] = Statistics(
|
|
421
421
|
datetime=agg_dt,
|
|
422
422
|
delta_h=0,
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
423
|
+
value_kwh=0,
|
|
424
|
+
consumption_by_tariff=[0.0, 0.0, 0.0],
|
|
425
|
+
surplus_kwh=0,
|
|
426
|
+
surplus_by_tariff=[0.0, 0.0, 0.0],
|
|
427
|
+
generation_kwh=0,
|
|
428
|
+
generation_by_tariff=[0.0, 0.0, 0.0],
|
|
429
|
+
selfconsumption_kwh=0,
|
|
430
|
+
selfconsumption_by_tariff=[0.0, 0.0, 0.0],
|
|
431
431
|
)
|
|
432
432
|
|
|
433
433
|
ref = agg_data[agg_dt]
|
|
434
434
|
ref.delta_h += item.delta_h
|
|
435
|
-
ref.
|
|
436
|
-
ref.
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
ref.
|
|
442
|
-
ref.
|
|
443
|
-
|
|
444
|
-
ref.
|
|
445
|
-
ref.surplus_p3_kWh += item.surplus_kWh
|
|
435
|
+
ref.consumption_kwh += item.consumption_kwh
|
|
436
|
+
ref.surplus_kwh += item.surplus_kwh
|
|
437
|
+
ref.generation_kwh += item.generation_kwh
|
|
438
|
+
ref.selfconsumption_kwh += item.selfconsumption_kwh
|
|
439
|
+
if 1 <= tariff <= 3:
|
|
440
|
+
idx = tariff - 1
|
|
441
|
+
ref.consumption_by_tariff[idx] += item.consumption_kwh
|
|
442
|
+
ref.surplus_by_tariff[idx] += item.surplus_kwh
|
|
443
|
+
ref.generation_by_tariff[idx] += item.generation_kwh
|
|
444
|
+
ref.selfconsumption_by_tariff[idx] += item.selfconsumption_kwh
|
|
446
445
|
|
|
447
446
|
return [agg_data[x] for x in agg_data]
|
|
448
447
|
|
|
449
|
-
async def _find_missing_stats(
|
|
448
|
+
async def _find_missing_stats(
|
|
449
|
+
self, agg: typing.Literal["day", "month"] = "day"
|
|
450
|
+
) -> list[datetime]:
|
|
450
451
|
"""Return the list of days that are missing energy data."""
|
|
451
452
|
|
|
452
453
|
stats = await self.get_statistics(agg, complete=False)
|
|
@@ -473,7 +474,7 @@ class DataService:
|
|
|
473
474
|
if last_record:
|
|
474
475
|
return last_record.datetime
|
|
475
476
|
|
|
476
|
-
async def _sync(self):
|
|
477
|
+
async def _sync(self) -> None:
|
|
477
478
|
"""Load state."""
|
|
478
479
|
|
|
479
480
|
self._supplies = await self.get_supplies()
|
|
@@ -8,65 +8,73 @@ from edata.providers.datadis import DatadisConnector
|
|
|
8
8
|
MOCK_USERNAME = "USERNAME"
|
|
9
9
|
MOCK_PASSWORD = "PASSWORD"
|
|
10
10
|
|
|
11
|
-
SUPPLIES_RESPONSE =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
11
|
+
SUPPLIES_RESPONSE = {
|
|
12
|
+
"supplies": [
|
|
13
|
+
{
|
|
14
|
+
"cups": "ESXXXXXXXXXXXXXXXXTEST",
|
|
15
|
+
"validDateFrom": "2022/03/09",
|
|
16
|
+
"validDateTo": "2022/10/28",
|
|
17
|
+
"address": "-",
|
|
18
|
+
"postalCode": "-",
|
|
19
|
+
"province": "-",
|
|
20
|
+
"municipality": "-",
|
|
21
|
+
"distributor": "-",
|
|
22
|
+
"pointType": 5,
|
|
23
|
+
"distributorCode": "2",
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
25
27
|
|
|
26
|
-
CONTRACTS_RESPONSE =
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
CONTRACTS_RESPONSE = {
|
|
29
|
+
"contract": [
|
|
30
|
+
{
|
|
31
|
+
"startDate": "2022/03/09",
|
|
32
|
+
"endDate": "2022/10/28",
|
|
33
|
+
"marketer": "MARKETER",
|
|
34
|
+
"distributorCode": "2",
|
|
35
|
+
"contractedPowerkW": [4.4, 4.4],
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
35
39
|
|
|
36
|
-
CONSUMPTIONS_RESPONSE =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
40
|
+
CONSUMPTIONS_RESPONSE = {
|
|
41
|
+
"timeCurve": [
|
|
42
|
+
{
|
|
43
|
+
"date": "2022/10/22",
|
|
44
|
+
"time": "01:00",
|
|
45
|
+
"consumptionKWh": 0.203,
|
|
46
|
+
"surplusEnergyKWh": 0,
|
|
47
|
+
"obtainMethod": "Real",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"date": "2022/10/22",
|
|
51
|
+
"time": "02:00",
|
|
52
|
+
"consumptionKWh": 0.163,
|
|
53
|
+
"surplusEnergyKWh": 0,
|
|
54
|
+
"obtainMethod": "Real",
|
|
55
|
+
},
|
|
56
|
+
]
|
|
57
|
+
}
|
|
52
58
|
|
|
53
|
-
MAXIMETER_RESPONSE =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
59
|
+
MAXIMETER_RESPONSE = {
|
|
60
|
+
"maxPower": [
|
|
61
|
+
{
|
|
62
|
+
"date": "2022/03/10",
|
|
63
|
+
"time": "14:15",
|
|
64
|
+
"maxPower": 2.436,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"date": "2022/03/14",
|
|
68
|
+
"time": "13:15",
|
|
69
|
+
"maxPower": 3.008,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"date": "2022/03/27",
|
|
73
|
+
"time": "10:30",
|
|
74
|
+
"maxPower": 3.288,
|
|
75
|
+
},
|
|
76
|
+
]
|
|
77
|
+
}
|
|
70
78
|
|
|
71
79
|
|
|
72
80
|
@patch("aiohttp.ClientSession.get")
|
|
@@ -157,7 +165,7 @@ def test_get_supplies_empty_response(mock_token, mock_get, snapshot):
|
|
|
157
165
|
mock_response = MagicMock()
|
|
158
166
|
mock_response.status = 200
|
|
159
167
|
mock_response.text = AsyncMock(return_value="text")
|
|
160
|
-
mock_response.json = AsyncMock(return_value=[])
|
|
168
|
+
mock_response.json = AsyncMock(return_value={"supplies": []})
|
|
161
169
|
mock_get.return_value.__aenter__.return_value = mock_response
|
|
162
170
|
connector = DatadisConnector(MOCK_USERNAME, MOCK_PASSWORD)
|
|
163
171
|
assert connector.get_supplies() == snapshot
|
|
@@ -169,7 +177,7 @@ def test_get_supplies_empty_response(mock_token, mock_get, snapshot):
|
|
|
169
177
|
)
|
|
170
178
|
def test_get_supplies_malformed_response(mock_token, mock_get, snapshot):
|
|
171
179
|
"""Test get_supplies with malformed response (missing required fields, syrupy snapshot)."""
|
|
172
|
-
malformed = [{"validDateFrom": "2022/03/09"}] # missing 'cups', etc.
|
|
180
|
+
malformed = {"supplies": [{"validDateFrom": "2022/03/09"}]} # missing 'cups', etc.
|
|
173
181
|
mock_response = MagicMock()
|
|
174
182
|
mock_response.status = 200
|
|
175
183
|
mock_response.text = AsyncMock(return_value="text")
|
|
@@ -185,10 +193,10 @@ def test_get_supplies_malformed_response(mock_token, mock_get, snapshot):
|
|
|
185
193
|
)
|
|
186
194
|
def test_get_supplies_partial_response(mock_token, mock_get, snapshot):
|
|
187
195
|
"""Test get_supplies with partial valid/invalid response."""
|
|
188
|
-
partial = [
|
|
189
|
-
SUPPLIES_RESPONSE[0],
|
|
196
|
+
partial = {"supplies": [
|
|
197
|
+
SUPPLIES_RESPONSE["supplies"][0],
|
|
190
198
|
{"validDateFrom": "2022/03/09"}, # invalid
|
|
191
|
-
]
|
|
199
|
+
]}
|
|
192
200
|
mock_response = MagicMock()
|
|
193
201
|
mock_response.status = 200
|
|
194
202
|
mock_response.text = AsyncMock(return_value="text")
|
|
@@ -244,20 +252,22 @@ def test_get_consumption_data_cache(mock_token, mock_get, snapshot):
|
|
|
244
252
|
)
|
|
245
253
|
def test_get_supplies_optional_fields_none(mock_token, mock_get, snapshot):
|
|
246
254
|
"""Test get_supplies with optional fields as None."""
|
|
247
|
-
response =
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
255
|
+
response = {
|
|
256
|
+
"supplies": [
|
|
257
|
+
{
|
|
258
|
+
"cups": "ESXXXXXXXXXXXXXXXXTEST",
|
|
259
|
+
"validDateFrom": "2022/03/09",
|
|
260
|
+
"validDateTo": "2022/10/28",
|
|
261
|
+
"address": None,
|
|
262
|
+
"postalCode": None,
|
|
263
|
+
"province": None,
|
|
264
|
+
"municipality": None,
|
|
265
|
+
"distributor": None,
|
|
266
|
+
"pointType": 5,
|
|
267
|
+
"distributorCode": "2",
|
|
268
|
+
}
|
|
269
|
+
]
|
|
270
|
+
}
|
|
261
271
|
mock_response = MagicMock()
|
|
262
272
|
mock_response.status = 200
|
|
263
273
|
mock_response.text = AsyncMock(return_value="text")
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
e_data-2.0.0.dev112.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
2
|
-
edata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
edata/cli.py,sha256=BuYQF9kSJsZM8UbsTP3x4b0Y733ZyEimWpoto14tbW8,3953
|
|
4
|
-
edata/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
edata/core/const.py,sha256=X9J2f7lt9jXJnghqz0JFi-J7USa3xlYvbuiki70eHNE,43
|
|
6
|
-
edata/core/utils.py,sha256=_zUsS116gIDJIOIUIeLFqb8_9g5knGObic_SyXTWtpg,1919
|
|
7
|
-
edata/database/__init__.py,sha256=HP_pDbGLzeMY9Ufm_Dps5cxzW_3YTonT8w8dstG558c,46
|
|
8
|
-
edata/database/controller.py,sha256=uGv7tR72Vuxb-kg8M_Q3uGSsYYgU1UPM8JQPUts_pyU,16046
|
|
9
|
-
edata/database/models.py,sha256=4rlRpPErULqYr6QJxZar1NBpJPWTcHftDN1MB3bwGyA,5096
|
|
10
|
-
edata/database/queries.py,sha256=EHRslAMQCAS9swIwKl6PvfqHHt4tclfLskcGDSFi6PI,6590
|
|
11
|
-
edata/database/utils.py,sha256=8-QP9h-iol8NEs_qFdNAoHeHa0zQBMwQlgzJhMmZ0VE,1070
|
|
12
|
-
edata/models/__init__.py,sha256=nMPPPkGjUsF51PBFlChgHyb5C3aQiccTRiVHUdJjNj4,153
|
|
13
|
-
edata/models/bill.py,sha256=iJ5ccoNQdo8MMyh5WjVe6ffm-wBfZAb2PSlIVnWxaR0,2746
|
|
14
|
-
edata/models/data.py,sha256=zHUD453aUq6xkE-qSUfgVS26kQugH1LI90eatIFNrF8,893
|
|
15
|
-
edata/models/supply.py,sha256=i4Gj7mN7xyGfFURbsPRmEmjGT6vntLDvgNNjsDTlE8U,610
|
|
16
|
-
edata/providers/__init__.py,sha256=5xaRKPttYM6EpVE1I_vhUAF25L9pEM_qK_5Ix6xnQe0,104
|
|
17
|
-
edata/providers/datadis.py,sha256=g47D54E-1y_ITLiwMIvz5yKrY0fp4v5GaBK7TH8sOA8,19786
|
|
18
|
-
edata/providers/redata.py,sha256=i5qcbWRPfAzuMd2r4gtcHMmw8SXjN--k1UcnnaTqhYo,3109
|
|
19
|
-
edata/services/bill_service.py,sha256=fYqaJOiAem68l5RiNmHCMz-28FKyyJ6hLHbeyqAm5ug,13258
|
|
20
|
-
edata/services/data_service.py,sha256=51-K-XhAshnlkpNT-GWm8fg4QahOOxWYocORoj764K0,16490
|
|
21
|
-
edata/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
-
edata/tests/test_datadis_connector.py,sha256=OaECc9GfZ1lt-DNirPbphdYXISInyKCL9feBkC24U_8,8847
|
|
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.dev112.dist-info/METADATA,sha256=r2t05BWQ1jDxcZc2nf_sqe99GTuKNrFBPXIMz-iwLQk,48068
|
|
26
|
-
e_data-2.0.0.dev112.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
27
|
-
e_data-2.0.0.dev112.dist-info/top_level.txt,sha256=Ez-fReWtUVTMcFuH0dzCnoKIgMBHgjLaBuba8QiKC7g,6
|
|
28
|
-
e_data-2.0.0.dev112.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|