libres 0.7.3__py3-none-any.whl → 0.9.0__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.
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import sedate
2
4
 
3
5
  from libres.db.models.types import UTCDateTime
@@ -6,8 +8,8 @@ from sqlalchemy.orm import deferred
6
8
  from sqlalchemy.schema import Column
7
9
 
8
10
 
9
- import typing as _t
10
- if _t.TYPE_CHECKING:
11
+ from typing import TYPE_CHECKING
12
+ if TYPE_CHECKING:
11
13
  from datetime import datetime
12
14
 
13
15
 
@@ -22,16 +24,16 @@ class TimestampMixin:
22
24
  """
23
25
 
24
26
  @staticmethod
25
- def timestamp() -> 'datetime':
27
+ def timestamp() -> datetime:
26
28
  return sedate.utcnow()
27
29
 
28
- if _t.TYPE_CHECKING:
30
+ if TYPE_CHECKING:
29
31
  created: Column[datetime]
30
- modified: Column[_t.Optional[datetime]]
32
+ modified: Column[datetime | None]
31
33
 
32
34
  else:
33
35
  @declared_attr
34
- def created(cls) -> 'Column[datetime]':
36
+ def created(cls) -> Column[datetime]:
35
37
  return deferred(
36
38
  Column(
37
39
  UTCDateTime(timezone=False),
@@ -40,7 +42,7 @@ class TimestampMixin:
40
42
  )
41
43
 
42
44
  @declared_attr
43
- def modified(cls) -> 'Column[_t.Optional[datetime]]':
45
+ def modified(cls) -> Column[datetime | None]:
44
46
  return deferred(
45
47
  Column(
46
48
  UTCDateTime(timezone=False),
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from .json_type import JSON
2
4
  from .utcdatetime import UTCDateTime
3
5
  from .uuid_type import UUID
@@ -1,48 +1,46 @@
1
- from json import loads, dumps
2
- from sqlalchemy.types import TypeDecorator, TEXT
1
+ from __future__ import annotations
3
2
 
3
+ from sqlalchemy.ext.mutable import MutableDict
4
+ from sqlalchemy.types import TypeDecorator
5
+ from sqlalchemy.dialects.postgresql import JSONB
4
6
 
5
- import typing as _t
6
- if _t.TYPE_CHECKING:
7
+
8
+ from typing import Any
9
+ from typing import TYPE_CHECKING
10
+ if TYPE_CHECKING:
7
11
  from sqlalchemy.engine import Dialect
8
12
 
9
- _Base = TypeDecorator[_t.Any]
13
+ _Base = TypeDecorator[dict[str, Any]]
10
14
  else:
11
15
  _Base = TypeDecorator
12
16
 
13
17
 
14
18
  class JSON(_Base):
15
- """Like the default JSON, but using the json serializer from the dialect
16
- (postgres) each time the value is read, even if it never left the ORM. The
17
- default json type will only do it when the record is read from the
18
- database.
19
+ """ A JSONB based type that coerces None's to empty dictionaries.
20
+
21
+ That is, this JSONB column cannot be `'null'::jsonb`. It could
22
+ still be `NULL` though, if it's nullable and never explicitly
23
+ set. But on the Python end you should always see a dictionary.
19
24
 
20
25
  """
21
26
 
22
- # Use TEXT for now to stay compatible with Postgres 9.1. In the future
23
- # this will be replaced by JSON (or JSONB) though that requires that we
24
- # require a later Postgres release. For now we stay backwards compatible
25
- # with a version that's still widely used (9.1).
26
- impl = TEXT
27
+ impl = JSONB
27
28
 
28
- def process_bind_param(
29
+ def process_bind_param( # type:ignore[override]
29
30
  self,
30
- value: _t.Any,
31
- dialect: 'Dialect'
32
- ) -> _t.Optional[str]:
31
+ value: dict[str, Any] | None,
32
+ dialect: Dialect
33
+ ) -> dict[str, Any]:
33
34
 
34
- if value is not None:
35
- value = (dialect._json_serializer or dumps)(value) # type:ignore
36
-
37
- return value
35
+ return {} if value is None else value
38
36
 
39
37
  def process_result_value(
40
38
  self,
41
- value: _t.Optional[str],
42
- dialect: 'Dialect'
43
- ) -> _t.Optional[_t.Any]:
39
+ value: dict[str, Any] | None,
40
+ dialect: Dialect
41
+ ) -> dict[str, Any]:
42
+
43
+ return {} if value is None else value
44
44
 
45
- if value is not None:
46
- value = (dialect._json_deserializer or loads)(value) # type:ignore
47
45
 
48
- return value
46
+ MutableDict.associate_with(JSON) # type:ignore[no-untyped-call]
@@ -1,10 +1,12 @@
1
+ from __future__ import annotations
2
+
1
3
  import sedate
2
4
 
3
5
  from sqlalchemy import types
4
6
 
5
7
 
6
- import typing as _t
7
- if _t.TYPE_CHECKING:
8
+ from typing import TYPE_CHECKING
9
+ if TYPE_CHECKING:
8
10
  from datetime import datetime
