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/allocation.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import sedate
|
|
2
4
|
|
|
3
5
|
from datetime import datetime, timedelta, time
|
|
@@ -24,18 +26,25 @@ from libres.modules.rasterizer import (
|
|
|
24
26
|
)
|
|
25
27
|
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
from typing import Any
|
|
30
|
+
from typing import TypeVar
|
|
31
|
+
from typing import TYPE_CHECKING
|
|
32
|
+
if TYPE_CHECKING:
|
|
29
33
|
import uuid
|
|
34
|
+
from collections.abc import Iterator
|
|
30
35
|
from sedate.types import TzInfoOrName
|
|
31
36
|
from sqlalchemy.orm import Query
|
|
37
|
+
from typing import NamedTuple
|
|
32
38
|
from typing_extensions import Self
|
|
33
39
|
|
|
34
|
-
from libres.db.models import
|
|
40
|
+
from libres.db.models import ReservedSlot
|
|
35
41
|
from libres.modules.rasterizer import Raster
|
|
36
42
|
|
|
37
|
-
_OptDT1 =
|
|
38
|
-
_OptDT2 =
|
|
43
|
+
_OptDT1 = TypeVar('_OptDT1', 'datetime | None', datetime, None)
|
|
44
|
+
_OptDT2 = TypeVar('_OptDT2', 'datetime | None', datetime, None)
|
|
45
|
+
|
|
46
|
+
class _ReservationIdRow(NamedTuple):
|
|
47
|
+
id: int
|
|
39
48
|
|
|
40
49
|
|
|
41
50
|
class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
@@ -63,7 +72,7 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
63
72
|
__tablename__ = 'allocations'
|
|
64
73
|
|
|
65
74
|
#: the id of the allocation, autoincremented
|
|
66
|
-
id:
|
|
75
|
+
id: Column[int] = Column(
|
|
67
76
|
types.Integer(),
|
|
68
77
|
primary_key=True,
|
|
69
78
|
autoincrement=True
|
|
@@ -71,27 +80,27 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
71
80
|
|
|
72
81
|
#: the resource uuid of the allocation, may not be an actual resource
|
|
73
82
|
#: see :class:`.models.Allocation` for more information
|
|
74
|
-
resource:
|
|
83
|
+
resource: Column[uuid.UUID] = Column(UUID(), nullable=False)
|
|
75
84
|
|
|
76
85
|
#: the polymorphic type of the allocation
|
|
77
|
-
type:
|
|
86
|
+
type: Column[str | None] = Column(types.Text(), nullable=True)
|
|
78
87
|
|
|
79
88
|
#: resource of which this allocation is a mirror. If the mirror_of
|
|
80
89
|
#: attribute equals the resource, this is a real resource
|
|
81
90
|
#: see :class:`.models.Allocation` for more information
|
|
82
|
-
mirror_of:
|
|
91
|
+
mirror_of: Column[uuid.UUID] = Column(UUID(), nullable=False)
|
|
83
92
|
|
|
84
93
|
#: Group uuid to which this allocation belongs to. Every allocation has a
|
|
85
94
|
#: group but some allocations may be the only one in their group.
|
|
86
|
-
group:
|
|
95
|
+
group: Column[uuid.UUID] = Column(UUID(), nullable=False)
|
|
87
96
|
|
|
88
97
|
#: Number of times this allocation may be reserved
|
|
89
98
|
# FIXME: Why is this not nullable=False? For now we pretend that it is
|
|
90
|
-
quota:
|
|
99
|
+
quota: Column[int] = Column(types.Integer(), default=1)
|
|
91
100
|
|
|
92
101
|
#: Maximum number of times this allocation may be reserved with one
|
|
93
102
|
#: single reservation.
|
|
94
|
-
quota_limit:
|
|
103
|
+
quota_limit: Column[int] = Column(
|
|
95
104
|
types.Integer(),
|
|
96
105
|
default=0,
|
|
97
106
|
nullable=False
|
|
@@ -99,37 +108,37 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
99
108
|
|
|
100
109
|
#: Partly available allocations may be reserved partially. How They may
|
|
101
110
|
#: be partitioned is defined by the allocation's raster.
|
|
102
|
-
partly_available:
|
|
111
|
+
partly_available: Column[bool] = Column(types.Boolean(), default=False)
|
|
103
112
|
|
|
104
113
|
#: True if reservations for this allocation must be approved manually.
|
|
105
|
-
approve_manually:
|
|
114
|
+
approve_manually: Column[bool] = Column(types.Boolean(), default=False)
|
|
106
115
|
|
|
107
116
|
#: The timezone this allocation resides in.
|
|
108
117
|
# FIXME: Why is this not nullable=False? A lot of properties rely on this!
|
|
109
|
-
timezone:
|
|
118
|
+
timezone: Column[str | None] = Column(types.String())
|
|
110
119
|
|
|
111
120
|
#: Custom data reserved for the user
|
|
112
|
-
data:
|
|
121
|
+
data: Column[dict[str, Any] | None] = Column(
|
|
113
122
|
JSON(),
|
|
114
123
|
nullable=True
|
|
115
124
|
)
|
|
116
125
|
|
|
117
|
-
_start:
|
|
126
|
+
_start: Column[datetime] = Column(
|
|
118
127
|
UTCDateTime(timezone=False),
|
|
119
128
|
nullable=False
|
|
120
129
|
)
|
|
121
|
-
_end:
|
|
130
|
+
_end: Column[datetime] = Column(
|
|
122
131
|
UTCDateTime(timezone=False),
|
|
123
132
|
nullable=False
|
|
124
133
|
)
|
|
125
|
-
_raster:
|
|
134
|
+
_raster: Column[Raster] = Column(
|
|
126
135
|
types.Integer(), # type:ignore[arg-type]
|
|
127
136
|
nullable=False
|
|
128
137
|
)
|
|
129
138
|
|
|
130
|
-
if
|
|
139
|
+
if TYPE_CHECKING:
|
|
131
140
|
# forward declare backref
|
|
132
|
-
reserved_slots:
|
|
141
|
+
reserved_slots: list[ReservedSlot]
|
|
133
142
|
|
|
134
143
|
__table_args__ = (
|
|
135
144
|
Index('mirror_resource_ix', 'mirror_of', 'resource'),
|
|
@@ -149,7 +158,7 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
149
158
|
def __hash__(self) -> int:
|
|
150
159
|
return id(self)
|
|
151
160
|
|
|
152
|
-
def copy(self) ->
|
|
161
|
+
def copy(self) -> Self:
|
|
153
162
|
""" Creates a new copy of this allocation. """
|
|
154
163
|
allocation = self.__class__()
|
|
155
164
|
allocation.resource = self.resource
|
|
@@ -180,7 +189,7 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
180
189
|
|
|
181
190
|
#: The start of this allocation. Must be timezone aware.
|
|
182
191
|
#: This date is rastered by the allocation's raster.
|
|
183
|
-
if
|
|
192
|
+
if TYPE_CHECKING:
|
|
184
193
|
# NOTE: type checkers perform some special sauce for property
|
|
185
194
|
# so the non-decorator style isn't well supported
|
|
186
195
|
@property
|
|
@@ -209,7 +218,7 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
209
218
|
#: to avoid overlaps with other allocations.
|
|
210
219
|
#: That is to say an allocation that ends at 15:00 really ends at
|
|
211
220
|
#: 14:59:59.999999
|
|
212
|
-
if
|
|
221
|
+
if TYPE_CHECKING:
|
|
213
222
|
# NOTE: type checkers perform some special sauce for property
|
|
214
223
|
# so the non-decorator style isn't well supported
|
|
215
224
|
@property
|
|
@@ -219,10 +228,10 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
219
228
|
else:
|
|
220
229
|
end = property(get_end, set_end)
|
|
221
230
|
|
|
222
|
-
def get_raster(self) ->
|
|
231
|
+
def get_raster(self) -> Raster:
|
|
223
232
|
return self._raster
|
|
224
233
|
|
|
225
|
-
def set_raster(self, raster:
|
|
234
|
+
def set_raster(self, raster: Raster) -> None:
|
|
226
235
|
# the raster can only be set once!
|
|
227
236
|
assert not self._raster
|
|
228
237
|
self._raster = raster # type:ignore[unreachable]
|
|
@@ -239,7 +248,7 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
239
248
|
if self._end:
|
|
240
249
|
self._end = rasterize_end(self._end, self.raster)
|
|
241
250
|
|
|
242
|
-
if
|
|
251
|
+
if TYPE_CHECKING:
|
|
243
252
|
# NOTE: type checkers perform some special sauce for property
|
|
244
253
|
# so the non-decorator style isn't well supported
|
|
245
254
|
@property
|
|
@@ -251,7 +260,7 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
251
260
|
|
|
252
261
|
def display_start(
|
|
253
262
|
self,
|
|
254
|
-
timezone:
|
|
263
|
+
timezone: TzInfoOrName | None = None
|
|
255
264
|
) -> datetime:
|
|
256
265
|
"""Returns the start in either the timezone given or the timezone
|
|
257
266
|
on the allocation."""
|
|
@@ -263,7 +272,7 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
263
272
|
|
|
264
273
|
def display_end(
|
|
265
274
|
self,
|
|
266
|
-
timezone:
|
|
275
|
+
timezone: TzInfoOrName | None = None
|
|
267
276
|
) -> datetime:
|
|
268
277
|
"""Returns the end plus one microsecond in either the timezone given
|
|
269
278
|
or the timezone on the allocation.
|
|
@@ -278,9 +287,9 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
278
287
|
|
|
279
288
|
def _prepare_range(
|
|
280
289
|
self,
|
|
281
|
-
start:
|
|
282
|
-
end:
|
|
283
|
-
) ->
|
|
290
|
+
start: _OptDT1,
|
|
291
|
+
end: _OptDT2
|
|
292
|
+
) -> tuple[_OptDT1, _OptDT2]:
|
|
284
293
|
|
|
285
294
|
if start:
|
|
286
295
|
assert self.timezone is not None
|
|
@@ -328,9 +337,9 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
328
337
|
|
|
329
338
|
def free_slots(
|
|
330
339
|
self,
|
|
331
|
-
start:
|
|
332
|
-
end:
|
|
333
|
-
) ->
|
|
340
|
+
start: datetime | None = None,
|
|
341
|
+
end: datetime | None = None
|
|
342
|
+
) -> list[tuple[datetime, datetime]]:
|
|
334
343
|
""" Returns the slots which are not yet reserved. """
|
|
335
344
|
reserved = {slot.start for slot in self.reserved_slots}
|
|
336
345
|
|
|
@@ -342,9 +351,9 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
342
351
|
|
|
343
352
|
def align_dates(
|
|
344
353
|
self,
|
|
345
|
-
start:
|
|
346
|
-
end:
|
|
347
|
-
) ->
|
|
354
|
+
start: datetime | None = None,
|
|
355
|
+
end: datetime | None = None
|
|
356
|
+
) -> tuple[datetime, datetime]:
|
|
348
357
|
""" Aligns the given dates to the start and end date of the allocation.
|
|
349
358
|
|
|
350
359
|
"""
|
|
@@ -361,9 +370,9 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
361
370
|
|
|
362
371
|
def all_slots(
|
|
363
372
|
self,
|
|
364
|
-
start:
|
|
365
|
-
end:
|
|
366
|
-
) ->
|
|
373
|
+
start: datetime | None = None,
|
|
374
|
+
end: datetime | None = None
|
|
375
|
+
) -> Iterator[tuple[datetime, datetime]]:
|
|
367
376
|
""" Returns the slots which exist with this timespan. Reserved or free.
|
|
368
377
|
|
|
369
378
|
"""
|
|
@@ -376,8 +385,8 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
376
385
|
|
|
377
386
|
def count_slots(
|
|
378
387
|
self,
|
|
379
|
-
start:
|
|
380
|
-
end:
|
|
388
|
+
start: datetime | None = None,
|
|
389
|
+
end: datetime | None = None
|
|
381
390
|
) -> int:
|
|
382
391
|
""" Returns the number of slots which exist with this timespan.
|
|
383
392
|
Reserved or free.
|
|
@@ -393,8 +402,8 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
393
402
|
|
|
394
403
|
def is_available(
|
|
395
404
|
self,
|
|
396
|
-
start:
|
|
397
|
-
end:
|
|
405
|
+
start: datetime | None = None,
|
|
406
|
+
end: datetime | None = None
|
|
398
407
|
) -> bool:
|
|
399
408
|
""" Returns true if the given daterange is completely available. """
|
|
400
409
|
|
|
@@ -414,11 +423,11 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
414
423
|
self,
|
|
415
424
|
start: time,
|
|
416
425
|
end: time,
|
|
417
|
-
timezone:
|
|
426
|
+
timezone: TzInfoOrName | None = None,
|
|
418
427
|
is_dst: bool = False,
|
|
419
428
|
raise_non_existent: bool = False,
|
|
420
429
|
raise_ambiguous: bool = False
|
|
421
|
-
) ->
|
|
430
|
+
) -> tuple[datetime, datetime]:
|
|
422
431
|
""" Takes the given timespan and moves the start/end date to
|
|
423
432
|
the closest reservable slot. So if 10:00 - 11:00 is requested it will
|
|
424
433
|
|
|
@@ -465,7 +474,7 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
465
474
|
return self.display_start(timezone), self.display_end(timezone)
|
|
466
475
|
|
|
467
476
|
@property
|
|
468
|
-
def pending_reservations(self) ->
|
|
477
|
+
def pending_reservations(self) -> Query[_ReservationIdRow]:
|
|
469
478
|
""" Returns the pending reservations query for this allocation.
|
|
470
479
|
As the pending reservations target the group and not a specific
|
|
471
480
|
allocation this function returns the same value for masters and
|
|
@@ -476,7 +485,8 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
476
485
|
"Don't call if the allocation does not yet exist"
|
|
477
486
|
)
|
|
478
487
|
|
|
479
|
-
Reservation = self.models.Reservation
|
|
488
|
+
Reservation = self.models.Reservation # noqa: N806
|
|
489
|
+
query: Query[_ReservationIdRow]
|
|
480
490
|
query = object_session(self).query(Reservation.id)
|
|
481
491
|
query = query.filter(Reservation.target == self.group)
|
|
482
492
|
query = query.filter(Reservation.status == 'pending')
|
|
@@ -584,7 +594,7 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
584
594
|
self,
|
|
585
595
|
start: datetime,
|
|
586
596
|
end: datetime
|
|
587
|
-
) ->
|
|
597
|
+
) -> Self | None:
|
|
588
598
|
""" Returns the first free allocation spot amongst the master and the
|
|
589
599
|
mirrors. Honors the quota set on the master and will only try the
|
|
590
600
|
master if the quota is set to 1.
|
|
@@ -623,10 +633,9 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
623
633
|
|
|
624
634
|
return True
|
|
625
635
|
|
|
626
|
-
def normalized_slots(self) ->
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
]]:
|
|
636
|
+
def normalized_slots(self) -> Iterator[
|
|
637
|
+
tuple[datetime, datetime] | tuple[None, None]
|
|
638
|
+
]:
|
|
630
639
|
"""Most of the times this will return the same thing as all_slots
|
|
631
640
|
however for DST timezones it will ensure the transitions days with
|
|
632
641
|
23 and 25 hours respectively still return 24 hours worth of slots.
|
|
@@ -690,7 +699,7 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
690
699
|
def availability_partitions(
|
|
691
700
|
self,
|
|
692
701
|
normalize_dst: bool = True
|
|
693
|
-
) ->
|
|
702
|
+
) -> list[tuple[float, bool]]:
|
|
694
703
|
"""Partitions the space between start and end into blocks of either
|
|
695
704
|
free or reserved time. Each block has a percentage representing the
|
|
696
705
|
space the block occupies compared to the size of the whole allocation.
|
|
@@ -786,17 +795,19 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
786
795
|
|
|
787
796
|
return self.resource == self.mirror_of
|
|
788
797
|
|
|
789
|
-
def get_master(self) ->
|
|
798
|
+
def get_master(self) -> Self:
|
|
790
799
|
if self.is_master:
|
|
791
800
|
return self
|
|
792
801
|
else:
|
|
793
|
-
query
|
|
802
|
+
# FIXME: This should either query `self.__class__` or
|
|
803
|
+
# we need to return `Allocation` rather than `Self`
|
|
804
|
+
query: Query[Self] = object_session(self).query(Allocation)
|
|
794
805
|
query = query.filter(Allocation._start == self._start)
|
|
795
806
|
query = query.filter(Allocation.resource == self.mirror_of)
|
|
796
807
|
|
|
797
808
|
return query.one()
|
|
798
809
|
|
|
799
|
-
def siblings(self, imaginary: bool = True) ->
|
|
810
|
+
def siblings(self, imaginary: bool = True) -> list[Self]:
|
|
800
811
|
"""Returns the master/mirrors group this allocation is part of.
|
|
801
812
|
|
|
802
813
|
If 'imaginary' is true, inexistant mirrors are created on the fly.
|
|
@@ -814,7 +825,9 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
814
825
|
assert self.is_master
|
|
815
826
|
return [self]
|
|
816
827
|
|
|
817
|
-
query
|
|
828
|
+
# FIXME: This should either query `self.__class__` or
|
|
829
|
+
# we need to return `Allocation` rather than `Self`
|
|
830
|
+
query: Query[Self] = object_session(self).query(Allocation)
|
|
818
831
|
query = query.filter(Allocation.mirror_of == self.mirror_of)
|
|
819
832
|
query = query.filter(Allocation._start == self._start)
|
|
820
833
|
|
libres/db/models/base.py
CHANGED
libres/db/models/other.py
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from typing import Protocol
|
|
7
|
+
|
|
3
8
|
import libres.db.models as _models
|
|
4
9
|
|
|
5
|
-
class _Models(
|
|
6
|
-
Allocation:
|
|
7
|
-
ReservedSlot:
|
|
8
|
-
Reservation:
|
|
10
|
+
class _Models(Protocol):
|
|
11
|
+
Allocation: type[_models.Allocation]
|
|
12
|
+
ReservedSlot: type[_models.ReservedSlot]
|
|
13
|
+
Reservation: type[_models.Reservation]
|
|
9
14
|
|
|
10
15
|
|
|
11
16
|
models = None
|
|
@@ -16,7 +21,7 @@ class OtherModels:
|
|
|
16
21
|
classes without causing circular imports. """
|
|
17
22
|
|
|
18
23
|
@property
|
|
19
|
-
def models(self) ->
|
|
24
|
+
def models(self) -> _Models:
|
|
20
25
|
global models
|
|
21
26
|
if not models:
|
|
22
27
|
# FIXME: libres.db exports ORMBase, do we really
|
libres/db/models/reservation.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import sedate
|
|
2
4
|
|
|
3
5
|
from datetime import datetime, timedelta
|
|
@@ -13,8 +15,11 @@ from libres.db.models.other import OtherModels
|
|
|
13
15
|
from libres.db.models.timestamp import TimestampMixin
|
|
14
16
|
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
from typing import Any
|
|
19
|
+
from typing import Literal
|
|
20
|
+
from typing import NamedTuple
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
22
|
+
if TYPE_CHECKING:
|
|
18
23
|
import uuid
|
|
19
24
|
from sedate.types import TzInfoOrName
|
|
20
25
|
from sqlalchemy.orm import Query
|
|
@@ -22,7 +27,7 @@ if _t.TYPE_CHECKING:
|
|
|
22
27
|
from libres.db.models import Allocation
|
|
23
28
|
|
|
24
29
|
|
|
25
|
-
class Timespan(
|
|
30
|
+
class Timespan(NamedTuple):
|
|
26
31
|
start: datetime
|
|
27
32
|
end: datetime
|
|
28
33
|
|
|
@@ -34,23 +39,23 @@ class Reservation(TimestampMixin, ORMBase, OtherModels):
|
|
|
34
39
|
|
|
35
40
|
__tablename__ = 'reservations'
|
|
36
41
|
|
|
37
|
-
id:
|
|
42
|
+
id: Column[int] = Column(
|
|
38
43
|
types.Integer(),
|
|
39
44
|
primary_key=True,
|
|
40
45
|
autoincrement=True
|
|
41
46
|
)
|
|
42
47
|
|
|
43
|
-
token:
|
|
48
|
+
token: Column[uuid.UUID] = Column(
|
|
44
49
|
UUID(),
|
|
45
50
|
nullable=False,
|
|
46
51
|
)
|
|
47
52
|
|
|
48
|
-
target:
|
|
53
|
+
target: Column[uuid.UUID] = Column(
|
|
49
54
|
UUID(),
|
|
50
55
|
nullable=False,
|
|
51
56
|
)
|
|
52
57
|
|
|
53
|
-
target_type:
|
|
58
|
+
target_type: Column[Literal['group', 'allocation']] = Column(
|
|
54
59
|
types.Enum( # type:ignore[arg-type]
|
|
55
60
|
'group', 'allocation',
|
|
56
61
|
name='reservation_target_type'
|
|
@@ -58,56 +63,56 @@ class Reservation(TimestampMixin, ORMBase, OtherModels):
|
|
|
58
63
|
nullable=False
|
|
59
64
|
)
|
|
60
65
|
|
|
61
|
-
type:
|
|
66
|
+
type: Column[str | None] = Column(
|
|
62
67
|
types.Text(),
|
|
63
68
|
nullable=True
|
|
64
69
|
)
|
|
65
70
|
|
|
66
|
-
resource:
|
|
71
|
+
resource: Column[uuid.UUID] = Column(
|
|
67
72
|
UUID(),
|
|
68
73
|
nullable=False
|
|
69
74
|
)
|
|
70
75
|
|
|
71
|
-
start:
|
|
76
|
+
start: Column[datetime | None] = Column(
|
|
72
77
|
UTCDateTime(timezone=False),
|
|
73
78
|
nullable=True
|
|
74
79
|
)
|
|
75
80
|
|
|
76
|
-
end:
|
|
81
|
+
end: Column[datetime | None] = Column(
|
|
77
82
|
UTCDateTime(timezone=False),
|
|
78
83
|
nullable=True
|
|
79
84
|
)
|
|
80
85
|
|
|
81
|
-
timezone:
|
|
86
|
+
timezone: Column[str | None] = Column(
|
|
82
87
|
types.String(),
|
|
83
88
|
nullable=True
|
|
84
89
|
)
|
|
85
90
|
|
|
86
|
-
status:
|
|
91
|
+
status: Column[Literal['pending', 'approved']] = Column(
|
|
87
92
|
types.Enum( # type:ignore[arg-type]
|
|
88
93
|
'pending', 'approved',
|
|
89
|
-
name=
|
|
94
|
+
name='reservation_status'
|
|
90
95
|
),
|
|
91
96
|
nullable=False
|
|
92
97
|
)
|
|
93
98
|
|
|
94
|
-
data:
|
|
99
|
+
data: Column[dict[str, Any] | None] = deferred(
|
|
95
100
|
Column(
|
|
96
101
|
JSON(),
|
|
97
102
|
nullable=True
|
|
98
103
|
)
|
|
99
104
|
)
|
|
100
105
|
|
|
101
|
-
email:
|
|
106
|
+
email: Column[str] = Column(
|
|
102
107
|
types.Unicode(254),
|
|
103
108
|
nullable=False
|
|
104
109
|
)
|
|
105
110
|
|
|
106
|
-
session_id:
|
|
111
|
+
session_id: Column[uuid.UUID | None] = Column(
|
|
107
112
|
UUID()
|
|
108
113
|
)
|
|
109
114
|
|
|
110
|
-
quota:
|
|
115
|
+
quota: Column[int] = Column(
|
|
111
116
|
types.Integer(),
|
|
112
117
|
nullable=False
|
|
113
118
|
)
|
|
@@ -121,7 +126,7 @@ class Reservation(TimestampMixin, ORMBase, OtherModels):
|
|
|
121
126
|
'polymorphic_on': type
|
|
122
127
|
}
|
|
123
128
|
|
|
124
|
-
def _target_allocations(self) ->
|
|
129
|
+
def _target_allocations(self) -> Query[Allocation]:
|
|
125
130
|
""" Returns the allocations this reservation is targeting. This should
|
|
126
131
|
NOT be confused with db.allocations_by_reservation. The method in
|
|
127
132
|
the db module returns the actual allocations belonging to an approved
|
|
@@ -133,7 +138,7 @@ class Reservation(TimestampMixin, ORMBase, OtherModels):
|
|
|
133
138
|
be dangerous.
|
|
134
139
|
|
|
135
140
|
"""
|
|
136
|
-
Allocation = self.models.Allocation
|
|
141
|
+
Allocation = self.models.Allocation # noqa: N806
|
|
137
142
|
query = object_session(self).query(Allocation)
|
|
138
143
|
query = query.filter(Allocation.group == self.target)
|
|
139
144
|
|
|
@@ -143,11 +148,11 @@ class Reservation(TimestampMixin, ORMBase, OtherModels):
|
|
|
143
148
|
# order by date
|
|
144
149
|
query = query.order_by(Allocation._start)
|
|
145
150
|
|
|
146
|
-
return query
|
|
151
|
+
return query # type: ignore[no-any-return]
|
|
147
152
|
|
|
148
153
|
def display_start(
|
|
149
154
|
self,
|
|
150
|
-
timezone:
|
|
155
|
+
timezone: TzInfoOrName | None = None
|
|
151
156
|
) -> datetime:
|
|
152
157
|
"""Does nothing but to form a nice pair to display_end."""
|
|
153
158
|
assert self.start is not None
|
|
@@ -158,7 +163,7 @@ class Reservation(TimestampMixin, ORMBase, OtherModels):
|
|
|
158
163
|
|
|
159
164
|
def display_end(
|
|
160
165
|
self,
|
|
161
|
-
timezone:
|
|
166
|
+
timezone: TzInfoOrName | None = None
|
|
162
167
|
) -> datetime:
|
|
163
168
|
"""Returns the end plus one microsecond (nicer display)."""
|
|
164
169
|
assert self.end is not None
|
|
@@ -169,7 +174,7 @@ class Reservation(TimestampMixin, ORMBase, OtherModels):
|
|
|
169
174
|
end = self.end + timedelta(microseconds=1)
|
|
170
175
|
return sedate.to_timezone(end, timezone)
|
|
171
176
|
|
|
172
|
-
def timespans(self) ->
|
|
177
|
+
def timespans(self) -> list[Timespan]:
|
|
173
178
|
""" Returns the timespans targeted by this reservation.
|
|
174
179
|
|
|
175
180
|
The result is a list of :class:`~libres.db.models.reservation.Timespan`
|
|
@@ -207,4 +212,4 @@ class Reservation(TimestampMixin, ORMBase, OtherModels):
|
|
|
207
212
|
# A reservation is deemed autoapprovable if no allocation
|
|
208
213
|
# requires explicit approval
|
|
209
214
|
|
|
210
|
-
return object_session(self).query(~query.exists()).scalar()
|
|
215
|
+
return object_session(self).query(~query.exists()).scalar() # type: ignore[no-any-return]
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import sedate
|
|
2
4
|
|
|
3
5
|
from datetime import datetime, timedelta
|
|
@@ -18,8 +20,8 @@ from libres.db.models.types import UUID, UTCDateTime
|
|
|
18
20
|
from libres.db.models.timestamp import TimestampMixin
|
|
19
21
|
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
if
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
24
|
+
if TYPE_CHECKING:
|
|
23
25
|
import uuid
|
|
24
26
|
from sedate.types import TzInfoOrName
|
|
25
27
|
|
|
@@ -29,32 +31,32 @@ class ReservedSlot(TimestampMixin, ORMBase):
|
|
|
29
31
|
|
|
30
32
|
__tablename__ = 'reserved_slots'
|
|
31
33
|
|
|
32
|
-
resource:
|
|
34
|
+
resource: Column[uuid.UUID] = Column(
|
|
33
35
|
UUID(),
|
|
34
36
|
primary_key=True,
|
|
35
37
|
nullable=False,
|
|
36
38
|
autoincrement=False
|
|
37
39
|
)
|
|
38
40
|
|
|
39
|
-
start:
|
|
41
|
+
start: Column[datetime] = Column(
|
|
40
42
|
UTCDateTime(timezone=False),
|
|
41
43
|
primary_key=True,
|
|
42
44
|
nullable=False,
|
|
43
45
|
autoincrement=False
|
|
44
46
|
)
|
|
45
47
|
|
|
46
|
-
end:
|
|
48
|
+
end: Column[datetime] = Column(
|
|
47
49
|
UTCDateTime(timezone=False),
|
|
48
50
|
nullable=False
|
|
49
51
|
)
|
|
50
52
|
|
|
51
|
-
allocation_id:
|
|
53
|
+
allocation_id: Column[int] = Column(
|
|
52
54
|
types.Integer(),
|
|
53
55
|
ForeignKey(Allocation.id),
|
|
54
56
|
nullable=False
|
|
55
57
|
)
|
|
56
58
|
|
|
57
|
-
allocation:
|
|
59
|
+
allocation: relationship[Allocation] = relationship(
|
|
58
60
|
Allocation,
|
|
59
61
|
primaryjoin=Allocation.id == allocation_id,
|
|
60
62
|
|
|
@@ -68,7 +70,7 @@ class ReservedSlot(TimestampMixin, ORMBase):
|
|
|
68
70
|
)
|
|
69
71
|
)
|
|
70
72
|
|
|
71
|
-
reservation_token:
|
|
73
|
+
reservation_token: Column[uuid.UUID] = Column(
|
|
72
74
|
UUID(),
|
|
73
75
|
nullable=False
|
|
74
76
|
)
|
|
@@ -79,7 +81,7 @@ class ReservedSlot(TimestampMixin, ORMBase):
|
|
|
79
81
|
|
|
80
82
|
def display_start(
|
|
81
83
|
self,
|
|
82
|
-
timezone:
|
|
84
|
+
timezone: TzInfoOrName | None = None
|
|
83
85
|
) -> datetime:
|
|
84
86
|
|
|
85
87
|
if timezone is None:
|
|
@@ -91,7 +93,7 @@ class ReservedSlot(TimestampMixin, ORMBase):
|
|
|
91
93
|
|
|
92
94
|
def display_end(
|
|
93
95
|
self,
|
|
94
|
-
timezone:
|
|
96
|
+
timezone: TzInfoOrName | None = None
|
|
95
97
|
) -> datetime:
|
|
96
98
|
|
|
97
99
|
if timezone is None:
|