e-data 2.0.2.dev135__py3-none-any.whl → 2.0.2.dev137__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {e_data-2.0.2.dev135.dist-info → e_data-2.0.2.dev137.dist-info}/METADATA +2 -2
- {e_data-2.0.2.dev135.dist-info → e_data-2.0.2.dev137.dist-info}/RECORD +8 -8
- edata/database/controller.py +8 -1
- edata/database/models.py +21 -20
- edata/tests/test_controller.py +33 -1
- {e_data-2.0.2.dev135.dist-info → e_data-2.0.2.dev137.dist-info}/WHEEL +0 -0
- {e_data-2.0.2.dev135.dist-info → e_data-2.0.2.dev137.dist-info}/licenses/LICENSE +0 -0
- {e_data-2.0.2.dev135.dist-info → e_data-2.0.2.dev137.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.2.
|
|
3
|
+
Version: 2.0.2.dev137
|
|
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
|
|
@@ -697,7 +697,7 @@ Requires-Dist: pydantic<3,>=2.10
|
|
|
697
697
|
Requires-Dist: python_dateutil<3,>=2.8
|
|
698
698
|
Requires-Dist: Requests<3,>=2.31
|
|
699
699
|
Requires-Dist: SQLAlchemy[asyncio]<3,>=2.0
|
|
700
|
-
Requires-Dist: sqlmodel<0.
|
|
700
|
+
Requires-Dist: sqlmodel<0.1,>=0.0.45
|
|
701
701
|
Requires-Dist: typer<1,>=0.12
|
|
702
702
|
Requires-Dist: aiosqlite<1,>=0.21
|
|
703
703
|
Dynamic: license-file
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
e_data-2.0.2.
|
|
1
|
+
e_data-2.0.2.dev137.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=CsBYDggt0grkLb4cebrckOvJyHr9PaGTnlWqT4yjuFo,5215
|
|
4
4
|
edata/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -6,8 +6,8 @@ edata/core/completion.py,sha256=A0ekfu7eCjaLblUYO5DrvaWuDPAO9k1buzy2qdfjdNc,3914
|
|
|
6
6
|
edata/core/const.py,sha256=X9J2f7lt9jXJnghqz0JFi-J7USa3xlYvbuiki70eHNE,43
|
|
7
7
|
edata/core/utils.py,sha256=uQX8GmNjuhof56jiXXoV-Fk-d590OuIoBJbM9ymZyV8,3194
|
|
8
8
|
edata/database/__init__.py,sha256=HP_pDbGLzeMY9Ufm_Dps5cxzW_3YTonT8w8dstG558c,46
|
|
9
|
-
edata/database/controller.py,sha256=
|
|
10
|
-
edata/database/models.py,sha256=
|
|
9
|
+
edata/database/controller.py,sha256=bI3cE1Q4SK9U1Eep_61EhlmyVjgYH87F5lfBLhwSpmg,23211
|
|
10
|
+
edata/database/models.py,sha256=WWZHYgFBgSDqHORxw5z4Vip7HvorMcQbt9oCpV61NJA,5780
|
|
11
11
|
edata/database/queries.py,sha256=XziDSG2V-PVFmvRkj8UrEvgrc9Qc-ShhnxgsPHIsyik,8304
|
|
12
12
|
edata/database/utils.py,sha256=V-FJfyAh8YJBGHB85BbWtG3Ilq09pM9Gl-L86TffB4g,1062
|
|
13
13
|
edata/database/migrations/__init__.py,sha256=P8VXZzfyceOV3GknYXif2HEMB8Bo4B7f7vKdIC02ZO0,1203
|
|
@@ -24,14 +24,14 @@ edata/services/bill_service.py,sha256=KAM78u7TpoVF-KTlB7bqj2Kp-rJERiPPoGTJCc-7rM
|
|
|
24
24
|
edata/services/data_service.py,sha256=JJCdivjrwNt3aLhz3uMBWv06y-EL7UiK2G5EBr97l_8,19374
|
|
25
25
|
edata/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
26
|
edata/tests/test_completion.py,sha256=-iyVt-juU3U1HqMuLUJXJthil2-n_4ddWiJq2GwnXTo,3551
|
|
27
|
-
edata/tests/test_controller.py,sha256=
|
|
27
|
+
edata/tests/test_controller.py,sha256=YvilJ9cIpjhdGGtVJtlEevhRL7IHAVnU0At7AmxM5_8,3983
|
|
28
28
|
edata/tests/test_datadis_connector.py,sha256=8Xet2M2eG-kMhiRDTCSH0R5nYO2oIpQTPOuTZKMKoLo,12538
|
|
29
29
|
edata/tests/test_incremental.py,sha256=dn1LRgjBi2HuNYELPKDVdZQlS01hk4lsP9L-vyvtEHo,3479
|
|
30
30
|
edata/tests/test_migrations.py,sha256=3Io2nokZVdv2xU9fCZCMIvwAwPVtGc0UivClSouP3pw,6418
|
|
31
31
|
edata/tests/test_redata_connector.py,sha256=SBvxsma5t-qvz2drxNisNM2P1STTurqEs1TSDgwaPJ8,508
|
|
32
32
|
edata/tests/test_services.py,sha256=Ya5cbADHj1Qsffb_IuMeDD0BViz8nqrgwazUWK0-hRc,8297
|
|
33
33
|
edata/tests/test_utils.py,sha256=h6yBzT7j3eA42zbXJ-AK9lXcZkjCMf16wnob1oqGvUE,2307
|
|
34
|
-
e_data-2.0.2.
|
|
35
|
-
e_data-2.0.2.
|
|
36
|
-
e_data-2.0.2.
|
|
37
|
-
e_data-2.0.2.
|
|
34
|
+
e_data-2.0.2.dev137.dist-info/METADATA,sha256=-7nOw3zSVBn4BVOgnmEXPt44pUE_qauv2ZZ2yjDjXt4,48084
|
|
35
|
+
e_data-2.0.2.dev137.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
36
|
+
e_data-2.0.2.dev137.dist-info/top_level.txt,sha256=Ez-fReWtUVTMcFuH0dzCnoKIgMBHgjLaBuba8QiKC7g,6
|
|
37
|
+
e_data-2.0.2.dev137.dist-info/RECORD,,
|
edata/database/controller.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import logging
|
|
2
3
|
import os
|
|
3
4
|
import typing
|
|
@@ -93,6 +94,7 @@ class EdataDB:
|
|
|
93
94
|
cls._engine = create_async_engine(db_url, future=True)
|
|
94
95
|
event.listen(cls._engine.sync_engine, "connect", _set_sqlite_pragmas)
|
|
95
96
|
cls._instance._tables_initialized = False
|
|
97
|
+
cls._instance._tables_lock = asyncio.Lock()
|
|
96
98
|
elif db_url != cls._db_url:
|
|
97
99
|
raise ValueError("EdataDB already initialized with a different db_url")
|
|
98
100
|
return cls._instance
|
|
@@ -108,7 +110,12 @@ class EdataDB:
|
|
|
108
110
|
|
|
109
111
|
if self._tables_initialized:
|
|
110
112
|
return
|
|
111
|
-
|
|
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
|
|
112
119
|
async with self.engine.begin() as conn:
|
|
113
120
|
await conn.run_sync(SQLModel.metadata.create_all)
|
|
114
121
|
await conn.run_sync(_create_missing_indexes)
|
edata/database/models.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
from datetime import datetime as dt
|
|
3
3
|
|
|
4
|
+
from pydantic import NaiveDatetime
|
|
4
5
|
from sqlmodel import AutoString, Column, Field, Index, SQLModel, UniqueConstraint
|
|
5
6
|
|
|
6
7
|
from edata.database.utils import PydanticJSON
|
|
@@ -27,8 +28,8 @@ class SupplyModel(SQLModel, table=True):
|
|
|
27
28
|
cups: str = Field(default=None, primary_key=True)
|
|
28
29
|
data: Supply = Field(sa_column=Column(PydanticJSON(Supply)))
|
|
29
30
|
version: int = Field(default=1)
|
|
30
|
-
created_at:
|
|
31
|
-
updated_at:
|
|
31
|
+
created_at: NaiveDatetime = Field(default_factory=_now, nullable=False)
|
|
32
|
+
updated_at: NaiveDatetime = Field(
|
|
32
33
|
default_factory=_now, nullable=False, sa_column_kwargs={"onupdate": dt.now}
|
|
33
34
|
)
|
|
34
35
|
|
|
@@ -44,12 +45,12 @@ class ContractModel(SQLModel, table=True):
|
|
|
44
45
|
|
|
45
46
|
id: int | None = Field(default=None, primary_key=True)
|
|
46
47
|
cups: str = Field(foreign_key="supply.cups", index=True)
|
|
47
|
-
date_start:
|
|
48
|
+
date_start: NaiveDatetime = Field(index=True)
|
|
48
49
|
data: Contract = Field(sa_column=Column(PydanticJSON(Contract)))
|
|
49
50
|
|
|
50
51
|
version: int = Field(default=1)
|
|
51
|
-
created_at:
|
|
52
|
-
updated_at:
|
|
52
|
+
created_at: NaiveDatetime = Field(default_factory=_now, nullable=False)
|
|
53
|
+
updated_at: NaiveDatetime = Field(
|
|
53
54
|
default_factory=_now, nullable=False, sa_column_kwargs={"onupdate": dt.now}
|
|
54
55
|
)
|
|
55
56
|
|
|
@@ -71,13 +72,13 @@ class EnergyModel(SQLModel, table=True):
|
|
|
71
72
|
id: int | None = Field(default=None, primary_key=True)
|
|
72
73
|
cups: str = Field(foreign_key="supply.cups", index=True)
|
|
73
74
|
delta_h: float
|
|
74
|
-
datetime:
|
|
75
|
+
datetime: NaiveDatetime = Field(index=True)
|
|
75
76
|
|
|
76
77
|
data: Energy = Field(sa_column=Column(PydanticJSON(Energy)))
|
|
77
78
|
|
|
78
79
|
version: int = Field(default=1)
|
|
79
|
-
created_at:
|
|
80
|
-
updated_at:
|
|
80
|
+
created_at: NaiveDatetime = Field(default_factory=_now, nullable=False)
|
|
81
|
+
updated_at: NaiveDatetime = Field(
|
|
81
82
|
default_factory=_now, nullable=False, sa_column_kwargs={"onupdate": dt.now}
|
|
82
83
|
)
|
|
83
84
|
|
|
@@ -93,13 +94,13 @@ class PowerModel(SQLModel, table=True):
|
|
|
93
94
|
|
|
94
95
|
id: int | None = Field(default=None, primary_key=True)
|
|
95
96
|
cups: str = Field(foreign_key="supply.cups", index=True)
|
|
96
|
-
datetime:
|
|
97
|
+
datetime: NaiveDatetime = Field(index=True)
|
|
97
98
|
|
|
98
99
|
data: Power = Field(sa_column=Column(PydanticJSON(Power)))
|
|
99
100
|
|
|
100
101
|
version: int = Field(default=1)
|
|
101
|
-
created_at:
|
|
102
|
-
updated_at:
|
|
102
|
+
created_at: NaiveDatetime = Field(default_factory=_now, nullable=False)
|
|
103
|
+
updated_at: NaiveDatetime = Field(
|
|
103
104
|
default_factory=_now, nullable=False, sa_column_kwargs={"onupdate": dt.now}
|
|
104
105
|
)
|
|
105
106
|
|
|
@@ -120,14 +121,14 @@ class StatisticsModel(SQLModel, table=True):
|
|
|
120
121
|
|
|
121
122
|
id: int | None = Field(default=None, primary_key=True)
|
|
122
123
|
cups: str = Field(foreign_key="supply.cups", index=True)
|
|
123
|
-
datetime:
|
|
124
|
+
datetime: NaiveDatetime = Field(index=True)
|
|
124
125
|
type: typing.Literal["day", "month"] = Field(index=True, sa_type=AutoString)
|
|
125
126
|
complete: bool = Field(False)
|
|
126
127
|
data: Statistics = Field(sa_column=Column(PydanticJSON(Statistics)))
|
|
127
128
|
|
|
128
129
|
version: int = Field(default=1)
|
|
129
|
-
created_at:
|
|
130
|
-
updated_at:
|
|
130
|
+
created_at: NaiveDatetime = Field(default_factory=_now, nullable=False)
|
|
131
|
+
updated_at: NaiveDatetime = Field(
|
|
131
132
|
default_factory=_now, nullable=False, sa_column_kwargs={"onupdate": dt.now}
|
|
132
133
|
)
|
|
133
134
|
|
|
@@ -137,12 +138,12 @@ class PVPCModel(SQLModel, table=True):
|
|
|
137
138
|
__tablename__ = "pvpc" # type: ignore
|
|
138
139
|
|
|
139
140
|
id: int | None = Field(default=None, primary_key=True)
|
|
140
|
-
datetime:
|
|
141
|
+
datetime: NaiveDatetime = Field(index=True, unique=True)
|
|
141
142
|
data: EnergyPrice = Field(sa_column=Column(PydanticJSON(EnergyPrice)))
|
|
142
143
|
|
|
143
144
|
version: int = Field(default=1)
|
|
144
|
-
created_at:
|
|
145
|
-
updated_at:
|
|
145
|
+
created_at: NaiveDatetime = Field(default_factory=_now, nullable=False)
|
|
146
|
+
updated_at: NaiveDatetime = Field(
|
|
146
147
|
default_factory=_now, nullable=False, sa_column_kwargs={"onupdate": dt.now}
|
|
147
148
|
)
|
|
148
149
|
|
|
@@ -163,14 +164,14 @@ class BillModel(SQLModel, table=True):
|
|
|
163
164
|
|
|
164
165
|
id: int | None = Field(default=None, primary_key=True)
|
|
165
166
|
cups: str = Field(foreign_key="supply.cups", index=True)
|
|
166
|
-
datetime:
|
|
167
|
+
datetime: NaiveDatetime = Field(index=True)
|
|
167
168
|
type: typing.Literal["hour", "day", "month"] = Field(index=True, sa_type=AutoString)
|
|
168
169
|
complete: bool = Field(False)
|
|
169
170
|
confhash: str
|
|
170
171
|
data: Bill = Field(sa_column=Column(PydanticJSON(Bill)))
|
|
171
172
|
|
|
172
173
|
version: int = Field(default=1)
|
|
173
|
-
created_at:
|
|
174
|
-
updated_at:
|
|
174
|
+
created_at: NaiveDatetime = Field(default_factory=_now, nullable=False)
|
|
175
|
+
updated_at: NaiveDatetime = Field(
|
|
175
176
|
default_factory=_now, nullable=False, sa_column_kwargs={"onupdate": dt.now}
|
|
176
177
|
)
|
edata/tests/test_controller.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Tests for the database controller."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
from collections.abc import AsyncIterator
|
|
4
5
|
from datetime import datetime, timedelta
|
|
5
6
|
|
|
6
7
|
import pytest
|
|
7
8
|
import pytest_asyncio
|
|
9
|
+
from sqlalchemy import DateTime
|
|
10
|
+
from sqlmodel import SQLModel
|
|
8
11
|
|
|
9
12
|
from edata.database.controller import EdataDB
|
|
10
13
|
from edata.models import Bill, Energy, Supply
|
|
@@ -89,3 +92,32 @@ async def test_add_bill_list_applies_overrides(db: EdataDB) -> None:
|
|
|
89
92
|
stored = await db.list_bill(CUPS, "hour")
|
|
90
93
|
assert len(stored) == 3
|
|
91
94
|
assert all(x.complete and x.confhash == "hash-b" for x in stored)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@pytest.mark.asyncio
|
|
98
|
+
async def test_concurrent_first_calls_do_not_race_index_creation(db: EdataDB) -> None:
|
|
99
|
+
# simulate a database created before the index existed, opened fresh
|
|
100
|
+
async with db.engine.begin() as conn:
|
|
101
|
+
await conn.exec_driver_sql("DROP INDEX ix_energy_cups_datetime")
|
|
102
|
+
db._tables_initialized = False
|
|
103
|
+
|
|
104
|
+
await asyncio.gather(*(db.get_last_energy(CUPS) for _ in range(10)))
|
|
105
|
+
|
|
106
|
+
async with db.engine.connect() as conn:
|
|
107
|
+
result = await conn.exec_driver_sql(
|
|
108
|
+
"SELECT name FROM sqlite_master WHERE name='ix_energy_cups_datetime'"
|
|
109
|
+
)
|
|
110
|
+
assert result.first() is not None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_datetime_columns_are_naive() -> None:
|
|
114
|
+
# the supply timezone is unknown, so datetimes are stored as-is (naive);
|
|
115
|
+
# plain ``datetime`` fields map to UTC-aware columns on sqlmodel>=0.0.45
|
|
116
|
+
columns = [
|
|
117
|
+
column
|
|
118
|
+
for table in SQLModel.metadata.sorted_tables
|
|
119
|
+
for column in table.columns
|
|
120
|
+
if isinstance(column.type, DateTime)
|
|
121
|
+
]
|
|
122
|
+
assert len(columns) == 20
|
|
123
|
+
assert all(column.type.timezone is False for column in columns)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|