e-data 2.0.2.dev135__tar.gz → 2.0.2.dev137__tar.gz

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.
Files changed (43) hide show
  1. {e_data-2.0.2.dev135/e_data.egg-info → e_data-2.0.2.dev137}/PKG-INFO +2 -2
  2. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137/e_data.egg-info}/PKG-INFO +2 -2
  3. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/e_data.egg-info/requires.txt +1 -1
  4. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/database/controller.py +8 -1
  5. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/database/models.py +21 -20
  6. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/tests/test_controller.py +33 -1
  7. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/pyproject.toml +2 -2
  8. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/LICENSE +0 -0
  9. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/MANIFEST.in +0 -0
  10. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/README.md +0 -0
  11. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/e_data.egg-info/SOURCES.txt +0 -0
  12. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/e_data.egg-info/dependency_links.txt +0 -0
  13. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/e_data.egg-info/top_level.txt +0 -0
  14. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/__init__.py +0 -0
  15. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/cli.py +0 -0
  16. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/core/__init__.py +0 -0
  17. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/core/completion.py +0 -0
  18. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/core/const.py +0 -0
  19. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/core/utils.py +0 -0
  20. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/database/__init__.py +0 -0
  21. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/database/migrations/__init__.py +0 -0
  22. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/database/migrations/base.py +0 -0
  23. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/database/migrations/legacy_json_1_3_3.py +0 -0
  24. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/database/queries.py +0 -0
  25. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/database/utils.py +0 -0
  26. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/models/__init__.py +0 -0
  27. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/models/bill.py +0 -0
  28. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/models/data.py +0 -0
  29. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/models/supply.py +0 -0
  30. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/providers/__init__.py +0 -0
  31. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/providers/datadis.py +0 -0
  32. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/providers/redata.py +0 -0
  33. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/services/bill_service.py +0 -0
  34. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/services/data_service.py +0 -0
  35. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/tests/__init__.py +0 -0
  36. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/tests/test_completion.py +0 -0
  37. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/tests/test_datadis_connector.py +0 -0
  38. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/tests/test_incremental.py +0 -0
  39. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/tests/test_migrations.py +0 -0
  40. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/tests/test_redata_connector.py +0 -0
  41. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/tests/test_services.py +0 -0
  42. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/edata/tests/test_utils.py +0 -0
  43. {e_data-2.0.2.dev135 → e_data-2.0.2.dev137}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: e-data
3
- Version: 2.0.2.dev135
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.0.45,>=0.0.22
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: e-data
3
- Version: 2.0.2.dev135
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.0.45,>=0.0.22
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
@@ -8,6 +8,6 @@ pydantic<3,>=2.10
8
8
  python_dateutil<3,>=2.8
9
9
  Requests<3,>=2.31
10
10
  SQLAlchemy[asyncio]<3,>=2.0
11
- sqlmodel<0.0.45,>=0.0.22
11
+ sqlmodel<0.1,>=0.0.45
12
12
  typer<1,>=0.12
13
13
  aiosqlite<1,>=0.21
@@ -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
- if self.engine:
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)
@@ -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: dt = Field(default_factory=_now, nullable=False)
31
- updated_at: dt = Field(
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: dt = Field(index=True)
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: dt = Field(default_factory=_now, nullable=False)
52
- updated_at: dt = Field(
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: dt = Field(index=True)
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: dt = Field(default_factory=_now, nullable=False)
80
- updated_at: dt = Field(
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: dt = Field(index=True)
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: dt = Field(default_factory=_now, nullable=False)
102
- updated_at: dt = Field(
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: dt = Field(index=True)
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: dt = Field(default_factory=_now, nullable=False)
130
- updated_at: dt = Field(
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: dt = Field(index=True, unique=True)
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: dt = Field(default_factory=_now, nullable=False)
145
- updated_at: dt = Field(
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: dt = Field(index=True)
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: dt = Field(default_factory=_now, nullable=False)
174
- updated_at: dt = Field(
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
  )
@@ -1,10 +1,13 @@
1
- """Bulk upsert tests for the database controller."""
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)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "e-data"
7
- version = "2.0.2.dev135"
7
+ version = "2.0.2.dev137"
8
8
  description = "Python library for managing spanish energy data from various web providers"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -30,7 +30,7 @@ dependencies = [
30
30
  "python_dateutil>=2.8,<3",
31
31
  "Requests>=2.31,<3",
32
32
  "SQLAlchemy[asyncio]>=2.0,<3",
33
- "sqlmodel>=0.0.22,<0.0.45", # 0.0.45+ rejects naive datetimes
33
+ "sqlmodel>=0.0.45,<0.1", # 0.0.45 added NaiveDatetime columns
34
34
  "typer>=0.12,<1",
35
35
  "aiosqlite>=0.21,<1",
36
36
  ]
File without changes
File without changes
File without changes
File without changes