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/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,24 +303,27 @@ 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
|
-
dates_query = query.with_entities(
|
|
309
|
+
dates_query: Query[tuple[datetime, datetime]] = query.with_entities(
|
|
310
|
+
Allocation._start,
|
|
311
|
+
Allocation._end
|
|
312
|
+
)
|
|
302
313
|
return dates_query.all()
|
|
303
314
|
|
|
304
315
|
def allocation_mirrors_by_master(
|
|
305
316
|
self,
|
|
306
317
|
master: Allocation
|
|
307
|
-
) ->
|
|
318
|
+
) -> list[Allocation]:
|
|
308
319
|
return [s for s in master.siblings() if not s.is_master]
|
|
309
320
|
|
|
310
321
|
def allocation_dates_by_ids(
|
|
311
322
|
self,
|
|
312
|
-
ids:
|
|
313
|
-
start_time:
|
|
314
|
-
end_time:
|
|
315
|
-
) ->
|
|
323
|
+
ids: Collection[int],
|
|
324
|
+
start_time: time | None = None,
|
|
325
|
+
end_time: time | None = None
|
|
326
|
+
) -> Iterator[tuple[datetime, datetime]]:
|
|
316
327
|
|
|
317
328
|
for allocation in self.allocations_by_ids(ids).all():
|
|
318
329
|
|
|
@@ -323,26 +334,26 @@ class Scheduler(ContextServicesMixin):
|
|
|
323
334
|
|
|
324
335
|
yield s_dt, e_dt - timedelta(microseconds=1)
|
|
325
336
|
|
|
326
|
-
def manual_approval_required(self, ids:
|
|
337
|
+
def manual_approval_required(self, ids: Collection[int]) -> bool:
|
|
327
338
|
""" Returns True if any of the allocations require manual approval. """
|
|
328
339
|
query = self.allocations_by_ids(ids)
|
|
329
340
|
query = query.filter(Allocation.approve_manually == True)
|
|
330
341
|
|
|
331
|
-
return self.session.query(query.exists()).scalar()
|
|
342
|
+
return self.session.query(query.exists()).scalar() # type: ignore[no-any-return]
|
|
332
343
|
|
|
333
344
|
def allocate(
|
|
334
345
|
self,
|
|
335
|
-
dates:
|
|
346
|
+
dates: _dtrange | Iterable[_dtrange],
|
|
336
347
|
partly_available: bool = False,
|
|
337
|
-
raster:
|
|
348
|
+
raster: Raster = rasterizer.MIN_RASTER,
|
|
338
349
|
whole_day: bool = False,
|
|
339
|
-
quota:
|
|
350
|
+
quota: int | None = None,
|
|
340
351
|
quota_limit: int = 0,
|
|
341
352
|
grouped: bool = False,
|
|
342
|
-
data:
|
|
353
|
+
data: Any | None = None,
|
|
343
354
|
approve_manually: bool = False,
|
|
344
355
|
skip_overlapping: bool = False,
|
|
345
|
-
) ->
|
|
356
|
+
) -> list[Allocation]:
|
|
346
357
|
""" Allocates a spot in the sedate.
|
|
347
358
|
|
|
348
359
|
An allocation defines a timerange which can be reserved. No
|
|
@@ -596,7 +607,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
596
607
|
|
|
597
608
|
"""
|
|
598
609
|
|
|
599
|
-
assert new_quota > 0,
|
|
610
|
+
assert new_quota > 0, 'Quota must be greater than 0'
|
|
600
611
|
|
|
601
612
|
if new_quota == master.quota:
|
|
602
613
|
return
|
|
@@ -607,7 +618,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
607
618
|
|
|
608
619
|
# Make sure that the quota can be decreased
|
|
609
620
|
mirrors = self.allocation_mirrors_by_master(master)
|
|
610
|
-
allocations = [master
|
|
621
|
+
allocations = [master, *mirrors]
|
|
611
622
|
|
|
612
623
|
free_allocations = [a for a in allocations if a.is_available()]
|
|
613
624
|
|
|
@@ -666,9 +677,9 @@ class Scheduler(ContextServicesMixin):
|
|
|
666
677
|
|
|
667
678
|
def reordered_keylist(
|
|
668
679
|
self,
|
|
669
|
-
allocations:
|
|
680
|
+
allocations: Collection[Allocation],
|
|
670
681
|
new_quota: int
|
|
671
|
-
) ->
|
|
682
|
+
) -> dict[UUID, UUID | None]:
|
|
672
683
|
""" Creates the map for the keylist reorganzation.
|
|
673
684
|
|
|
674
685
|
Each key of the returned dictionary is a resource uuid pointing to the
|
|
@@ -687,7 +698,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
687
698
|
keylist.extend(utils.generate_uuids(master.resource, master.quota))
|
|
688
699
|
|
|
689
700
|
# prefill the map
|
|
690
|
-
reordered:
|
|
701
|
+
reordered: dict[UUID, UUID | None] = {
|
|
691
702
|
k: None
|
|
692
703
|
for k in keylist
|
|
693
704
|
}
|
|
@@ -705,8 +716,8 @@ class Scheduler(ContextServicesMixin):
|
|
|
705
716
|
|
|
706
717
|
def availability(
|
|
707
718
|
self,
|
|
708
|
-
start:
|
|
709
|
-
end:
|
|
719
|
+
start: datetime | None = None,
|
|
720
|
+
end: datetime | None = None
|
|
710
721
|
) -> float:
|
|
711
722
|
"""Goes through all allocations and sums up the availability."""
|
|
712
723
|
|
|
@@ -720,14 +731,14 @@ class Scheduler(ContextServicesMixin):
|
|
|
720
731
|
def move_allocation(
|
|
721
732
|
self,
|
|
722
733
|
master_id: int,
|
|
723
|
-
new_start:
|
|
724
|
-
new_end:
|
|
725
|
-
group:
|
|
726
|
-
new_quota:
|
|
727
|
-
approve_manually:
|
|
734
|
+
new_start: datetime | None = None,
|
|
735
|
+
new_end: datetime | None = None,
|
|
736
|
+
group: UUID | None = None,
|
|
737
|
+
new_quota: int | None = None,
|
|
738
|
+
approve_manually: bool | None = None,
|
|
728
739
|
quota_limit: int = 0,
|
|
729
|
-
whole_day:
|
|
730
|
-
data:
|
|
740
|
+
whole_day: bool | None = None,
|
|
741
|
+
data: Any | None = missing
|
|
731
742
|
) -> None:
|
|
732
743
|
|
|
733
744
|
assert master_id
|
|
@@ -740,7 +751,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
740
751
|
master = self.allocation_by_id(master_id)
|
|
741
752
|
mirrors = self.allocation_mirrors_by_master(master)
|
|
742
753
|
|
|
743
|
-
changing = [master
|
|
754
|
+
changing = [master, *mirrors]
|
|
744
755
|
ids = [c.id for c in changing]
|
|
745
756
|
|
|
746
757
|
assert master.timezone == self.timezone, """
|
|
@@ -834,24 +845,24 @@ class Scheduler(ContextServicesMixin):
|
|
|
834
845
|
if data is not missing:
|
|
835
846
|
change.data = data
|
|
836
847
|
|
|
837
|
-
@
|
|
848
|
+
@overload
|
|
838
849
|
def remove_allocation(self, id: int) -> None: ...
|
|
839
850
|
|
|
840
|
-
@
|
|
851
|
+
@overload
|
|
841
852
|
def remove_allocation(
|
|
842
853
|
self,
|
|
843
854
|
id: None = ...,
|
|
844
855
|
*,
|
|
845
|
-
groups:
|
|
856
|
+
groups: Collection[UUID]
|
|
846
857
|
) -> None: ...
|
|
847
858
|
|
|
848
859
|
def remove_allocation(
|
|
849
860
|
self,
|
|
850
|
-
id:
|
|
851
|
-
groups:
|
|
861
|
+
id: int | None = None,
|
|
862
|
+
groups: Collection[UUID] | None = None
|
|
852
863
|
) -> None:
|
|
853
864
|
|
|
854
|
-
allocations:
|
|
865
|
+
allocations: Iterable[Allocation]
|
|
855
866
|
if id:
|
|
856
867
|
# FIXME: We probably should `assert groups is None`
|
|
857
868
|
# since the parameter does nothing if `id` is
|
|
@@ -894,9 +905,9 @@ class Scheduler(ContextServicesMixin):
|
|
|
894
905
|
|
|
895
906
|
def remove_unused_allocations(
|
|
896
907
|
self,
|
|
897
|
-
start:
|
|
898
|
-
end:
|
|
899
|
-
days:
|
|
908
|
+
start: DateLike,
|
|
909
|
+
end: DateLike,
|
|
910
|
+
days: Iterable[Day | DayNumber] | None = None,
|
|
900
911
|
exclude_groups: bool = False
|
|
901
912
|
) -> int:
|
|
902
913
|
""" Removes all allocations without reservations between start and
|
|
@@ -922,7 +933,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
922
933
|
sedate.as_datetime(end)
|
|
923
934
|
)
|
|
924
935
|
|
|
925
|
-
day_numbers:
|
|
936
|
+
day_numbers: set[DayNumber] | None = None
|
|
926
937
|
if days:
|
|
927
938
|
# get the day from the map - if impossible take the verbatim value
|
|
928
939
|
# this allows for using strings or integers
|
|
@@ -1001,39 +1012,39 @@ class Scheduler(ContextServicesMixin):
|
|
|
1001
1012
|
deleted += 1
|
|
1002
1013
|
return deleted
|
|
1003
1014
|
|
|
1004
|
-
@
|
|
1015
|
+
@overload
|
|
1005
1016
|
def reserve(
|
|
1006
1017
|
self,
|
|
1007
1018
|
email: str,
|
|
1008
|
-
dates:
|
|
1019
|
+
dates: _dtrange | Collection[_dtrange],
|
|
1009
1020
|
group: None = ...,
|
|
1010
|
-
data:
|
|
1011
|
-
session_id:
|
|
1021
|
+
data: Any | None = ...,
|
|
1022
|
+
session_id: UUID | None = ...,
|
|
1012
1023
|
quota: int = ...,
|
|
1013
1024
|
single_token_per_session: bool = ...
|
|
1014
1025
|
) -> UUID: ...
|
|
1015
1026
|
|
|
1016
|
-
@
|
|
1027
|
+
@overload
|
|
1017
1028
|
def reserve(
|
|
1018
1029
|
self,
|
|
1019
1030
|
email: str,
|
|
1020
1031
|
dates: None,
|
|
1021
1032
|
group: UUID,
|
|
1022
|
-
data:
|
|
1023
|
-
session_id:
|
|
1033
|
+
data: Any | None = ...,
|
|
1034
|
+
session_id: UUID | None = ...,
|
|
1024
1035
|
quota: int = ...,
|
|
1025
1036
|
single_token_per_session: bool = ...
|
|
1026
1037
|
) -> UUID: ...
|
|
1027
1038
|
|
|
1028
|
-
@
|
|
1039
|
+
@overload
|
|
1029
1040
|
def reserve(
|
|
1030
1041
|
self,
|
|
1031
1042
|
email: str,
|
|
1032
1043
|
dates: None = ...,
|
|
1033
1044
|
*,
|
|
1034
1045
|
group: UUID,
|
|
1035
|
-
data:
|
|
1036
|
-
session_id:
|
|
1046
|
+
data: Any | None = ...,
|
|
1047
|
+
session_id: UUID | None = ...,
|
|
1037
1048
|
quota: int = ...,
|
|
1038
1049
|
single_token_per_session: bool = ...
|
|
1039
1050
|
) -> UUID: ...
|
|
@@ -1041,10 +1052,10 @@ class Scheduler(ContextServicesMixin):
|
|
|
1041
1052
|
def reserve(
|
|
1042
1053
|
self,
|
|
1043
1054
|
email: str,
|
|
1044
|
-
dates:
|
|
1045
|
-
group:
|
|
1046
|
-
data:
|
|
1047
|
-
session_id:
|
|
1055
|
+
dates: _dtrange | Collection[_dtrange] | None = None,
|
|
1056
|
+
group: UUID | None = None,
|
|
1057
|
+
data: Any | None = None,
|
|
1058
|
+
session_id: UUID | None = None,
|
|
1048
1059
|
quota: int = 1,
|
|
1049
1060
|
single_token_per_session: bool = False
|
|
1050
1061
|
) -> UUID:
|
|
@@ -1173,9 +1184,8 @@ class Scheduler(ContextServicesMixin):
|
|
|
1173
1184
|
if not allocation.contains(start, end):
|
|
1174
1185
|
raise errors.TimerangeTooLong()
|
|
1175
1186
|
|
|
1176
|
-
if allocation.quota_limit
|
|
1177
|
-
|
|
1178
|
-
raise errors.QuotaOverLimit
|
|
1187
|
+
if 0 < allocation.quota_limit < quota:
|
|
1188
|
+
raise errors.QuotaOverLimit
|
|
1179
1189
|
|
|
1180
1190
|
if allocation.quota < quota:
|
|
1181
1191
|
raise errors.QuotaImpossible
|
|
@@ -1194,8 +1204,8 @@ class Scheduler(ContextServicesMixin):
|
|
|
1194
1204
|
# or none of them. As such there's no start / end date which is defined
|
|
1195
1205
|
# implicitly by the allocation
|
|
1196
1206
|
def new_reservations_by_group(
|
|
1197
|
-
group:
|
|
1198
|
-
) ->
|
|
1207
|
+
group: UUID | None
|
|
1208
|
+
) -> Iterator[Reservation]:
|
|
1199
1209
|
|
|
1200
1210
|
if group:
|
|
1201
1211
|
reservation = self.reservation_cls()
|
|
@@ -1213,8 +1223,8 @@ class Scheduler(ContextServicesMixin):
|
|
|
1213
1223
|
|
|
1214
1224
|
# all other reservations are reserved by start/end date
|
|
1215
1225
|
def new_reservations_by_dates(
|
|
1216
|
-
dates:
|
|
1217
|
-
) ->
|
|
1226
|
+
dates: list[tuple[datetime, datetime]]
|
|
1227
|
+
) -> Iterator[Reservation]:
|
|
1218
1228
|
|
|
1219
1229
|
already_reserved_groups = set()
|
|
1220
1230
|
|
|
@@ -1266,8 +1276,9 @@ class Scheduler(ContextServicesMixin):
|
|
|
1266
1276
|
# reservation twice on a single session
|
|
1267
1277
|
if session_id:
|
|
1268
1278
|
found = self.queries.reservations_by_session(session_id)
|
|
1269
|
-
|
|
1270
|
-
|
|
1279
|
+
found_set: set[tuple[UUID, datetime | None]] = set(
|
|
1280
|
+
found.with_entities(Reservation.target, Reservation.start)
|
|
1281
|
+
)
|
|
1271
1282
|
|
|
1272
1283
|
for reservation in reservations:
|
|
1273
1284
|
if (reservation.target, reservation.start) in found_set:
|
|
@@ -1283,12 +1294,12 @@ class Scheduler(ContextServicesMixin):
|
|
|
1283
1294
|
def _approve_reservation_record(
|
|
1284
1295
|
self,
|
|
1285
1296
|
reservation: Reservation
|
|
1286
|
-
) ->
|
|
1297
|
+
) -> list[ReservedSlot]:
|
|
1287
1298
|
|
|
1288
1299
|
# write out the slots
|
|
1289
1300
|
slots_to_reserve = []
|
|
1290
1301
|
|
|
1291
|
-
dates:
|
|
1302
|
+
dates: tuple[_dtrange, ...] | list[_dtrange]
|
|
1292
1303
|
if reservation.target_type == 'group':
|
|
1293
1304
|
dates = self.allocation_dates_by_group(reservation.target)
|
|
1294
1305
|
else:
|
|
@@ -1330,7 +1341,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1330
1341
|
|
|
1331
1342
|
return slots_to_reserve
|
|
1332
1343
|
|
|
1333
|
-
def approve_reservations(self, token: UUID) ->
|
|
1344
|
+
def approve_reservations(self, token: UUID) -> list[ReservedSlot]:
|
|
1334
1345
|
""" This function approves an existing reservation and writes the
|
|
1335
1346
|
reserved slots accordingly.
|
|
1336
1347
|
|
|
@@ -1342,14 +1353,14 @@ class Scheduler(ContextServicesMixin):
|
|
|
1342
1353
|
|
|
1343
1354
|
reservations = self.reservations_by_token(token).all()
|
|
1344
1355
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1356
|
+
try:
|
|
1357
|
+
for reservation in reservations:
|
|
1347
1358
|
slots_to_reserve.extend(
|
|
1348
1359
|
self._approve_reservation_record(reservation)
|
|
1349
1360
|
)
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1361
|
+
except errors.LibresError as e:
|
|
1362
|
+
e.reservation = reservation
|
|
1363
|
+
raise e
|
|
1353
1364
|
|
|
1354
1365
|
events.on_reservations_approved(self.context, reservations)
|
|
1355
1366
|
|
|
@@ -1373,7 +1384,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1373
1384
|
def remove_reservation(
|
|
1374
1385
|
self,
|
|
1375
1386
|
token: UUID,
|
|
1376
|
-
id:
|
|
1387
|
+
id: int | None = None
|
|
1377
1388
|
) -> None:
|
|
1378
1389
|
""" Removes all reserved slots of the given reservation token.
|
|
1379
1390
|
|
|
@@ -1411,7 +1422,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1411
1422
|
def change_reservation_data(
|
|
1412
1423
|
self,
|
|
1413
1424
|
token: UUID,
|
|
1414
|
-
data:
|
|
1425
|
+
data: Any | None
|
|
1415
1426
|
) -> None:
|
|
1416
1427
|
|
|
1417
1428
|
for reservation in self.reservations_by_token(token).all():
|
|
@@ -1419,8 +1430,8 @@ class Scheduler(ContextServicesMixin):
|
|
|
1419
1430
|
|
|
1420
1431
|
def change_reservation_time_candidates(
|
|
1421
1432
|
self,
|
|
1422
|
-
tokens:
|
|
1423
|
-
) ->
|
|
1433
|
+
tokens: Collection[UUID] | None = None
|
|
1434
|
+
) -> Query[Reservation]:
|
|
1424
1435
|
""" Returns the reservations that fullfill the restrictions
|
|
1425
1436
|
imposed by change_reservation_time.
|
|
1426
1437
|
|
|
@@ -1448,7 +1459,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1448
1459
|
id: int,
|
|
1449
1460
|
new_start: datetime,
|
|
1450
1461
|
new_end: datetime
|
|
1451
|
-
) ->
|
|
1462
|
+
) -> Reservation | None:
|
|
1452
1463
|
""" Kept for backwards compatibility, use :meth:`change_reservation`
|
|
1453
1464
|
instead.
|
|
1454
1465
|
|
|
@@ -1468,8 +1479,8 @@ class Scheduler(ContextServicesMixin):
|
|
|
1468
1479
|
id: int,
|
|
1469
1480
|
new_start: datetime,
|
|
1470
1481
|
new_end: datetime,
|
|
1471
|
-
quota:
|
|
1472
|
-
) ->
|
|
1482
|
+
quota: int | None = None
|
|
1483
|
+
) -> Reservation | None:
|
|
1473
1484
|
""" Allows to change the timespan of a reservation under certain
|
|
1474
1485
|
conditions:
|
|
1475
1486
|
|
|
@@ -1493,7 +1504,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1493
1504
|
existing_reservation = self.reservations_by_token(token, id).one()
|
|
1494
1505
|
|
|
1495
1506
|
# if there's nothing to change, do not change
|
|
1496
|
-
if quota is None or existing_reservation.quota == quota:
|
|
1507
|
+
if quota is None or existing_reservation.quota == quota: # noqa: SIM102
|
|
1497
1508
|
if existing_reservation.start == new_start:
|
|
1498
1509
|
ends = (new_end, new_end - timedelta(microseconds=1))
|
|
1499
1510
|
|
|
@@ -1511,7 +1522,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1511
1522
|
if not allocation.contains(new_start, new_end):
|
|
1512
1523
|
raise errors.TimerangeTooLong()
|
|
1513
1524
|
|
|
1514
|
-
reservation_arguments:
|
|
1525
|
+
reservation_arguments: _ReserveArgs = {
|
|
1515
1526
|
'email': existing_reservation.email,
|
|
1516
1527
|
'dates': (new_start, new_end),
|
|
1517
1528
|
'data': existing_reservation.data,
|
|
@@ -1549,13 +1560,13 @@ class Scheduler(ContextServicesMixin):
|
|
|
1549
1560
|
self,
|
|
1550
1561
|
start: datetime,
|
|
1551
1562
|
end: datetime,
|
|
1552
|
-
days:
|
|
1563
|
+
days: Collection[Day | DayNumber] | None = None,
|
|
1553
1564
|
minspots: int = 0,
|
|
1554
1565
|
available_only: bool = False,
|
|
1555
|
-
whole_day:
|
|
1556
|
-
groups:
|
|
1566
|
+
whole_day: Literal['any', 'yes', 'no'] = 'any',
|
|
1567
|
+
groups: Literal['any', 'yes', 'no'] = 'any',
|
|
1557
1568
|
strict: bool = False
|
|
1558
|
-
) ->
|
|
1569
|
+
) -> list[Allocation]:
|
|
1559
1570
|
""" Search allocations using a number of options. The date is split
|
|
1560
1571
|
into date/time. All allocations between start and end date within
|
|
1561
1572
|
the given time (on each day) are included.
|
|
@@ -1623,7 +1634,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1623
1634
|
assert whole_day in ('yes', 'no', 'any')
|
|
1624
1635
|
assert groups in ('yes', 'no', 'any')
|
|
1625
1636
|
|
|
1626
|
-
day_numbers:
|
|
1637
|
+
day_numbers: set[DayNumber] | None
|
|
1627
1638
|
if days:
|
|
1628
1639
|
# get the day from the map - if impossible take the verbatim value
|
|
1629
1640
|
# this allows for using strings or integers
|
|
@@ -1657,9 +1668,11 @@ class Scheduler(ContextServicesMixin):
|
|
|
1657
1668
|
if not allocation.overlaps(s, e):
|
|
1658
1669
|
continue
|
|
1659
1670
|
|
|
1660
|
-
if
|
|
1661
|
-
|
|
1662
|
-
|
|
1671
|
+
if (
|
|
1672
|
+
day_numbers
|
|
1673
|
+
and allocation.display_start().weekday() not in day_numbers
|
|
1674
|
+
):
|
|
1675
|
+
continue
|
|
1663
1676
|
|
|
1664
1677
|
if whole_day != 'any':
|
|
1665
1678
|
if whole_day == 'yes' and not allocation.whole_day:
|
|
@@ -1673,14 +1686,11 @@ class Scheduler(ContextServicesMixin):
|
|
|
1673
1686
|
#
|
|
1674
1687
|
# the spots are later checked again for actual availability, but
|
|
1675
1688
|
# that is a heavier check, so it doesn't belong here.
|
|
1676
|
-
if minspots:
|
|
1677
|
-
|
|
1678
|
-
if allocation.quota_limit < minspots:
|
|
1679
|
-
continue
|
|
1689
|
+
if minspots and (0 < allocation.quota_limit < minspots):
|
|
1690
|
+
continue
|
|
1680
1691
|
|
|
1681
|
-
if available_only:
|
|
1682
|
-
|
|
1683
|
-
continue
|
|
1692
|
+
if available_only and not allocation.find_spot(s, e):
|
|
1693
|
+
continue
|
|
1684
1694
|
|
|
1685
1695
|
if minspots:
|
|
1686
1696
|
availability = self.availability(
|
|
@@ -1750,7 +1760,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1750
1760
|
self,
|
|
1751
1761
|
start: datetime,
|
|
1752
1762
|
end: datetime
|
|
1753
|
-
) ->
|
|
1763
|
+
) -> list[Allocation]:
|
|
1754
1764
|
""" Returns a list of allocations that are free within start and end.
|
|
1755
1765
|
These allocations may come from the master or any of the mirrors.
|
|
1756
1766
|
|
|
@@ -1777,8 +1787,8 @@ class Scheduler(ContextServicesMixin):
|
|
|
1777
1787
|
def reserved_slots_by_reservation(
|
|
1778
1788
|
self,
|
|
1779
1789
|
token: UUID,
|
|
1780
|
-
id:
|
|
1781
|
-
) ->
|
|
1790
|
+
id: int | None = None
|
|
1791
|
+
) -> Query[ReservedSlot]:
|
|
1782
1792
|
""" Returns all reserved slots of the given reservation.
|
|
1783
1793
|
The id is optional and may be used only return the slots from a
|
|
1784
1794
|
specific reservation matching token and id.
|
|
@@ -1796,7 +1806,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1796
1806
|
ids = allocations.with_entities(Allocation.id)
|
|
1797
1807
|
return query.filter(ReservedSlot.allocation_id.in_(ids))
|
|
1798
1808
|
|
|
1799
|
-
def reservations_by_group(self, group: UUID) ->
|
|
1809
|
+
def reservations_by_group(self, group: UUID) -> Query[Reservation]:
|
|
1800
1810
|
tokens = self.managed_reservations().with_entities(Reservation.token)
|
|
1801
1811
|
tokens = tokens.filter(Reservation.target == group)
|
|
1802
1812
|
|
|
@@ -1807,7 +1817,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1807
1817
|
def reservations_by_allocation(
|
|
1808
1818
|
self,
|
|
1809
1819
|
allocation_id: int
|
|
1810
|
-
) ->
|
|
1820
|
+
) -> Query[Reservation]:
|
|
1811
1821
|
|
|
1812
1822
|
master = self.allocation_by_id(allocation_id)
|
|
1813
1823
|
return self.reservations_by_group(master.group)
|
|
@@ -1815,8 +1825,8 @@ class Scheduler(ContextServicesMixin):
|
|
|
1815
1825
|
def reservations_by_token(
|
|
1816
1826
|
self,
|
|
1817
1827
|
token: UUID,
|
|
1818
|
-
id:
|
|
1819
|
-
) ->
|
|
1828
|
+
id: int | None = None
|
|
1829
|
+
) -> Query[Reservation]:
|
|
1820
1830
|
|
|
1821
1831
|
query = self.managed_reservations()
|
|
1822
1832
|
query = query.filter(Reservation.token == token)
|