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.
- libres/__init__.py +3 -1
- libres/context/core.py +37 -31
- libres/context/exposure.py +7 -3
- libres/context/registry.py +15 -11
- libres/context/session.py +8 -6
- libres/context/settings.py +9 -6
- libres/db/__init__.py +3 -1
- libres/db/models/__init__.py +2 -0
- libres/db/models/allocation.py +72 -59
- libres/db/models/base.py +2 -0
- libres/db/models/other.py +12 -7
- libres/db/models/reservation.py +30 -25
- libres/db/models/reserved_slot.py +12 -10
- libres/db/models/timestamp.py +9 -7
- libres/db/models/types/__init__.py +2 -0
- libres/db/models/types/json_type.py +26 -28
- libres/db/models/types/utcdatetime.py +10 -8
- libres/db/models/types/uuid_type.py +10 -8
- libres/db/queries.py +32 -27
- libres/db/scheduler.py +141 -131
- libres/modules/__init__.py +0 -1
- libres/modules/errors.py +9 -7
- libres/modules/events.py +57 -56
- libres/modules/rasterizer.py +11 -7
- libres/modules/utils.py +16 -14
- {libres-0.7.3.dist-info → libres-0.9.0.dist-info}/METADATA +49 -16
- libres-0.9.0.dist-info/RECORD +33 -0
- {libres-0.7.3.dist-info → libres-0.9.0.dist-info}/WHEEL +1 -1
- libres-0.7.3.dist-info/RECORD +0 -33
- {libres-0.7.3.dist-info → libres-0.9.0.dist-info/licenses}/LICENSE +0 -0
- {libres-0.7.3.dist-info → libres-0.9.0.dist-info}/top_level.txt +0 -0
libres/db/models/timestamp.py
CHANGED
|
@@ -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
|
-
|
|
10
|
-
if
|
|
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() ->
|
|
27
|
+
def timestamp() -> datetime:
|
|
26
28
|
return sedate.utcnow()
|
|
27
29
|
|
|
28
|
-
if
|
|
30
|
+
if TYPE_CHECKING:
|
|
29
31
|
created: Column[datetime]
|
|
30
|
-
modified: Column[
|
|
32
|
+
modified: Column[datetime | None]
|
|
31
33
|
|
|
32
34
|
else:
|
|
33
35
|
@declared_attr
|
|
34
|
-
def created(cls) ->
|
|
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) ->
|
|
45
|
+
def modified(cls) -> Column[datetime | None]:
|
|
44
46
|
return deferred(
|
|
45
47
|
Column(
|
|
46
48
|
UTCDateTime(timezone=False),
|
|
@@ -1,48 +1,46 @@
|
|
|
1
|
-
from
|
|
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
|
-
|
|
6
|
-
|
|
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[
|
|
13
|
+
_Base = TypeDecorator[dict[str, Any]]
|
|
10
14
|
else:
|
|
11
15
|
_Base = TypeDecorator
|
|
12
16
|
|
|
13
17
|
|
|
14
18
|
class JSON(_Base):
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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:
|
|
31
|
-
dialect:
|
|
32
|
-
) ->
|
|
31
|
+
value: dict[str, Any] | None,
|
|
32
|
+
dialect: Dialect
|
|
33
|
+
) -> dict[str, Any]:
|
|
33
34
|
|
|
34
|
-
if value is
|
|
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:
|
|
42
|
-
dialect:
|
|
43
|
-
) ->
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
if
|
|
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:
|
|
33
|
-
dialect:
|
|
34
|
-
) ->
|
|
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:
|
|
43
|
-
dialect:
|
|
44
|
-
) ->
|
|
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
|
-
|
|
8
|
-
if
|
|
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:
|
|
50
|
-
dialect:
|
|
51
|
-
) ->
|
|
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:
|
|
60
|
-
dialect:
|
|
61
|
-
) ->
|
|
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
|
-
|
|
15
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
) ->
|
|
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:
|
|
56
|
+
query: Query[_T],
|
|
52
57
|
start: datetime,
|
|
53
58
|
end: datetime
|
|
54
|
-
) ->
|
|
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:
|
|
75
|
-
dates:
|
|
76
|
-
) ->
|
|
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:
|
|
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:
|
|
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:
|
|
147
|
-
) ->
|
|
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:
|
|
185
|
-
) ->
|
|
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(
|
|
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:
|
|
204
|
-
token:
|
|
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:
|
|
233
|
-
token:
|
|
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({
|
|
270
|
+
query.update({'modified': sedate.utcnow()})
|
|
266
271
|
|
|
267
272
|
def find_expired_reservation_sessions(
|
|
268
273
|
self,
|
|
269
|
-
expiration_date:
|
|
270
|
-
) ->
|
|
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:
|
|
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:
|
|
310
|
-
) ->
|
|
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
|
|