diracx-db 0.0.1a47__py3-none-any.whl → 0.0.1a49__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/sql/job/db.py CHANGED
@@ -5,10 +5,11 @@ __all__ = ["JobDB"]
5
5
  from datetime import datetime, timezone
6
6
  from typing import TYPE_CHECKING, Any, Iterable
7
7
 
8
- from sqlalchemy import bindparam, case, delete, insert, select, update
8
+ from sqlalchemy import bindparam, case, delete, literal, select, update
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from sqlalchemy.sql.elements import BindParameter
12
+ from sqlalchemy.sql import expression
12
13
 
13
14
  from diracx.core.exceptions import InvalidQueryError
14
15
  from diracx.core.models import JobCommand, SearchSpec, SortSpec
@@ -128,27 +129,14 @@ class JobDB(BaseSQLDB):
128
129
  ],
129
130
  )
130
131
 
131
- @staticmethod
132
- def _set_job_attributes_fix_value(column, value):
133
- """Apply corrections to the values before inserting them into the database.
134
-
135
- TODO: Move this logic into the sqlalchemy model.
136
- """
137
- if column == "VerifiedFlag":
138
- value_str = str(value)
139
- if value_str in ("True", "False"):
140
- return value_str
141
- if column == "AccountedFlag":
142
- value_str = str(value)
143
- if value_str in ("True", "False", "Failed"):
144
- return value_str
145
- else:
146
- return value
147
- raise NotImplementedError(f"Unrecognized value for column {column}: {value}")
148
-
149
132
  async def set_job_attributes(self, job_data):
150
133
  """Update the parameters of the given jobs."""
151
134
  # TODO: add myDate and force parameters.
135
+
136
+ if not job_data:
137
+ # nothing to do!
138
+ raise ValueError("job_data is empty")
139
+
152
140
  for job_id in job_data.keys():
153
141
  if "Status" in job_data[job_id]:
