libres 0.7.3__py3-none-any.whl → 0.8.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 +32 -26
- libres/context/exposure.py +7 -3
- libres/context/registry.py +14 -10
- 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 +60 -56
- libres/db/models/base.py +2 -0
- libres/db/models/other.py +12 -7
- libres/db/models/reservation.py +28 -23
- 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 +12 -9
- 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 +133 -127
- 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.8.0.dist-info}/METADATA +31 -14
- libres-0.8.0.dist-info/RECORD +33 -0
- {libres-0.7.3.dist-info → libres-0.8.0.dist-info}/WHEEL +1 -1
- libres-0.7.3.dist-info/RECORD +0 -33
- {libres-0.7.3.dist-info → libres-0.8.0.dist-info}/LICENSE +0 -0
- {libres-0.7.3.dist-info → libres-0.8.0.dist-info}/top_level.txt +0 -0
libres/db/scheduler.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import sedate
|
|
2
4
|
|
|
3
5
|
from datetime import datetime, time, timedelta
|
|
@@ -16,8 +18,14 @@ from libres.modules import rasterizer
|
|
|
16
18
|
from libres.modules import utils
|
|
17
19
|
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
from typing import overload
|
|
22
|
+
from typing import Any
|
|
23
|
+
from typing import Literal
|
|
24
|
+
from typing import TYPE_CHECKING
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from collections.abc import Collection
|
|
27
|
+
from collections.abc import Iterable
|
|
28
|
+
from collections.abc import Iterator
|
|
21
29
|
from sedate.types import DateLike
|
|
22
30
|
from sqlalchemy.orm import Query
|
|
23
31
|
from typing_extensions import NotRequired, Self, TypeAlias, TypedDict
|
|
@@ -25,32 +33,32 @@ if _t.TYPE_CHECKING:
|
|
|
25
33
|
from libres.context.core import Context
|
|
26
34
|
from libres.modules.rasterizer import Raster
|
|
27
35
|
|
|
28
|
-
_dtrange: TypeAlias =
|
|
36
|
+
_dtrange: TypeAlias = tuple[datetime, datetime] # noqa: PYI042
|
|
29
37
|
|
|
30
38
|
class _ReserveArgs1(TypedDict):
|
|
31
39
|
email: str
|
|
32
|
-
dates:
|
|
33
|
-
data: NotRequired[
|
|
34
|
-
session_id: NotRequired[
|
|
40
|
+
dates: _dtrange | Collection[_dtrange]
|
|
41
|
+
data: NotRequired[Any | None]
|
|
42
|
+
session_id: NotRequired[UUID | None]
|
|
35
43
|
quota: NotRequired[int]
|
|
36
44
|
single_token_per_session: NotRequired[bool]
|
|
37
45
|
|
|
38
46
|
class _ReserveArgs2(TypedDict):
|
|
39
47
|
email: str
|
|
40
48
|
group: UUID
|
|
41
|
-
data: NotRequired[
|
|
42
|
-
session_id: NotRequired[
|
|
49
|
+
data: NotRequired[Any | None]
|
|
50
|
+
session_id: NotRequired[UUID | None]
|
|
43
51
|
quota: NotRequired[int]
|
|
44
52
|
single_token_per_session: NotRequired[bool]
|
|
45
53
|
|
|
46
|
-
_ReserveArgs: TypeAlias =
|
|
54
|
+
_ReserveArgs: TypeAlias = '_ReserveArgs1 | _ReserveArgs2'
|
|
47
55
|
|
|
48
56
|
|
|
49
57
|
missing = object()
|
|
50
58
|
|
|
51
|
-
Day =
|
|
52
|
-
DayNumber =
|
|
53
|
-
DAYS_MAP:
|
|
59
|
+
Day: TypeAlias = Literal['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su']
|
|
60
|
+
DayNumber: TypeAlias = Literal[0, 1, 2, 3, 4, 5, 6]
|
|
61
|
+
DAYS_MAP: dict[Day, DayNumber] = {
|
|
54
62
|
'mo': 0,
|
|
55
63
|
'tu': 1,
|
|
56
64
|
'we': 2,
|
|
@@ -68,12 +76,12 @@ class Scheduler(ContextServicesMixin):
|
|
|
68
76
|
|
|
69
77
|
def __init__(
|
|
70
78
|
self,
|
|
71
|
-
context:
|
|
79
|
+
context: Context,
|
|
72
80
|
name: str,
|
|
73
81
|
# FIXME: Not quite sure why we don't allow a PyTzInfo
|
|
74
82
|
timezone: str,
|
|
75
|
-
allocation_cls:
|
|
76
|
-
reservation_cls:
|
|
83
|
+
allocation_cls: type[Allocation] = Allocation,
|
|
84
|
+
reservation_cls: type[Reservation] = Reservation
|
|
77
85
|
):
|
|
78
86
|
""" Initializeds a new Scheduler instance.
|
|
79
87
|
|
|
@@ -112,7 +120,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
112
120
|
self.allocation_cls = allocation_cls
|
|
113
121
|
self.reservation_cls = reservation_cls
|
|
114
122
|
|
|
115
|
-
def clone(self) ->
|
|
123
|
+
def clone(self) -> Self:
|
|
116
124
|
""" Clones the scheduler. The result will be a new scheduler using the
|
|
117
125
|
same context, name, settings and attributes.
|
|
118
126
|
|
|
@@ -145,8 +153,8 @@ class Scheduler(ContextServicesMixin):
|
|
|
145
153
|
|
|
146
154
|
def _prepare_dates(
|
|
147
155
|
self,
|
|
148
|
-
dates:
|
|
149
|
-
) ->
|
|
156
|
+
dates: utils._NestedIterable[datetime]
|
|
157
|
+
) -> list[tuple[datetime, datetime]]:
|
|
150
158
|
return [
|
|
151
159
|
(
|
|
152
160
|
sedate.standardize_date(s, self.timezone),
|
|
@@ -158,20 +166,20 @@ class Scheduler(ContextServicesMixin):
|
|
|
158
166
|
self,
|
|
159
167
|
start: datetime,
|
|
160
168
|
end: datetime
|
|
161
|
-
) ->
|
|
169
|
+
) -> tuple[datetime, datetime]:
|
|
162
170
|
return (
|
|
163
171
|
sedate.standardize_date(start, self.timezone),
|
|
164
172
|
sedate.standardize_date(end, self.timezone)
|
|
165
173
|
)
|
|
166
174
|
|
|
167
|
-
def managed_allocations(self) ->
|
|
175
|
+
def managed_allocations(self) -> Query[Allocation]:
|
|
168
176
|
""" The allocations managed by this scheduler / resource. """
|
|
169
177
|
query = self.session.query(Allocation)
|
|
170
178
|
query = query.filter(Allocation.mirror_of == self.resource)
|
|
171
179
|
|
|
172
180
|
return query
|
|
173
181
|
|
|
174
|
-
def managed_reserved_slots(self) ->
|
|
182
|
+
def managed_reserved_slots(self) -> Query[ReservedSlot]:
|
|
175
183
|
""" The reserved_slots managed by this scheduler / resource. """
|
|
176
184
|
uuids = self.managed_allocations().with_entities(Allocation.resource)
|
|
177
185
|
|
|
@@ -180,7 +188,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
180
188
|
|
|
181
189
|
return query
|
|
182
190
|
|
|
183
|
-
def managed_reservations(self) ->
|
|
191
|
+
def managed_reservations(self) -> Query[Reservation]:
|
|
184
192
|
""" The reservations managed by this scheduler / resource. """
|
|
185
193
|
query = self.session.query(Reservation)
|
|
186
194
|
query = query.filter(Reservation.resource == self.resource)
|
|
@@ -205,8 +213,8 @@ class Scheduler(ContextServicesMixin):
|
|
|
205
213
|
|
|
206
214
|
def allocations_by_ids(
|
|
207
215
|
self,
|
|
208
|
-
ids:
|
|
209
|
-
) ->
|
|
216
|
+
ids: Collection[int]
|
|
217
|
+
) -> Query[Allocation]:
|
|
210
218
|
query = self.managed_allocations()
|
|
211
219
|
query = query.filter(Allocation.id.in_(ids))
|
|
212
220
|
query = query.order_by(Allocation._start)
|
|
@@ -216,14 +224,14 @@ class Scheduler(ContextServicesMixin):
|
|
|
216
224
|
self,
|
|
217
225
|
group: UUID,
|
|
218
226
|
masters_only: bool = True
|
|
219
|
-
) ->
|
|
227
|
+
) -> Query[Allocation]:
|
|
220
228
|
return self.allocations_by_groups([group], masters_only=masters_only)
|
|
221
229
|
|
|
222
230
|
def allocations_by_groups(
|
|
223
231
|
self,
|
|
224
|
-
groups:
|
|
232
|
+
groups: Collection[UUID],
|
|
225
233
|
masters_only: bool = True
|
|
226
|
-
) ->
|
|
234
|
+
) -> Query[Allocation]:
|
|
227
235
|
|
|
228
236
|
query = self.managed_allocations()
|
|
229
237
|
query = query.filter(Allocation.group.in_(groups))
|
|
@@ -236,8 +244,8 @@ class Scheduler(ContextServicesMixin):
|
|
|
236
244
|
def allocations_by_reservation(
|
|
237
245
|
self,
|
|
238
246
|
token: UUID,
|
|
239
|
-
id:
|
|
240
|
-
) ->
|
|
247
|
+
id: int | None = None
|
|
248
|
+
) -> Query[Allocation]:
|
|
241
249
|
""" Returns the allocations for the reservation if it was *approved*,
|
|
242
250
|
pending reservations return nothing. If you need to get the allocation
|
|
243
251
|
a pending reservation might be targeting, use _target_allocations
|
|
@@ -276,7 +284,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
276
284
|
start: datetime,
|
|
277
285
|
end: datetime,
|
|
278
286
|
masters_only: bool = True
|
|
279
|
-
) ->
|
|
287
|
+
) -> Query[Allocation]:
|
|
280
288
|
|
|
281
289
|
start, end = self._prepare_range(start, end)
|
|
282
290
|
|
|
@@ -295,7 +303,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
295
303
|
def allocation_dates_by_group(
|
|
296
304
|
self,
|
|
297
305
|
group: UUID
|
|
298
|
-
) ->
|
|
306
|
+
) -> list[tuple[datetime, datetime]]:
|
|
299
307
|
|
|
300
308
|
query = self.allocations_by_group(group)
|
|
301
309
|
dates_query = query.with_entities(Allocation._start, Allocation._end)
|
|
@@ -304,15 +312,15 @@ class Scheduler(ContextServicesMixin):
|
|
|
304
312
|
def allocation_mirrors_by_master(
|
|
305
313
|
self,
|
|
306
314
|
master: Allocation
|
|
307
|
-
) ->
|
|
315
|
+
) -> list[Allocation]:
|
|
308
316
|
return [s for s in master.siblings() if not s.is_master]
|
|
309
317
|
|
|
310
318
|
def allocation_dates_by_ids(
|
|
311
319
|
self,
|
|
312
|
-
ids:
|
|
313
|
-
start_time:
|
|
314
|
-
end_time:
|
|
315
|
-
) ->
|
|
320
|
+
ids: Collection[int],
|
|
321
|
+
start_time: time | None = None,
|
|
322
|
+
end_time: time | None = None
|
|
323
|
+
) -> Iterator[tuple[datetime, datetime]]:
|
|
316
324
|
|
|
317
325
|
for allocation in self.allocations_by_ids(ids).all():
|
|
318
326
|
|
|
@@ -323,7 +331,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
323
331
|
|
|
324
332
|
yield s_dt, e_dt - timedelta(microseconds=1)
|
|
325
333
|
|
|
326
|
-
def manual_approval_required(self, ids:
|
|
334
|
+
def manual_approval_required(self, ids: Collection[int]) -> bool:
|
|
327
335
|
""" Returns True if any of the allocations require manual approval. """
|
|
328
336
|
query = self.allocations_by_ids(ids)
|
|
329
337
|
query = query.filter(Allocation.approve_manually == True)
|
|
@@ -332,17 +340,17 @@ class Scheduler(ContextServicesMixin):
|
|
|
332
340
|
|
|
333
341
|
def allocate(
|
|
334
342
|
self,
|
|
335
|
-
dates:
|
|
343
|
+
dates: _dtrange | Iterable[_dtrange],
|
|
336
344
|
partly_available: bool = False,
|
|
337
|
-
raster:
|
|
345
|
+
raster: Raster = rasterizer.MIN_RASTER,
|
|
338
346
|
whole_day: bool = False,
|
|
339
|
-
quota:
|
|
347
|
+
quota: int | None = None,
|
|
340
348
|
quota_limit: int = 0,
|
|
341
349
|
grouped: bool = False,
|
|
342
|
-
data:
|
|
350
|
+
data: Any | None = None,
|
|
343
351
|
approve_manually: bool = False,
|
|
344
352
|
skip_overlapping: bool = False,
|
|
345
|
-
) ->
|
|
353
|
+
) -> list[Allocation]:
|
|
346
354
|
""" Allocates a spot in the sedate.
|
|
347
355
|
|
|
348
356
|
An allocation defines a timerange which can be reserved. No
|
|
@@ -596,7 +604,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
596
604
|
|
|
597
605
|
"""
|
|
598
606
|
|
|
599
|
-
assert new_quota > 0,
|
|
607
|
+
assert new_quota > 0, 'Quota must be greater than 0'
|
|
600
608
|
|
|
601
609
|
if new_quota == master.quota:
|
|
602
610
|
return
|
|
@@ -607,7 +615,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
607
615
|
|
|
608
616
|
# Make sure that the quota can be decreased
|
|
609
617
|
mirrors = self.allocation_mirrors_by_master(master)
|
|
610
|
-
allocations = [master
|
|
618
|
+
allocations = [master, *mirrors]
|
|
611
619
|
|
|
612
620
|
free_allocations = [a for a in allocations if a.is_available()]
|
|
613
621
|
|
|
@@ -666,9 +674,9 @@ class Scheduler(ContextServicesMixin):
|
|
|
666
674
|
|
|
667
675
|
def reordered_keylist(
|
|
668
676
|
self,
|
|
669
|
-
allocations:
|
|
677
|
+
allocations: Collection[Allocation],
|
|
670
678
|
new_quota: int
|
|
671
|
-
) ->
|
|
679
|
+
) -> dict[UUID, UUID | None]:
|
|
672
680
|
""" Creates the map for the keylist reorganzation.
|
|
673
681
|
|
|
674
682
|
Each key of the returned dictionary is a resource uuid pointing to the
|
|
@@ -687,7 +695,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
687
695
|
keylist.extend(utils.generate_uuids(master.resource, master.quota))
|
|
688
696
|
|
|
689
697
|
# prefill the map
|
|
690
|
-
reordered:
|
|
698
|
+
reordered: dict[UUID, UUID | None] = {
|
|
691
699
|
k: None
|
|
692
700
|
for k in keylist
|
|
693
701
|
}
|
|
@@ -705,8 +713,8 @@ class Scheduler(ContextServicesMixin):
|
|
|
705
713
|
|
|
706
714
|
def availability(
|
|
707
715
|
self,
|
|
708
|
-
start:
|
|
709
|
-
end:
|
|
716
|
+
start: datetime | None = None,
|
|
717
|
+
end: datetime | None = None
|
|
710
718
|
) -> float:
|
|
711
719
|
"""Goes through all allocations and sums up the availability."""
|
|
712
720
|
|
|
@@ -720,14 +728,14 @@ class Scheduler(ContextServicesMixin):
|
|
|
720
728
|
def move_allocation(
|
|
721
729
|
self,
|
|
722
730
|
master_id: int,
|
|
723
|
-
new_start:
|
|
724
|
-
new_end:
|
|
725
|
-
group:
|
|
726
|
-
new_quota:
|
|
727
|
-
approve_manually:
|
|
731
|
+
new_start: datetime | None = None,
|
|
732
|
+
new_end: datetime | None = None,
|
|
733
|
+
group: UUID | None = None,
|
|
734
|
+
new_quota: int | None = None,
|
|
735
|
+
approve_manually: bool | None = None,
|
|
728
736
|
quota_limit: int = 0,
|
|
729
|
-
whole_day:
|
|
730
|
-
data:
|
|
737
|
+
whole_day: bool | None = None,
|
|
738
|
+
data: Any | None = missing
|
|
731
739
|
) -> None:
|
|
732
740
|
|
|
733
741
|
assert master_id
|
|
@@ -740,7 +748,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
740
748
|
master = self.allocation_by_id(master_id)
|
|
741
749
|
mirrors = self.allocation_mirrors_by_master(master)
|
|
742
750
|
|
|
743
|
-
changing = [master
|
|
751
|
+
changing = [master, *mirrors]
|
|
744
752
|
ids = [c.id for c in changing]
|
|
745
753
|
|
|
746
754
|
assert master.timezone == self.timezone, """
|
|
@@ -834,24 +842,24 @@ class Scheduler(ContextServicesMixin):
|
|
|
834
842
|
if data is not missing:
|
|
835
843
|
change.data = data
|
|
836
844
|
|
|
837
|
-
@
|
|
845
|
+
@overload
|
|
838
846
|
def remove_allocation(self, id: int) -> None: ...
|
|
839
847
|
|
|
840
|
-
@
|
|
848
|
+
@overload
|
|
841
849
|
def remove_allocation(
|
|
842
850
|
self,
|
|
843
851
|
id: None = ...,
|
|
844
852
|
*,
|
|
845
|
-
groups:
|
|
853
|
+
groups: Collection[UUID]
|
|
846
854
|
) -> None: ...
|
|
847
855
|
|
|
848
856
|
def remove_allocation(
|
|
849
857
|
self,
|
|
850
|
-
id:
|
|
851
|
-
groups:
|
|
858
|
+
id: int | None = None,
|
|
859
|
+
groups: Collection[UUID] | None = None
|
|
852
860
|
) -> None:
|
|
853
861
|
|
|
854
|
-
allocations:
|
|
862
|
+
allocations: Iterable[Allocation]
|
|
855
863
|
if id:
|
|
856
864
|
# FIXME: We probably should `assert groups is None`
|
|
857
865
|
# since the parameter does nothing if `id` is
|
|
@@ -894,9 +902,9 @@ class Scheduler(ContextServicesMixin):
|
|
|
894
902
|
|
|
895
903
|
def remove_unused_allocations(
|
|
896
904
|
self,
|
|
897
|
-
start:
|
|
898
|
-
end:
|
|
899
|
-
days:
|
|
905
|
+
start: DateLike,
|
|
906
|
+
end: DateLike,
|
|
907
|
+
days: Iterable[Day | DayNumber] | None = None,
|
|
900
908
|
exclude_groups: bool = False
|
|
901
909
|
) -> int:
|
|
902
910
|
""" Removes all allocations without reservations between start and
|
|
@@ -922,7 +930,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
922
930
|
sedate.as_datetime(end)
|
|
923
931
|
)
|
|
924
932
|
|
|
925
|
-
day_numbers:
|
|
933
|
+
day_numbers: set[DayNumber] | None = None
|
|
926
934
|
if days:
|
|
927
935
|
# get the day from the map - if impossible take the verbatim value
|
|
928
936
|
# this allows for using strings or integers
|
|
@@ -1001,39 +1009,39 @@ class Scheduler(ContextServicesMixin):
|
|
|
1001
1009
|
deleted += 1
|
|
1002
1010
|
return deleted
|
|
1003
1011
|
|
|
1004
|
-
@
|
|
1012
|
+
@overload
|
|
1005
1013
|
def reserve(
|
|
1006
1014
|
self,
|
|
1007
1015
|
email: str,
|
|
1008
|
-
dates:
|
|
1016
|
+
dates: _dtrange | Collection[_dtrange],
|
|
1009
1017
|
group: None = ...,
|
|
1010
|
-
data:
|
|
1011
|
-
session_id:
|
|
1018
|
+
data: Any | None = ...,
|
|
1019
|
+
session_id: UUID | None = ...,
|
|
1012
1020
|
quota: int = ...,
|
|
1013
1021
|
single_token_per_session: bool = ...
|
|
1014
1022
|
) -> UUID: ...
|
|
1015
1023
|
|
|
1016
|
-
@
|
|
1024
|
+
@overload
|
|
1017
1025
|
def reserve(
|
|
1018
1026
|
self,
|
|
1019
1027
|
email: str,
|
|
1020
1028
|
dates: None,
|
|
1021
1029
|
group: UUID,
|
|
1022
|
-
data:
|
|
1023
|
-
session_id:
|
|
1030
|
+
data: Any | None = ...,
|
|
1031
|
+
session_id: UUID | None = ...,
|
|
1024
1032
|
quota: int = ...,
|
|
1025
1033
|
single_token_per_session: bool = ...
|
|
1026
1034
|
) -> UUID: ...
|
|
1027
1035
|
|
|
1028
|
-
@
|
|
1036
|
+
@overload
|
|
1029
1037
|
def reserve(
|
|
1030
1038
|
self,
|
|
1031
1039
|
email: str,
|
|
1032
1040
|
dates: None = ...,
|
|
1033
1041
|
*,
|
|
1034
1042
|
group: UUID,
|
|
1035
|
-
data:
|
|
1036
|
-
session_id:
|
|
1043
|
+
data: Any | None = ...,
|
|
1044
|
+
session_id: UUID | None = ...,
|
|
1037
1045
|
quota: int = ...,
|
|
1038
1046
|
single_token_per_session: bool = ...
|
|
1039
1047
|
) -> UUID: ...
|
|
@@ -1041,10 +1049,10 @@ class Scheduler(ContextServicesMixin):
|
|
|
1041
1049
|
def reserve(
|
|
1042
1050
|
self,
|
|
1043
1051
|
email: str,
|
|
1044
|
-
dates:
|
|
1045
|
-
group:
|
|
1046
|
-
data:
|
|
1047
|
-
session_id:
|
|
1052
|
+
dates: _dtrange | Collection[_dtrange] | None = None,
|
|
1053
|
+
group: UUID | None = None,
|
|
1054
|
+
data: Any | None = None,
|
|
1055
|
+
session_id: UUID | None = None,
|
|
1048
1056
|
quota: int = 1,
|
|
1049
1057
|
single_token_per_session: bool = False
|
|
1050
1058
|
) -> UUID:
|
|
@@ -1173,9 +1181,8 @@ class Scheduler(ContextServicesMixin):
|
|
|
1173
1181
|
if not allocation.contains(start, end):
|
|
1174
1182
|
raise errors.TimerangeTooLong()
|
|
1175
1183
|
|
|
1176
|
-
if allocation.quota_limit
|
|
1177
|
-
|
|
1178
|
-
raise errors.QuotaOverLimit
|
|
1184
|
+
if 0 < allocation.quota_limit < quota:
|
|
1185
|
+
raise errors.QuotaOverLimit
|
|
1179
1186
|
|
|
1180
1187
|
if allocation.quota < quota:
|
|
1181
1188
|
raise errors.QuotaImpossible
|
|
@@ -1194,8 +1201,8 @@ class Scheduler(ContextServicesMixin):
|
|
|
1194
1201
|
# or none of them. As such there's no start / end date which is defined
|
|
1195
1202
|
# implicitly by the allocation
|
|
1196
1203
|
def new_reservations_by_group(
|
|
1197
|
-
group:
|
|
1198
|
-
) ->
|
|
1204
|
+
group: UUID | None
|
|
1205
|
+
) -> Iterator[Reservation]:
|
|
1199
1206
|
|
|
1200
1207
|
if group:
|
|
1201
1208
|
reservation = self.reservation_cls()
|
|
@@ -1213,8 +1220,8 @@ class Scheduler(ContextServicesMixin):
|
|
|
1213
1220
|
|
|
1214
1221
|
# all other reservations are reserved by start/end date
|
|
1215
1222
|
def new_reservations_by_dates(
|
|
1216
|
-
dates:
|
|
1217
|
-
) ->
|
|
1223
|
+
dates: list[tuple[datetime, datetime]]
|
|
1224
|
+
) -> Iterator[Reservation]:
|
|
1218
1225
|
|
|
1219
1226
|
already_reserved_groups = set()
|
|
1220
1227
|
|
|
@@ -1283,12 +1290,12 @@ class Scheduler(ContextServicesMixin):
|
|
|
1283
1290
|
def _approve_reservation_record(
|
|
1284
1291
|
self,
|
|
1285
1292
|
reservation: Reservation
|
|
1286
|
-
) ->
|
|
1293
|
+
) -> list[ReservedSlot]:
|
|
1287
1294
|
|
|
1288
1295
|
# write out the slots
|
|
1289
1296
|
slots_to_reserve = []
|
|
1290
1297
|
|
|
1291
|
-
dates:
|
|
1298
|
+
dates: tuple[_dtrange, ...] | list[_dtrange]
|
|
1292
1299
|
if reservation.target_type == 'group':
|
|
1293
1300
|
dates = self.allocation_dates_by_group(reservation.target)
|
|
1294
1301
|
else:
|
|
@@ -1330,7 +1337,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1330
1337
|
|
|
1331
1338
|
return slots_to_reserve
|
|
1332
1339
|
|
|
1333
|
-
def approve_reservations(self, token: UUID) ->
|
|
1340
|
+
def approve_reservations(self, token: UUID) -> list[ReservedSlot]:
|
|
1334
1341
|
""" This function approves an existing reservation and writes the
|
|
1335
1342
|
reserved slots accordingly.
|
|
1336
1343
|
|
|
@@ -1342,14 +1349,14 @@ class Scheduler(ContextServicesMixin):
|
|
|
1342
1349
|
|
|
1343
1350
|
reservations = self.reservations_by_token(token).all()
|
|
1344
1351
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1352
|
+
try:
|
|
1353
|
+
for reservation in reservations:
|
|
1347
1354
|
slots_to_reserve.extend(
|
|
1348
1355
|
self._approve_reservation_record(reservation)
|
|
1349
1356
|
)
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1357
|
+
except errors.LibresError as e:
|
|
1358
|
+
e.reservation = reservation
|
|
1359
|
+
raise e
|
|
1353
1360
|
|
|
1354
1361
|
events.on_reservations_approved(self.context, reservations)
|
|
1355
1362
|
|
|
@@ -1373,7 +1380,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1373
1380
|
def remove_reservation(
|
|
1374
1381
|
self,
|
|
1375
1382
|
token: UUID,
|
|
1376
|
-
id:
|
|
1383
|
+
id: int | None = None
|
|
1377
1384
|
) -> None:
|
|
1378
1385
|
""" Removes all reserved slots of the given reservation token.
|
|
1379
1386
|
|
|
@@ -1411,7 +1418,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1411
1418
|
def change_reservation_data(
|
|
1412
1419
|
self,
|
|
1413
1420
|
token: UUID,
|
|
1414
|
-
data:
|
|
1421
|
+
data: Any | None
|
|
1415
1422
|
) -> None:
|
|
1416
1423
|
|
|
1417
1424
|
for reservation in self.reservations_by_token(token).all():
|
|
@@ -1419,8 +1426,8 @@ class Scheduler(ContextServicesMixin):
|
|
|
1419
1426
|
|
|
1420
1427
|
def change_reservation_time_candidates(
|
|
1421
1428
|
self,
|
|
1422
|
-
tokens:
|
|
1423
|
-
) ->
|
|
1429
|
+
tokens: Collection[UUID] | None = None
|
|
1430
|
+
) -> Query[Reservation]:
|
|
1424
1431
|
""" Returns the reservations that fullfill the restrictions
|
|
1425
1432
|
imposed by change_reservation_time.
|
|
1426
1433
|
|
|
@@ -1448,7 +1455,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1448
1455
|
id: int,
|
|
1449
1456
|
new_start: datetime,
|
|
1450
1457
|
new_end: datetime
|
|
1451
|
-
) ->
|
|
1458
|
+
) -> Reservation | None:
|
|
1452
1459
|
""" Kept for backwards compatibility, use :meth:`change_reservation`
|
|
1453
1460
|
instead.
|
|
1454
1461
|
|
|
@@ -1468,8 +1475,8 @@ class Scheduler(ContextServicesMixin):
|
|
|
1468
1475
|
id: int,
|
|
1469
1476
|
new_start: datetime,
|
|
1470
1477
|
new_end: datetime,
|
|
1471
|
-
quota:
|
|
1472
|
-
) ->
|
|
1478
|
+
quota: int | None = None
|
|
1479
|
+
) -> Reservation | None:
|
|
1473
1480
|
""" Allows to change the timespan of a reservation under certain
|
|
1474
1481
|
conditions:
|
|
1475
1482
|
|
|
@@ -1493,7 +1500,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1493
1500
|
existing_reservation = self.reservations_by_token(token, id).one()
|
|
1494
1501
|
|
|
1495
1502
|
# if there's nothing to change, do not change
|
|
1496
|
-
if quota is None or existing_reservation.quota == quota:
|
|
1503
|
+
if quota is None or existing_reservation.quota == quota: # noqa: SIM102
|
|
1497
1504
|
if existing_reservation.start == new_start:
|
|
1498
1505
|
ends = (new_end, new_end - timedelta(microseconds=1))
|
|
1499
1506
|
|
|
@@ -1511,7 +1518,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1511
1518
|
if not allocation.contains(new_start, new_end):
|
|
1512
1519
|
raise errors.TimerangeTooLong()
|
|
1513
1520
|
|
|
1514
|
-
reservation_arguments:
|
|
1521
|
+
reservation_arguments: _ReserveArgs = {
|
|
1515
1522
|
'email': existing_reservation.email,
|
|
1516
1523
|
'dates': (new_start, new_end),
|
|
1517
1524
|
'data': existing_reservation.data,
|
|
@@ -1549,13 +1556,13 @@ class Scheduler(ContextServicesMixin):
|
|
|
1549
1556
|
self,
|
|
1550
1557
|
start: datetime,
|
|
1551
1558
|
end: datetime,
|
|
1552
|
-
days:
|
|
1559
|
+
days: Collection[Day | DayNumber] | None = None,
|
|
1553
1560
|
minspots: int = 0,
|
|
1554
1561
|
available_only: bool = False,
|
|
1555
|
-
whole_day:
|
|
1556
|
-
groups:
|
|
1562
|
+
whole_day: Literal['any', 'yes', 'no'] = 'any',
|
|
1563
|
+
groups: Literal['any', 'yes', 'no'] = 'any',
|
|
1557
1564
|
strict: bool = False
|
|
1558
|
-
) ->
|
|
1565
|
+
) -> list[Allocation]:
|
|
1559
1566
|
""" Search allocations using a number of options. The date is split
|
|
1560
1567
|
into date/time. All allocations between start and end date within
|
|
1561
1568
|
the given time (on each day) are included.
|
|
@@ -1623,7 +1630,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1623
1630
|
assert whole_day in ('yes', 'no', 'any')
|
|
1624
1631
|
assert groups in ('yes', 'no', 'any')
|
|
1625
1632
|
|
|
1626
|
-
day_numbers:
|
|
1633
|
+
day_numbers: set[DayNumber] | None
|
|
1627
1634
|
if days:
|
|
1628
1635
|
# get the day from the map - if impossible take the verbatim value
|
|
1629
1636
|
# this allows for using strings or integers
|
|
@@ -1657,9 +1664,11 @@ class Scheduler(ContextServicesMixin):
|
|
|
1657
1664
|
if not allocation.overlaps(s, e):
|
|
1658
1665
|
continue
|
|
1659
1666
|
|
|
1660
|
-
if
|
|
1661
|
-
|
|
1662
|
-
|
|
1667
|
+
if (
|
|
1668
|
+
day_numbers
|
|
1669
|
+
and allocation.display_start().weekday() not in day_numbers
|
|
1670
|
+
):
|
|
1671
|
+
continue
|
|
1663
1672
|
|
|
1664
1673
|
if whole_day != 'any':
|
|
1665
1674
|
if whole_day == 'yes' and not allocation.whole_day:
|
|
@@ -1673,14 +1682,11 @@ class Scheduler(ContextServicesMixin):
|
|
|
1673
1682
|
#
|
|
1674
1683
|
# the spots are later checked again for actual availability, but
|
|
1675
1684
|
# that is a heavier check, so it doesn't belong here.
|
|
1676
|
-
if minspots:
|
|
1677
|
-
|
|
1678
|
-
if allocation.quota_limit < minspots:
|
|
1679
|
-
continue
|
|
1685
|
+
if minspots and (0 < allocation.quota_limit < minspots):
|
|
1686
|
+
continue
|
|
1680
1687
|
|
|
1681
|
-
if available_only:
|
|
1682
|
-
|
|
1683
|
-
continue
|
|
1688
|
+
if available_only and not allocation.find_spot(s, e):
|
|
1689
|
+
continue
|
|
1684
1690
|
|
|
1685
1691
|
if minspots:
|
|
1686
1692
|
availability = self.availability(
|
|
@@ -1750,7 +1756,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1750
1756
|
self,
|
|
1751
1757
|
start: datetime,
|
|
1752
1758
|
end: datetime
|
|
1753
|
-
) ->
|
|
1759
|
+
) -> list[Allocation]:
|
|
1754
1760
|
""" Returns a list of allocations that are free within start and end.
|
|
1755
1761
|
These allocations may come from the master or any of the mirrors.
|
|
1756
1762
|
|
|
@@ -1777,8 +1783,8 @@ class Scheduler(ContextServicesMixin):
|
|
|
1777
1783
|
def reserved_slots_by_reservation(
|
|
1778
1784
|
self,
|
|
1779
1785
|
token: UUID,
|
|
1780
|
-
id:
|
|
1781
|
-
) ->
|
|
1786
|
+
id: int | None = None
|
|
1787
|
+
) -> Query[ReservedSlot]:
|
|
1782
1788
|
""" Returns all reserved slots of the given reservation.
|
|
1783
1789
|
The id is optional and may be used only return the slots from a
|
|
1784
1790
|
specific reservation matching token and id.
|
|
@@ -1796,7 +1802,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1796
1802
|
ids = allocations.with_entities(Allocation.id)
|
|
1797
1803
|
return query.filter(ReservedSlot.allocation_id.in_(ids))
|
|
1798
1804
|
|
|
1799
|
-
def reservations_by_group(self, group: UUID) ->
|
|
1805
|
+
def reservations_by_group(self, group: UUID) -> Query[Reservation]:
|
|
1800
1806
|
tokens = self.managed_reservations().with_entities(Reservation.token)
|
|
1801
1807
|
tokens = tokens.filter(Reservation.target == group)
|
|
1802
1808
|
|
|
@@ -1807,7 +1813,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1807
1813
|
def reservations_by_allocation(
|
|
1808
1814
|
self,
|
|
1809
1815
|
allocation_id: int
|
|
1810
|
-
) ->
|
|
1816
|
+
) -> Query[Reservation]:
|
|
1811
1817
|
|
|
1812
1818
|
master = self.allocation_by_id(allocation_id)
|
|
1813
1819
|
return self.reservations_by_group(master.group)
|
|
@@ -1815,8 +1821,8 @@ class Scheduler(ContextServicesMixin):
|
|
|
1815
1821
|
def reservations_by_token(
|
|
1816
1822
|
self,
|
|
1817
1823
|
token: UUID,
|
|
1818
|
-
id:
|
|
1819
|
-
) ->
|
|
1824
|
+
id: int | None = None
|
|
1825
|
+
) -> Query[Reservation]:
|
|
1820
1826
|
|
|
1821
1827
|
query = self.managed_reservations()
|
|
1822
1828
|
query = query.filter(Reservation.token == token)
|