9
11
  from sqlalchemy.engine import Dialect
10
12
 
@@ -29,9 +31,9 @@ class UTCDateTime(_Base):
29
31
 
30
32
  def process_bind_param( # type:ignore[override]
31
33
  self,
32
- value: _t.Optional['datetime'],
33
- dialect: 'Dialect'
34
- ) -> _t.Optional['datetime']:
34
+ value: datetime | None,
35
+ dialect: Dialect
36
+ ) -> datetime | None:
35
37
 
36
38
  if value is not None:
37
39
  return sedate.to_timezone(value, 'UTC').replace(tzinfo=None)
@@ -39,9 +41,9 @@ class UTCDateTime(_Base):
39
41
 
40
42
  def process_result_value(
41
43
  self,
42
- value: _t.Optional['datetime'],
43
- dialect: 'Dialect'
44
- ) -> _t.Optional['datetime']:
44
+ value: datetime | None,
45
+ dialect: Dialect
46
+ ) -> datetime | None:
45
47
 
46
48
  if value is not None:
47
49
  return sedate.replace_timezone(value, 'UTC')
@@ -1,11 +1,13 @@
1
+ from __future__ import annotations
2
+
1
3
  import uuid
2
4
 
3
5
  from sqlalchemy.types import TypeDecorator
4
6
  from sqlalchemy.dialects import postgresql
5
7
 
6
8
 
7
- import typing as _t
8
- if _t.TYPE_CHECKING:
9
+ from typing import TYPE_CHECKING
10
+ if TYPE_CHECKING:
9
11
  from sqlalchemy.engine import Dialect
10
12
 
11
13
  _Base = TypeDecorator['SoftUUID']
@@ -46,9 +48,9 @@ class UUID(_Base):
46
48
 
47
49
  def process_bind_param(
48
50
  self,
49
- value: _t.Optional[uuid.UUID],
50
- dialect: 'Dialect'
51
- ) -> _t.Optional[str]:
51
+ value: uuid.UUID | None,
52
+ dialect: Dialect
53
+ ) -> str | None:
52
54
 
53
55
  if value is not None:
54
56
  return str(value)
@@ -56,9 +58,9 @@ class UUID(_Base):
56
58
 
57
59
  def process_result_value(
58
60
  self,
59
- value: _t.Optional[str],
60
- dialect: 'Dialect'
61
- ) -> _t.Optional[SoftUUID]:
61
+ value: str | None,
62
+ dialect: Dialect
63
+ ) -> SoftUUID | None:
62
64
  if value is not None:
63
65
  # Postgres always returns the uuid in the same format, so we
64
66
  # can turn it into an int immediately, avoiding some checks
libres/db/queries.py CHANGED
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import logging
2
4
  import sedate
3
5
 
@@ -11,14 +13,17 @@ from sqlalchemy.orm import joinedload
11
13
  from sqlalchemy.sql import and_, or_
12
14
 
13
15
 
14
- import typing as _t
15
- if _t.TYPE_CHECKING:
16
+ from typing import TypeVar
17
+ from typing import TYPE_CHECKING
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Collection
20
+ from collections.abc import Iterable
16
21
  from sqlalchemy.orm import Query
17
22
  from uuid import UUID
18
23
 
19
24
  from libres.context.core import Context
20
25
 
21
- _T = _t.TypeVar('_T')
26
+ _T = TypeVar('_T')
22
27
 
23
28
 
24
29
  log = logging.getLogger('libres')
@@ -34,24 +39,24 @@ class Queries(ContextServicesMixin):
34
39
 
35
40
  """
36
41
 
37
- def __init__(self, context: 'Context'):
42
+ def __init__(self, context: Context):
38
43
  self.context = context
39
44
 
40
45
  def all_allocations_in_range(
41
46
  self,
42
47
  start: datetime,
43
48
  end: datetime
44
- ) -> 'Query[Allocation]':
49
+ ) -> Query[Allocation]:
45
50
  return self.allocations_in_range(
46
51
  self.session.query(Allocation), start, end
47
52
  )
48
53
 
49
54
  @staticmethod
50
55
  def allocations_in_range(
51
- query: 'Query[_T]',
56
+ query: Query[_T],
52
57
  start: datetime,
53
58
  end: datetime
54
- ) -> 'Query[_T]':
59
+ ) -> Query[_T]:
55
60
  """ Takes an allocation query and limits it to the allocations
56
61
  overlapping with start and end.
57
62
 
@@ -71,9 +76,9 @@ class Queries(ContextServicesMixin):
71
76
 
72
77
  @staticmethod
