diracx-db 0.0.1a49__py3-none-any.whl → 0.0.2__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.
diracx/db/__main__.py CHANGED
@@ -39,6 +39,7 @@ async def init_sql():
39
39
  if db._db_url.startswith("sqlite"):
40
40
  await conn.exec_driver_sql("PRAGMA foreign_keys=ON")
41
41
  await conn.run_sync(db.metadata.create_all)
42
+ await db.post_create(conn)
42
43
 
43
44
 
44
45
  async def init_os():
diracx/db/sql/auth/db.py CHANGED
@@ -1,16 +1,21 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import logging
3
4
  import secrets
5
+ from datetime import UTC, datetime
6
+ from itertools import pairwise
4
7
 
5
- from sqlalchemy import insert, select, update
8
+ from dateutil.rrule import MONTHLY, rrule
9
+ from sqlalchemy import insert, select, text, update
6
10
  from sqlalchemy.exc import IntegrityError, NoResultFound
11
+ from sqlalchemy.ext.asyncio import AsyncConnection
7
12
  from uuid_utils import UUID, uuid7
8
13
 
9
14
  from diracx.core.exceptions import (
10
15
  AuthorizationError,
11
16
  TokenNotFoundError,
12
17
  )
13
- from diracx.db.sql.utils import BaseSQLDB, hash, substract_date
18
+ from diracx.db.sql.utils import BaseSQLDB, hash, substract_date, uuid7_from_datetime
14
19
 