154
142
  job_data[job_id].update(
@@ -160,7 +148,11 @@ class JobDB(BaseSQLDB):
160
148
  *[
161
149
  (
162
150
  Jobs.__table__.c.JobID == job_id,
163
- self._set_job_attributes_fix_value(column, attrs[column]),
151
+ # Since the setting of the new column value is obscured by the CASE statement,
152
+ # ensure that SQLAlchemy renders the new column value with the correct type
153
+ literal(attrs[column], type_=Jobs.__table__.c[column].type)
154
+ if not isinstance(attrs[column], expression.FunctionElement)
155
+ else attrs[column],
164
156
  )
165
157
  for job_id, attrs in job_data.items()
166
158
  if column in attrs
@@ -193,7 +185,7 @@ class JobDB(BaseSQLDB):
193
185
  async def set_job_commands(self, commands: list[tuple[int, str, str]]) -> None:
194
186
  """Store a command to be passed to the job together with the next heart beat."""
195
187
  await self.conn.execute(
196
- insert(JobCommands),
188
+ JobCommands.__table__.insert(),
197
189
  [
198
190
  {
199
191
  "JobID": job_id,
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import sqlalchemy.types as types
4
4
  from sqlalchemy import (
5
- DateTime,
6
5
  ForeignKey,
7
6
  Index,
8
7
  Integer,
@@ -11,6 +10,8 @@ from sqlalchemy import (
11
10
  )
12
11
  from sqlalchemy.orm import declarative_base
13
12
 
13
+ from diracx.db.sql.utils.types import SmarterDateTime
14
+
14
15
  from ..utils import Column, EnumBackedBool, NullColumn
15
16
 
16
17
  JobDBBase = declarative_base()
@@ -19,11 +20,8 @@ JobDBBase = declarative_base()
19
20
  class AccountedFlagEnum(types.TypeDecorator):
20
21
  """Maps a ``AccountedFlagEnum()`` column to True/False in Python."""
21
22
 
22
- impl = types.Enum
23
- cache_ok: bool = True
24
-
25
- def __init__(self) -> None:
26
- super().__init__("True", "False", "Failed")
23
+ impl = types.Enum("True", "False", "Failed", name="accounted_flag_enum")
24
+ cache_ok = True
27
25
 
28
26
  def process_bind_param(self, value, dialect) -> str:
29
27
  if value is True:
@@ -63,12 +61,30 @@ class Jobs(JobDBBase):
63
61
  owner = Column("Owner", String(64), default="Unknown")
64
62
  owner_group = Column("OwnerGroup", String(128), default="Unknown")
65
63
  vo = Column("VO", String(32))
66
- submission_time = NullColumn("SubmissionTime", DateTime)
67
- reschedule_time = NullColumn("RescheduleTime", DateTime)
68
- last_update_time = NullColumn("LastUpdateTime", DateTime)
69
- start_exec_time = NullColumn("StartExecTime", DateTime)
70
- heart_beat_time = NullColumn("HeartBeatTime", DateTime)
71
- end_exec_time = NullColumn("EndExecTime", DateTime)
64
+ submission_time = NullColumn(
65
+ "SubmissionTime",
66
+ SmarterDateTime(),
67
+ )
68
+ reschedule_time = NullColumn(
69
+ "RescheduleTime",
70
+ SmarterDateTime(),
71
+ )
72
+ last_update_time = NullColumn(
73
+ "LastUpdateTime",
74
+ SmarterDateTime(),
75
+ )
76
+ start_exec_time = NullColumn(
77
+ "StartExecTime",
78
+ SmarterDateTime(),
79
+ )
80
+ heart_beat_time = NullColumn(
81
+ "HeartBeatTime",
82
+ SmarterDateTime(),
83
+ )
84
+ end_exec_time = NullColumn(
85
+ "EndExecTime",
86
+ SmarterDateTime(),
87
+ )
72
88
  status = Column("Status", String(32), default="Received")
73
89
  minor_status = Column("MinorStatus", String(128), default="Unknown")
74
90
  application_status = Column("ApplicationStatus", String(255), default="Unknown")
@@ -143,7 +159,11 @@ class HeartBeatLoggingInfo(JobDBBase):
143
159
  )
144
160
  name = Column("Name", String(100), primary_key=True)
145
161
  value = Column("Value", Text)
146
- heart_beat_time = Column("HeartBeatTime", DateTime, primary_key=True)
162
+ heart_beat_time = Column(
163
+ "HeartBeatTime",
164
+ SmarterDateTime(),
165
+ primary_key=True,
166
+ )
147
167
 
148
168
 
149
169
  class JobCommands(JobDBBase):
@@ -154,5 +174,12 @@ class JobCommands(JobDBBase):
154
174
  command = Column("Command", String(100))
155
175
  arguments = Column("Arguments", String(100))
156
176
  status = Column("Status", String(64), default="Received")
157
- reception_time = Column("ReceptionTime", DateTime, primary_key=True)
158
- execution_time = NullColumn("ExecutionTime", DateTime)
177
+ reception_time = Column(
178
+ "ReceptionTime",
179
+ SmarterDateTime(),
180
+ primary_key=True,
181
+ )
182
+ execution_time = NullColumn(
183
+ "ExecutionTime",
184
+ SmarterDateTime(),
185
+ )
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from sqlalchemy import (
4
- DateTime,
5
4
  Double,
6
5
  Index,
7
6
  Integer,
@@ -10,6 +9,8 @@ from sqlalchemy import (
10
9
  )
11
10
  from sqlalchemy.orm import declarative_base
12
11
 
12
+ from diracx.db.sql.utils.types import SmarterDateTime
13
+
13
14
  from ..utils import Column, EnumBackedBool, NullColumn
14
15
 
15
16
  PilotAgentsDBBase = declarative_base()
@@ -29,8 +30,8 @@ class PilotAgents(PilotAgentsDBBase):
29
30
  vo = Column("VO", String(128))
30
31
  grid_type = Column("GridType", String(32), default="LCG")
31
32
  benchmark = Column("BenchMark", Double, default=0.0)
32
- submission_time = NullColumn("SubmissionTime", DateTime)
33
- last_update_time = NullColumn("LastUpdateTime", DateTime)
33
+ submission_time = NullColumn("SubmissionTime", SmarterDateTime)
34
+ last_update_time = NullColumn("LastUpdateTime", SmarterDateTime)
34
35
  status = Column("Status", String(32), default="Unknown")
35
36
  status_reason = Column("StatusReason", String(255), default="Unknown")
36
37
  accounting_sent = Column("AccountingSent", EnumBackedBool(), default=False)
@@ -47,7 +48,7 @@ class JobToPilotMapping(PilotAgentsDBBase):
47
48
 
48
49
  pilot_id = Column("PilotID", Integer, primary_key=True)
49
50
  job_id = Column("JobID", Integer, primary_key=True)
50
- start_time = Column("StartTime", DateTime)
51
+ start_time = Column("StartTime", SmarterDateTime)
51
52
 
52
53
  __table_args__ = (Index("JobID", "JobID"), Index("PilotID", "PilotID"))
53
54
 
@@ -24,6 +24,7 @@ from diracx.core.models import (
24
24
  )
25
25
  from diracx.core.settings import SqlalchemyDsn
26
26
  from diracx.db.exceptions import DBUnavailableError
27
+ from diracx.db.sql.utils.types import SmarterDateTime
27
28
 
28
29
  from .functions import date_trunc
29
30
 
@@ -345,7 +346,7 @@ def apply_search_filters(column_mapping, stmt, search):
345
346
  except KeyError as e:
346
347
  raise InvalidQueryError(f"Unknown column {query['parameter']}") from e
347
348
 
348
- if isinstance(column.type, DateTime):
349
+ if isinstance(column.type, (DateTime, SmarterDateTime)):
349
350
  if "value" in query and isinstance(query["value"], str):
350
351
  resolution, value = find_time_resolution(query["value"])
351
352
  if resolution:
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from datetime import datetime
3
4
  from functools import partial
5
+ from zoneinfo import ZoneInfo
4
6
 
5
7
  import sqlalchemy.types as types
6
8
  from sqlalchemy import Column as RawColumn
@@ -20,11 +22,8 @@ def EnumColumn(name, enum_type, **kwargs): # noqa: N802
20
22
  class EnumBackedBool(types.TypeDecorator):
21
23
  """Maps a ``EnumBackedBool()`` column to True/False in Python."""
22
24
 
23
- impl = types.Enum
24
- cache_ok: bool = True
25
-
26
- def __init__(self) -> None:
27
- super().__init__("True", "False")
25
+ impl = types.Enum("True", "False", name="enum_backed_bool")
26
+ cache_ok = True
28
27
 
29
28
  def process_bind_param(self, value, dialect) -> str:
30
29
  if value is True:
@@ -41,3 +40,98 @@ class EnumBackedBool(types.TypeDecorator):
41
40
  return False
42
41
  else:
43
42
  raise NotImplementedError(f"Unknown {value=}")
43
+
44
+
45
+ class SmarterDateTime(types.TypeDecorator):
46
+ """A DateTime type that also accepts ISO8601 strings.
47
+
48
+ Takes into account converting timezone aware datetime objects into
49
+ naive form and back when needed.
50
+
51
+ """
52
+
53
+ impl = DateTime()
54
+ cache_ok = True
55
+
56
+ def __init__(
57
+ self,
58
+ stored_tz: ZoneInfo | None = ZoneInfo("UTC"),
59
+ returned_tz: ZoneInfo = ZoneInfo("UTC"),
60
+ stored_naive_sqlite=True,
61
+ stored_naive_mysql=True,
62
+ stored_naive_postgres=False, # Forces timezone-awareness
63
+ ):
64
+ self._stored_naive_dialect = {
65
+ "sqlite": stored_naive_sqlite,
66
+ "mysql": stored_naive_mysql,
67
+ "postgres": stored_naive_postgres,
68
+ }
69
+ self._stored_tz: ZoneInfo | None = stored_tz # None = Local timezone
70
+ self._returned_tz: ZoneInfo = returned_tz
71
+
72
+ def _stored_naive(self, dialect):
73
+ if dialect.name not in self._stored_naive_dialect:
74
+ raise NotImplementedError(dialect.name)
75
+ return self._stored_naive_dialect.get(dialect.name)
76
+
77
+ def process_bind_param(self, value, dialect):
78
+ if value is None:
79
+ return None
80
+
81
+ if isinstance(value, str):
82
+ try:
83
+ value: datetime = datetime.fromisoformat(value)
84
+ except ValueError as err:
85
+ raise ValueError(f"Unable to parse datetime string: {value}") from err
86
+
87
+ if not isinstance(value, datetime):
88
+ raise ValueError(f"Expected datetime or ISO8601 string, but got {value!r}")
89
+
90
+ if not value.tzinfo:
91
+ raise ValueError(
92
+ f"Provided timestamp {value=} has no tzinfo -"
93
+ " this is problematic and may cause inconsistencies in stored timestamps.\n"
94
+ " Please always work with tz-aware datetimes / attach tzinfo to your datetime objects:"
95
+ " e.g. datetime.now(tz=timezone.utc) or use datetime_obj.astimezone() with no arguments if you need to "
96
+ "attach the local timezone to a local naive timestamp."
97
+ )
98
+
99
+ # Check that we need to convert the timezone to match self._stored_tz timezone:
100
+ if self._stored_naive(dialect):
101
+ # if self._stored_tz is None, we use our local/system timezone.
102
+ stored_tz = self._stored_tz
103
+
104
+ # astimezone converts to the stored timezone (local timezone if None)
105
+ # replace strips the TZ info --> naive datetime object
106
+ value = value.astimezone(tz=stored_tz).replace(tzinfo=None)
107
+
108
+ return value
109
+
110
+ def process_result_value(self, value, dialect):
111
+ if value is None:
112
+ return None
113
+ if not isinstance(value, datetime):
114
+ raise NotImplementedError(f"{value=} not a datetime object")
115
+
116
+ if self._stored_naive(dialect):
117
+ # Here we add back the tzinfo to the naive timestamp
118
+ # from the DB to make it aware again.
119
+ if value.tzinfo is None:
120
+ # we are definitely given a naive timestamp, so handle it.
121
+ # add back the timezone info if stored_tz is set
122
+ if self._stored_tz:
123
+ value = value.replace(tzinfo=self._stored_tz)
124
+ else:
125
+ # if stored as a local time, add back the system timezone info...
126
+ value = value.astimezone()
127
+ else:
128
+ raise ValueError(
129
+ f"stored_naive is True for {dialect.name=}, but the database engine returned "
130
+ "a tz-aware datetime. You need to check the SQLAlchemy model is consistent with the DB schema."
131
+ )
132
+
133
+ # finally, convert the datetime according to the "returned_tz"
134
+ value = value.astimezone(self._returned_tz)
135
+
136
+ # phew...
137
+ return value
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diracx-db
3
- Version: 0.0.1a47
3
+ Version: 0.0.1a49
4
4
  Summary: TODO
5
5
  License: GPL-3.0-only
6
6
  Classifier: Intended Audience :: Science/Research
@@ -13,14 +13,14 @@ diracx/db/sql/dummy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
13
13
  diracx/db/sql/dummy/db.py,sha256=MKSUSJI1BlRgK08tjCfkCkOz02asvJAeBw60pAdiGV8,1212
14
14
  diracx/db/sql/dummy/schema.py,sha256=9zI53pKlzc6qBezsyjkatOQrNZdGCjwgjQ8Iz_pyAXs,789
15
15
  diracx/db/sql/job/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- diracx/db/sql/job/db.py,sha256=TsHbMVUO-87228hVbodGQclTgY2b7fI0XBsbNbCVgc4,10298
17
- diracx/db/sql/job/schema.py,sha256=eFgZshe6NEzOM2qI0HI9Y3abrqDMoQIwa9L0vZugHcU,5431
16
+ diracx/db/sql/job/db.py,sha256=bX-4OMyW4h9tqeTE3OvonxTXlL6j_Qvv9uEtK5SthN8,10120
17
+ diracx/db/sql/job/schema.py,sha256=fJdmiLp6psdAjo_CoBfSAGSYk2NJkSBwvik9tznESD0,5740
18
18
  diracx/db/sql/job_logging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  diracx/db/sql/job_logging/db.py,sha256=hyklARuEj3R1sSJ8UaObRprmsRx7RjbKAcbfgT9BwRg,5496
20
20
  diracx/db/sql/job_logging/schema.py,sha256=k6uBw-RHAcJ5GEleNpiWoXEJBhCiNG-y4xAgBKHZjjM,2524
21
21
  diracx/db/sql/pilot_agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  diracx/db/sql/pilot_agents/db.py,sha256=6CQ0QGV4NhsGKVCygEtE4kmIjT89xJwrIMuYZTslWFE,1231
23
- diracx/db/sql/pilot_agents/schema.py,sha256=KeWnFSpYOTrT3-_rOCFjbjNnPNXKnUZiJVsu4vv5U2U,2149
23
+ diracx/db/sql/pilot_agents/schema.py,sha256=BTFLuiwcxAvAtTvTP9C7DbGtXoM-IHVDG9k7HMx62AA,2211
24
24
  diracx/db/sql/sandbox_metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  diracx/db/sql/sandbox_metadata/db.py,sha256=FtyPx6GAGJAH-lmuw8PQj6_KGHG6t3AC3-E9uWf-JNs,10236
26
26
  diracx/db/sql/sandbox_metadata/schema.py,sha256=V5gV2PHwzTbBz_th9ribLfE7Lqk8YGemDmvqq4jWQJ4,1530
@@ -28,10 +28,10 @@ diracx/db/sql/task_queue/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
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
30
  diracx/db/sql/utils/__init__.py,sha256=XYbv-AJAPl7bb8dETpjc07olmtXQ0h1MFUbLqjAphQE,585
31
- diracx/db/sql/utils/base.py,sha256=snZFJmUJV-wweZLpio29MxuPFghfugpVMDC1iE_jM7w,15568
31
+ diracx/db/sql/utils/base.py,sha256=Dn7a-ICuFNTT3w0QtbS63uF1S4Wnn5NK3cu6Pzn4a6A,15641
32
32
  diracx/db/sql/utils/functions.py,sha256=_E4tc9Gti6LuSh7QEyoqPJSvCuByVqvRenOXCzxsulE,4014
33
- diracx/db/sql/utils/types.py,sha256=yU-tXsu6hFGPsr9ba1n3ZjGPnHQI_06lbpkTeDCWJtg,1287
34
- diracx_db-0.0.1a47.dist-info/METADATA,sha256=z9GxhxY4-mwWkwLQ1Ue72mU28cXPAB2W4tr_UU2J6yA,675
35
- diracx_db-0.0.1a47.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
36
- diracx_db-0.0.1a47.dist-info/entry_points.txt,sha256=UPqhLvb9gui0kOyWeI_edtefcrHToZmQt1p76vIwujo,317
37
- diracx_db-0.0.1a47.dist-info/RECORD,,
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,,