73
78
  def overlapping_allocations(
74
- query: 'Query[_T]',
75
- dates: _t.Iterable[_t.Tuple[datetime, datetime]]
76
- ) -> 'Query[_T]':
79
+ query: Query[_T],
80
+ dates: Iterable[tuple[datetime, datetime]]
81
+ ) -> Query[_T]:
77
82
  """ Takes an allocation query and limits it to the allocations
78
83
  overlapping with any of the passed in datetime ranges
79
84
 
@@ -94,7 +99,7 @@ class Queries(ContextServicesMixin):
94
99
 
95
100
  @staticmethod
96
101
  def availability_by_allocations(
97
- allocations: _t.Iterable['Allocation']
102
+ allocations: Iterable[Allocation]
98
103
  ) -> float:
99
104
  """Takes any iterable with alloctions and calculates the availability.
100
105
  Counts missing mirrors as 100% free and returns a value between 0-100
@@ -124,7 +129,7 @@ class Queries(ContextServicesMixin):
124
129
  self,
125
130
  start: datetime,
126
131
  end: datetime,
127
- resources: _t.Collection['UUID']
132
+ resources: Collection[UUID]
128
133
  ) -> float:
129
134
  """Returns the availability for the given resources in the given range.
130
135
  The exposure is used to check if the allocation is visible.
@@ -143,8 +148,8 @@ class Queries(ContextServicesMixin):
143
148
  self,
144
149
  start: datetime,
145
150
  end: datetime,
146
- resources: _t.Collection['UUID']
147
- ) -> _t.Dict[date, _t.Tuple[float, _t.Set['UUID']]]:
151
+ resources: Collection[UUID]
152
+ ) -> dict[date, tuple[float, set[UUID]]]:
148
153
  """Availability by range with a twist. Instead of returning a grand
149
154
  total, a dictionary is returned with each day in the range as key and
150
155
  a tuple of availability and the resources counted for that day.
@@ -181,8 +186,8 @@ class Queries(ContextServicesMixin):
181
186
 
182
187
  def reservations_by_session(
183
188
  self,
184
- session_id: _t.Optional['UUID']
185
- ) -> 'Query[Reservation]':
189
+ session_id: UUID | None
190
+ ) -> Query[Reservation]:
186
191
 
187
192
  # be sure to not query for all reservations. since a query should be
188
193
  # returned in any case we just use an impossible clause
@@ -190,7 +195,7 @@ class Queries(ContextServicesMixin):
190
195
  # this is mainly a security feature
191
196
  if not session_id:
192
197
  log.warn('empty session id')
193
- return self.session.query(Reservation).filter("0=1")
198
+ return self.session.query(Reservation).filter('0=1')
194
199
 
195
200
  query = self.session.query(Reservation)
196
201
  query = query.filter(Reservation.session_id == session_id)
@@ -200,8 +205,8 @@ class Queries(ContextServicesMixin):
200
205
 
201
206
  def confirm_reservations_for_session(
202
207
  self,
203
- session_id: 'UUID',
204
- token: _t.Optional['UUID'] = None
208
+ session_id: UUID,
209
+ token: UUID | None = None
205
210
  ) -> None:
206
211
  """ Confirms all reservations of the given session id. Optionally
207
212
  confirms only the reservations with the given token. All if None.
@@ -229,8 +234,8 @@ class Queries(ContextServicesMixin):
229
234
 
230
235
  def remove_reservation_from_session(
231
236
  self,
232
- session_id: 'UUID',
233
- token: 'UUID'
237
+ session_id: UUID,
238
+ token: UUID
234
239
  ) -> None:
235
240
  """ Removes the reservation with the given session_id and token. """
236
241
 
@@ -262,12 +267,12 @@ class Queries(ContextServicesMixin):
262
267
  query = self.session.query(Reservation)
263
268
  query = query.filter(Reservation.session_id == session_id)
264
269
 
265
- query.update({"modified": sedate.utcnow()})
270
+ query.update({'modified': sedate.utcnow()})
266
271
 
267
272
  def find_expired_reservation_sessions(
268
273
  self,
269
- expiration_date: _t.Optional[datetime]
270
- ) -> _t.List['UUID']:
274
+ expiration_date: datetime | None
275
+ ) -> list[UUID]:
271
276
  """ Goes through all reservations and returns the session ids of the
272
277
  unconfirmed ones which are older than the given expiration date.
273
278
  By default the expiration date is now - 15 minutes.
@@ -282,7 +287,7 @@ class Queries(ContextServicesMixin):
282
287
  )
283
288
 
284
289
  # first get the session ids which are expired
285
- query: 'Query[_t.Tuple[UUID, datetime, _t.Optional[datetime]]]'
290
+ query: Query[tuple[UUID, datetime, datetime | None]]
286
291
  query = self.session.query(
287
292
  Reservation.session_id,
288
293
  func.max(Reservation.created),
@@ -306,8 +311,8 @@ class Queries(ContextServicesMixin):
306
311
 
307
312
  def remove_expired_reservation_sessions(
308
313
  self,
309
- expiration_date: _t.Optional[datetime] = None
310
- ) -> _t.List['UUID']:
314
+ expiration_date: datetime | None = None
315
+ ) -> list[UUID]:
311
316
  """ Removes all reservations which have an expired session id.
312
317
  By default the expiration date is now - 15 minutes.
313
318