15
20
  from .schema import (
16
21
  AuthorizationFlows,
@@ -25,10 +30,72 @@ from .schema import Base as AuthDBBase
25
30
  USER_CODE_ALPHABET = "BCDFGHJKLMNPQRSTVWXZ"
26
31
  MAX_RETRY = 5
27
32
 
33
+ logger = logging.getLogger(__name__)
34
+
28
35
 
29
36
  class AuthDB(BaseSQLDB):
30
37
  metadata = AuthDBBase.metadata
31
38
 
39
+ @classmethod
40
+ async def post_create(cls, conn: AsyncConnection) -> None:
41
+ """Create partitions if it is a MySQL DB and it does not have
42
+ it yet and the table does not have any data yet.
43
+ We do this as a post_create step as sqlalchemy does not support
44
+ partition so well.
45
+ """
46
+ if conn.dialect.name == "mysql":
47
+ check_partition_query = text(
48
+ "SELECT PARTITION_NAME FROM information_schema.partitions "
49
+ "WHERE TABLE_NAME = 'RefreshTokens' AND PARTITION_NAME is not NULL"
50
+ )
51
+ partition_names = (await conn.execute(check_partition_query)).all()
52
+
53
+ if not partition_names:
54
+ # Create a monthly partition from today until 2 years
55
+ # The partition are named p_<year>_<month>
56
+ start_date = datetime.now(tz=UTC).replace(
57
+ day=1, hour=0, minute=0, second=0, microsecond=0
58
+ )
59
+ end_date = start_date.replace(year=start_date.year + 2)
60
+
61
+ dates = [
62
+ dt for dt in rrule(MONTHLY, dtstart=start_date, until=end_date)
63
+ ]
64
+
65
+ partition_list = []
66
+ for name, limit in pairwise(dates):
67
+ partition_list.append(
68
+ f"PARTITION p_{name.year}_{name.month} "
69
+ f"VALUES LESS THAN ('{str(uuid7_from_datetime(limit, randomize=False)).replace('-', '')}')"
70
+ )
71
+ partition_list.append("PARTITION p_future VALUES LESS THAN (MAXVALUE)")
72
+
73
+ alter_query = text(
74
+ f"ALTER TABLE RefreshTokens PARTITION BY RANGE COLUMNS (JTI) ({','.join(partition_list)})"
75
+ )
76
+
77
+ check_table_empty_query = text("SELECT * FROM RefreshTokens LIMIT 1")
78
+ refresh_table_content = (
79
+ await conn.execute(check_table_empty_query)
80
+ ).all()
81
+ if refresh_table_content:
82
+ logger.warning(
83
+ "RefreshTokens table not empty. Run the following query yourself"
84
+ )
85
+ logger.warning(alter_query)
86
+ return
87
+
88
+ await conn.execute(alter_query)
89
+
90
+ partition_names = (
91
+ await conn.execute(
92
+ check_partition_query, {"table_name": "RefreshTokens"}
93
+ )
94
+ ).all()
95
+ assert partition_names, (
96
+ f"There should be partitions now {partition_names}"
97
+ )
98
+
32
99
  async def device_flow_validate_user_code(
33
100
  self, user_code: str, max_validity: int
34
101
  ) -> str:
@@ -10,7 +10,12 @@ from sqlalchemy import (
10
10
  )
11
11
  from sqlalchemy.orm import declarative_base
12
12
 
13
- from diracx.db.sql.utils import Column, DateNowColumn, EnumColumn, NullColumn
13
+ from diracx.db.sql.utils import (
14
+ Column,
15
+ DateNowColumn,
16
+ EnumColumn,
17
+ NullColumn,
18
+ )
14
19
 
15
20
  USER_CODE_LENGTH = 8
16
21
 
@@ -92,7 +97,6 @@ class RefreshTokens(Base):
92
97
  status = EnumColumn(
93
98
  "Status", RefreshTokenStatus, server_default=RefreshTokenStatus.CREATED.name
94
99
  )
95
- creation_time = DateNowColumn("CreationTime", index=True)
96
100
  scope = Column("Scope", String(1024))
97
101
 
98
102
  # User attributes bound to the refresh token
@@ -1,16 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from .base import (
4
- BaseSQLDB,
5
- SQLDBUnavailableError,
6
- _get_columns,
7
- apply_search_filters,
8
- apply_sort_constraints,
9
- )
10
- from .functions import hash, substract_date, utcnow
11
- from .types import Column, DateNowColumn, EnumBackedBool, EnumColumn, NullColumn
12
-
13
- __all__ = (
3
+ __all__ = [
14
4
  "_get_columns",
15
5
  "utcnow",
16
6
  "Column",
@@ -24,4 +14,18 @@ __all__ = (
24
14
  "substract_date",
25
15
  "hash",
26
16
  "SQLDBUnavailableError",
17
+ "uuid7_from_datetime",
18
+ "uuid7_to_datetime",
19
+ ]
20
+
21
+ from .base import (
22
+ BaseSQLDB,
23
+ SQLDBUnavailableError,
24
+ _get_columns,
25
+ apply_search_filters,
26
+ apply_sort_constraints,
27
+ uuid7_from_datetime,
28
+ uuid7_to_datetime,
27
29
  )
30
+ from .functions import hash, substract_date, utcnow
31
+ from .types import Column, DateNowColumn, EnumBackedBool, EnumColumn, NullColumn
@@ -7,13 +7,15 @@ import re
7
7
  from abc import ABCMeta
8
8
  from collections.abc import AsyncIterator
9
9
  from contextvars import ContextVar
10
- from datetime import datetime
10
+ from datetime import datetime, timezone
11
11
  from typing import Any, Self, cast
12
+ from uuid import UUID as StdUUID # noqa: N811
12
13
 
13
14
  from pydantic import TypeAdapter
14
15
  from sqlalchemy import DateTime, MetaData, func, select
15
16
  from sqlalchemy.exc import OperationalError
16
17
  from sqlalchemy.ext.asyncio import AsyncConnection, AsyncEngine, create_async_engine
18
+ from uuid_utils import UUID, uuid7
17
19
 
18
20
  from diracx.core.exceptions import InvalidQueryError
19
21
  from diracx.core.extensions import select_from_extension
@@ -153,6 +155,11 @@ class BaseSQLDB(metaclass=ABCMeta):
153
155
  raise
154
156
  return db_urls
155
157
 
158
+ @classmethod
159
+ async def post_create(cls, conn: AsyncConnection) -> None:
160
+ """Execute actions after the schema has been created."""
161
+ return
162
+
156
163
  @classmethod
157
164
  def transaction(cls) -> Self:
158
165
  raise NotImplementedError("This should never be called")
@@ -416,3 +423,33 @@ def apply_sort_constraints(column_mapping, stmt, sorts):
416
423
  if sort_columns:
417
424
  stmt = stmt.order_by(*sort_columns)
418
425
  return stmt
426
+
427
+
428
+ def uuid7_to_datetime(uuid: UUID | StdUUID | str) -> datetime:
429
+ """Convert a UUIDv7 to a datetime."""
430
+ if isinstance(uuid, StdUUID):
431
+ # Convert stdlib UUID to uuid_utils.UUID
432
+ uuid = UUID(str(uuid))
433
+ elif not isinstance(uuid, UUID):
434
+ # Convert string or other types to uuid_utils.UUID
435
+ uuid = UUID(uuid)
436
+ if uuid.version != 7:
437
+ raise ValueError(f"UUID {uuid} is not a UUIDv7")
438
+ return datetime.fromtimestamp(uuid.timestamp / 1000.0, tz=timezone.utc)
439
+
440
+
441
+ def uuid7_from_datetime(dt: datetime, *, randomize: bool = True) -> UUID:
442
+ """Generate a UUIDv7 corresponding to the given datetime.
443
+
444
+ If randomize is True, the standard uuid7 function is used resulting in the
445
+ lowest 62-bits being random. If randomize is False, the UUIDv7 will be the
446
+ lowest possible UUIDv7 for the given datetime.
447
+ """
448
+ timestamp = dt.timestamp()
449
+ if randomize:
450
+ uuid = uuid7(int(timestamp), int((timestamp % 1) * 1e9))
451
+ else:
452
+ time_high = int(timestamp * 1000) >> 16
453
+ time_low = int(timestamp * 1000) & 0xFFFF
454
+ uuid = UUID.from_fields((time_high, time_low, 0x7000, 0x80, 0, 0))
455
+ return uuid
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diracx-db
3
- Version: 0.0.1a49
3
+ Version: 0.0.2
4
4
  Summary: TODO
5
5
  License: GPL-3.0-only
6
6
  Classifier: Intended Audience :: Science/Research
@@ -12,8 +12,11 @@ Requires-Python: >=3.11
12
12
  Requires-Dist: diracx-core
13
13
  Requires-Dist: opensearch-py[async]
14
14
  Requires-Dist: pydantic>=2.10
15
+ Requires-Dist: python-dateutil
15
16
  Requires-Dist: sqlalchemy[aiomysql,aiosqlite]>=2
16
17
  Requires-Dist: uuid-utils
17
18
  Provides-Extra: testing
18
19
  Requires-Dist: diracx-testing; extra == 'testing'
19
20
  Requires-Dist: freezegun; extra == 'testing'
21
+ Provides-Extra: types
22
+ Requires-Dist: types-python-dateutil; extra == 'types'
@@ -1,5 +1,5 @@
1
1
  diracx/db/__init__.py,sha256=2oeUeVwZq53bo_ZOflEYZsBn7tcR5Tzb2AIu0TAWELM,109
2
- diracx/db/__main__.py,sha256=3yaUP1ig-yaPSQM4wy6CtSXXHivQg-hIz2FeBt7joBc,1714
2
+ diracx/db/__main__.py,sha256=6YlmpiU1cLLHjKLy1DfdEOQUyvSla-MbJsJ7aQwAOVs,1757
3
3
  diracx/db/exceptions.py,sha256=1nn-SZLG-nQwkxbvHjZqXhE5ouzWj1f3qhSda2B4ZEg,83
4
4
  diracx/db/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  diracx/db/os/__init__.py,sha256=IZr6z6SefrRvuC8sTC4RmB3_wwOyEt1GzpDuwSMH8O4,112
@@ -7,8 +7,8 @@ diracx/db/os/job_parameters.py,sha256=3w_CeA2z-cY5pWwXkGu-Fod27FobbUXuwVKK-jN037
7
7
  diracx/db/os/utils.py,sha256=V4T-taos64SFNcorfIr7mq5l5y88K6TzyCj1YqWk8VI,11562
8
8
  diracx/db/sql/__init__.py,sha256=JYu0b0IVhoXy3lX2m2r2dmAjsRS7IbECBUMEDvX0Te4,391
9
9
  diracx/db/sql/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- diracx/db/sql/auth/db.py,sha256=QJtBqMrhOf97UvMG0WpyjsgIRiu19v04FoDzXAyXtT0,8952
11
- diracx/db/sql/auth/schema.py,sha256=x2PEbmM_bNPdZUN5BMGMrdSmX8zkDeJ3P9XfhLBGBTs,3173
10
+ diracx/db/sql/auth/db.py,sha256=sGXKFLy1iCdrkg1952_F-c7nRSIx4F1JuDloIV6AXN8,11814
11
+ diracx/db/sql/auth/schema.py,sha256=9fUV7taDPnoAcoiwRAmQraOmF2Ytoizjs2TFvN7zsVs,3132
12
12
  diracx/db/sql/dummy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  diracx/db/sql/dummy/db.py,sha256=MKSUSJI1BlRgK08tjCfkCkOz02asvJAeBw60pAdiGV8,1212
14
14
  diracx/db/sql/dummy/schema.py,sha256=9zI53pKlzc6qBezsyjkatOQrNZdGCjwgjQ8Iz_pyAXs,789
@@ -27,11 +27,11 @@ diracx/db/sql/sandbox_metadata/schema.py,sha256=V5gV2PHwzTbBz_th9ribLfE7Lqk8YGem
27
27
  diracx/db/sql/task_queue/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
28
  diracx/db/sql/task_queue/db.py,sha256=2qul1D2tX2uCI92N591WK5xWHakG0pNibzDwKQ7W-I8,6246
29
29
  diracx/db/sql/task_queue/schema.py,sha256=5efAgvNYRkLlaJ2NzRInRfmVa3tyIzQu2l0oRPy4Kzw,3258
30
- diracx/db/sql/utils/__init__.py,sha256=XYbv-AJAPl7bb8dETpjc07olmtXQ0h1MFUbLqjAphQE,585
31
- diracx/db/sql/utils/base.py,sha256=Dn7a-ICuFNTT3w0QtbS63uF1S4Wnn5NK3cu6Pzn4a6A,15641
30
+ diracx/db/sql/utils/__init__.py,sha256=k1DI4Idlqv36pXn2BhQysb947Peio9DnYaePslkTpUQ,685
31
+ diracx/db/sql/utils/base.py,sha256=cXwAzWtYtEkukjHSBGaGNtXSozHfXHpZ9p9eXdQTpDY,17065
32
32
  diracx/db/sql/utils/functions.py,sha256=_E4tc9Gti6LuSh7QEyoqPJSvCuByVqvRenOXCzxsulE,4014
33
33
  diracx/db/sql/utils/types.py,sha256=KNZWJfpvHTjfIPg6Nn7zY-rS0q3ybnirHcTcLAYSYbE,5118
34
- diracx_db-0.0.1a49.dist-info/METADATA,sha256=hPW3mb1Ain8hSsRRDWlL3kGeRNKpYc1bHtJ9hlSALIE,675
35
- diracx_db-0.0.1a49.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
36
- diracx_db-0.0.1a49.dist-info/entry_points.txt,sha256=UPqhLvb9gui0kOyWeI_edtefcrHToZmQt1p76vIwujo,317
37
- diracx_db-0.0.1a49.dist-info/RECORD,,
34
+ diracx_db-0.0.2.dist-info/METADATA,sha256=n8QuMXRrA6-iLKw14y29Jj2WxBzQZ2plW5qMngBnEqs,780
35
+ diracx_db-0.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
36
+ diracx_db-0.0.2.dist-info/entry_points.txt,sha256=UPqhLvb9gui0kOyWeI_edtefcrHToZmQt1p76vIwujo,317
37
+ diracx_db-0.0.2.dist-info/RECORD,,