libres 0.9.0__py3-none-any.whl → 0.10.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 +2 -2
- libres/context/registry.py +2 -0
- libres/db/models/__init__.py +3 -1
- libres/db/models/allocation.py +69 -19
- libres/db/models/blocker.py +164 -0
- libres/db/models/other.py +2 -1
- libres/db/models/reservation.py +2 -7
- libres/db/models/reserved_slot.py +15 -11
- libres/db/models/timespan.py +12 -0
- libres/db/queries.py +2 -1
- libres/db/scheduler.py +420 -24
- {libres-0.9.0.dist-info → libres-0.10.0.dist-info}/METADATA +35 -1
- {libres-0.9.0.dist-info → libres-0.10.0.dist-info}/RECORD +16 -14
- {libres-0.9.0.dist-info → libres-0.10.0.dist-info}/WHEEL +1 -1
- {libres-0.9.0.dist-info → libres-0.10.0.dist-info}/licenses/LICENSE +0 -0
- {libres-0.9.0.dist-info → libres-0.10.0.dist-info}/top_level.txt +0 -0
libres/__init__.py
CHANGED
|
@@ -3,9 +3,9 @@ from __future__ import annotations
|
|
|
3
3
|
from libres.context.registry import create_default_registry
|
|
4
4
|
from libres.db import new_scheduler
|
|
5
5
|
|
|
6
|
-
registry = create_default_registry()
|
|
6
|
+
registry = create_default_registry() # noqa: RUF067
|
|
7
7
|
|
|
8
|
-
__version__ = '0.
|
|
8
|
+
__version__ = '0.10.0'
|
|
9
9
|
__all__ = (
|
|
10
10
|
'new_scheduler',
|
|
11
11
|
'registry'
|
libres/context/registry.py
CHANGED
libres/db/models/__init__.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from libres.db.models.base import ORMBase
|
|
4
4
|
from libres.db.models.allocation import Allocation
|
|
5
|
+
from libres.db.models.blocker import ReservationBlocker
|
|
5
6
|
from libres.db.models.reserved_slot import ReservedSlot
|
|
6
7
|
from libres.db.models.reservation import Reservation
|
|
7
8
|
|
|
@@ -10,5 +11,6 @@ __all__ = (
|
|
|
10
11
|
'ORMBase',
|
|
11
12
|
'Allocation',
|
|
12
13
|
'ReservedSlot',
|
|
13
|
-
'Reservation'
|
|
14
|
+
'Reservation',
|
|
15
|
+
'ReservationBlocker',
|
|
14
16
|
)
|
libres/db/models/allocation.py
CHANGED
|
@@ -10,9 +10,10 @@ from sqlalchemy.schema import Column
|
|
|
10
10
|
from sqlalchemy.schema import Index
|
|
11
11
|
from sqlalchemy.schema import UniqueConstraint
|
|
12
12
|
from sqlalchemy.orm import object_session
|
|
13
|
+
from sqlalchemy.orm import relationship
|
|
13
14
|
from sqlalchemy.orm.util import has_identity
|
|
14
15
|
|
|
15
|
-
from libres.db.models import ORMBase
|
|
16
|
+
from libres.db.models.base import ORMBase
|
|
16
17
|
from libres.db.models.types import UUID, UTCDateTime, JSON
|
|
17
18
|
from libres.db.models.other import OtherModels
|
|
18
19
|
from libres.db.models.timestamp import TimestampMixin
|
|
@@ -136,9 +137,15 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
136
137
|
nullable=False
|
|
137
138
|
)
|
|
138
139
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
# Reserved_slots are eagerly joined since we usually want both
|
|
141
|
+
# allocation and reserved_slots. There's barely a function which does
|
|
142
|
+
# not need to know about reserved slots when working with allocations.
|
|
143
|
+
reserved_slots: relationship[list[ReservedSlot]] = relationship(
|
|
144
|
+
'ReservedSlot',
|
|
145
|
+
lazy='joined',
|
|
146
|
+
cascade='all, delete-orphan',
|
|
147
|
+
back_populates='allocation'
|
|
148
|
+
)
|
|
142
149
|
|
|
143
150
|
__table_args__ = (
|
|
144
151
|
Index('mirror_resource_ix', 'mirror_of', 'resource'),
|
|
@@ -502,15 +509,32 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
502
509
|
"""Returns the availability in percent."""
|
|
503
510
|
|
|
504
511
|
total = self.count_slots()
|
|
505
|
-
|
|
512
|
+
blocked = sum(
|
|
513
|
+
1
|
|
514
|
+
for s in self.reserved_slots
|
|
515
|
+
if s.source_type == 'blocker'
|
|
516
|
+
)
|
|
517
|
+
reserved = sum(
|
|
518
|
+
1
|
|
519
|
+
for s in self.reserved_slots
|
|
520
|
+
if s.source_type == 'reservation'
|
|
521
|
+
)
|
|
506
522
|
|
|
507
|
-
if total ==
|
|
523
|
+
if total == blocked:
|
|
524
|
+
# if everything is blocked this allocation is unavailable
|
|
508
525
|
return 0.0
|
|
509
526
|
|
|
510
|
-
|
|
527
|
+
# blockers detract from the total slots
|
|
528
|
+
# they're not part of the availability
|
|
529
|
+
total -= blocked
|
|
530
|
+
|
|
531
|
+
if total == reserved:
|
|
532
|
+
return 0.0
|
|
533
|
+
|
|
534
|
+
if reserved == 0:
|
|
511
535
|
return 100.0
|
|
512
536
|
|
|
513
|
-
return 100.0 - (
|
|
537
|
+
return 100.0 - 100.0 * (reserved / total)
|
|
514
538
|
|
|
515
539
|
@property
|
|
516
540
|
def normalized_availability(self) -> float:
|
|
@@ -542,27 +566,53 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
|
|
|
542
566
|
# the normalized total slots correspond to the naive delta
|
|
543
567
|
total = naive_delta.total_seconds() // (self.raster * 60)
|
|
544
568
|
if real_delta > naive_delta:
|
|
545
|
-
# this is the most complicated case since we need to
|
|
546
|
-
#
|
|
569
|
+
# this is the most complicated case since we need to reduce the
|
|
570
|
+
# set of reserved slots by the hour we removed from the total
|
|
547
571
|
ambiguous_start = start.replace(
|
|
548
572
|
hour=2, minute=0, second=0, microsecond=0)
|
|
549
573
|
ambiguous_end = ambiguous_start.replace(hour=3)
|
|
550
|
-
|
|
551
|
-
1
|
|
574
|
+
blocked = sum(
|
|
575
|
+
1
|
|
576
|
+
for r in self.reserved_slots
|
|
577
|
+
if r.source_type == 'blocker'
|
|
578
|
+
if not ambiguous_start <= r.start < ambiguous_end
|
|
579
|
+
)
|
|
580
|
+
reserved = sum(
|
|
581
|
+
1
|
|
582
|
+
for r in self.reserved_slots
|
|
583
|
+
if r.source_type == 'reservation'
|
|
552
584
|
if not ambiguous_start <= r.start < ambiguous_end
|
|
553
585
|
)
|
|
554
586
|
else:
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
587
|
+
blocked = sum(
|
|
588
|
+
1
|
|
589
|
+
for s in self.reserved_slots
|
|
590
|
+
if s.source_type == 'blocker'
|
|
591
|
+
)
|
|
592
|
+
reserved = sum(
|
|
593
|
+
1
|
|
594
|
+
for s in self.reserved_slots
|
|
595
|
+
if s.source_type == 'reservation'
|
|
596
|
+
)
|
|
597
|
+
# add one hour's worth of slots to compensate for the extra
|
|
598
|
+
# hour we added to the total.
|
|
599
|
+
reserved += 60 // self.raster
|
|
558
600
|
|
|
559
|
-
if
|
|
560
|
-
|
|
601
|
+
if total == blocked:
|
|
602
|
+
# if everything is blocked this allocation is unavailable
|
|
603
|
+
return 0.0
|
|
604
|
+
|
|
605
|
+
# blockers detract from the total slots
|
|
606
|
+
# they're not part of the availability
|
|
607
|
+
total -= blocked
|
|
561
608
|
|
|
562
|
-
if total ==
|
|
609
|
+
if total == reserved:
|
|
563
610
|
return 0.0
|
|
564
611
|
|
|
565
|
-
|
|
612
|
+
if reserved == 0:
|
|
613
|
+
return 100.0
|
|
614
|
+
|
|
615
|
+
return 100.0 - 100.0 * (reserved / total)
|
|
566
616
|
|
|
567
617
|
@property
|
|
568
618
|
def in_group(self) -> int:
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sedate
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import types
|
|
8
|
+
from sqlalchemy.orm import object_session
|
|
9
|
+
from sqlalchemy.schema import Column
|
|
10
|
+
from sqlalchemy.schema import Index
|
|
11
|
+
|
|
12
|
+
from libres.db.models.base import ORMBase
|
|
13
|
+
from libres.db.models.types import UUID, UTCDateTime
|
|
14
|
+
from libres.db.models.other import OtherModels
|
|
15
|
+
from libres.db.models.timespan import Timespan
|
|
16
|
+
from libres.db.models.timestamp import TimestampMixin
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
from typing import Literal
|
|
20
|
+
from typing import TYPE_CHECKING
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
import uuid
|
|
23
|
+
from sedate.types import TzInfoOrName
|
|
24
|
+
from sqlalchemy.orm import Query
|
|
25
|
+
|
|
26
|
+
from libres.db.models import Allocation
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ReservationBlocker(TimestampMixin, ORMBase, OtherModels):
|
|
30
|
+
"""Describes a reservation blocker.
|
|
31
|
+
|
|
32
|
+
Blockers can be used to signify that an allocation will be blocked for
|
|
33
|
+
the specified time span, in order to e.g. perform cleaning duties on
|
|
34
|
+
the relevant resource.
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
__tablename__ = 'reservation_blockers'
|
|
39
|
+
|
|
40
|
+
id: Column[int] = Column(
|
|
41
|
+
types.Integer(),
|
|
42
|
+
primary_key=True,
|
|
43
|
+
autoincrement=True
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
token: Column[uuid.UUID] = Column(
|
|
47
|
+
UUID(),
|
|
48
|
+
nullable=False,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
target: Column[uuid.UUID] = Column(
|
|
52
|
+
UUID(),
|
|
53
|
+
nullable=False,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
target_type: Column[Literal['group', 'allocation']] = Column(
|
|
57
|
+
types.Enum( # type:ignore[arg-type]
|
|
58
|
+
'group', 'allocation',
|
|
59
|
+
name='reservation_blocker_target_type'
|
|
60
|
+
),
|
|
61
|
+
nullable=False
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
resource: Column[uuid.UUID] = Column(
|
|
65
|
+
UUID(),
|
|
66
|
+
nullable=False
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
start: Column[datetime | None] = Column(
|
|
70
|
+
UTCDateTime(timezone=False),
|
|
71
|
+
nullable=True
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
end: Column[datetime | None] = Column(
|
|
75
|
+
UTCDateTime(timezone=False),
|
|
76
|
+
nullable=True
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
timezone: Column[str | None] = Column(
|
|
80
|
+
types.String(),
|
|
81
|
+
nullable=True
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
reason: Column[str | None] = Column(
|
|
85
|
+
types.String(),
|
|
86
|
+
nullable=True
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
__table_args__ = (
|
|
90
|
+
Index('blocker_target_ix', 'target', 'id'),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def target_allocations(
|
|
94
|
+
self,
|
|
95
|
+
masters_only: bool = True
|
|
96
|
+
) -> Query[Allocation]:
|
|
97
|
+
""" Returns the allocations this blocker is targeting.
|
|
98
|
+
|
|
99
|
+
"""
|
|
100
|
+
Allocation = self.models.Allocation # noqa: N806
|
|
101
|
+
query = object_session(self).query(Allocation)
|
|
102
|
+
query = query.filter(Allocation.group == self.target)
|
|
103
|
+
|
|
104
|
+
if masters_only:
|
|
105
|
+
query = query.filter(Allocation.resource == Allocation.mirror_of)
|
|
106
|
+
|
|
107
|
+
# order by date
|
|
108
|
+
query = query.order_by(Allocation._start)
|
|
109
|
+
|
|
110
|
+
return query # type: ignore[no-any-return]
|
|
111
|
+
|
|
112
|
+
def display_start(
|
|
113
|
+
self,
|
|
114
|
+
timezone: TzInfoOrName | None = None
|
|
115
|
+
) -> datetime:
|
|
116
|
+
"""Does nothing but to form a nice pair to display_end."""
|
|
117
|
+
assert self.start is not None
|
|
118
|
+
if timezone is None:
|
|
119
|
+
assert self.timezone is not None
|
|
120
|
+
timezone = self.timezone
|
|
121
|
+
return sedate.to_timezone(self.start, timezone)
|
|
122
|
+
|
|
123
|
+
def display_end(
|
|
124
|
+
self,
|
|
125
|
+
timezone: TzInfoOrName | None = None
|
|
126
|
+
) -> datetime:
|
|
127
|
+
"""Returns the end plus one microsecond (nicer display)."""
|
|
128
|
+
assert self.end is not None
|
|
129
|
+
if timezone is None:
|
|
130
|
+
assert self.timezone is not None
|
|
131
|
+
timezone = self.timezone
|
|
132
|
+
|
|
133
|
+
end = self.end + timedelta(microseconds=1)
|
|
134
|
+
return sedate.to_timezone(end, timezone)
|
|
135
|
+
|
|
136
|
+
def timespans(self) -> list[Timespan]:
|
|
137
|
+
""" Returns the timespans targeted by this blocker.
|
|
138
|
+
|
|
139
|
+
The result is a list of :class:`~libres.db.models.timespan.Timespan`
|
|
140
|
+
timespans. The start and end are the start and end dates of the
|
|
141
|
+
referenced allocations.
|
|
142
|
+
|
|
143
|
+
The timespans are ordered by start.
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
if self.target_type == 'allocation':
|
|
148
|
+
# we don't need to hit the database in this case
|
|
149
|
+
assert self.start is not None
|
|
150
|
+
assert self.end is not None
|
|
151
|
+
return [
|
|
152
|
+
Timespan(self.start, self.end + timedelta(microseconds=1))
|
|
153
|
+
]
|
|
154
|
+
elif self.target_type == 'group':
|
|
155
|
+
return [
|
|
156
|
+
Timespan(allocation.start, allocation.end)
|
|
157
|
+
for allocation in self.target_allocations()
|
|
158
|
+
]
|
|
159
|
+
else:
|
|
160
|
+
raise NotImplementedError
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def title(self) -> str | None:
|
|
164
|
+
return self.reason
|
libres/db/models/other.py
CHANGED
|
@@ -11,6 +11,7 @@ if TYPE_CHECKING:
|
|
|
11
11
|
Allocation: type[_models.Allocation]
|
|
12
12
|
ReservedSlot: type[_models.ReservedSlot]
|
|
13
13
|
Reservation: type[_models.Reservation]
|
|
14
|
+
ReservationBlocker: type[_models.ReservationBlocker]
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
models = None
|
|
@@ -25,7 +26,7 @@ class OtherModels:
|
|
|
25
26
|
global models
|
|
26
27
|
if not models:
|
|
27
28
|
# FIXME: libres.db exports ORMBase, do we really
|
|
28
|
-
# want to makes this
|
|
29
|
+
# want to makes this accessible?
|
|
29
30
|
from libres.db import models as m_
|
|
30
31
|
models = m_
|
|
31
32
|
|
libres/db/models/reservation.py
CHANGED
|
@@ -9,15 +9,15 @@ from sqlalchemy.orm import object_session, deferred
|
|
|
9
9
|
from sqlalchemy.schema import Column
|
|
10
10
|
from sqlalchemy.schema import Index
|
|
11
11
|
|
|
12
|
-
from libres.db.models import ORMBase
|
|
12
|
+
from libres.db.models.base import ORMBase
|
|
13
13
|
from libres.db.models.types import UUID, UTCDateTime, JSON
|
|
14
14
|
from libres.db.models.other import OtherModels
|
|
15
|
+
from libres.db.models.timespan import Timespan
|
|
15
16
|
from libres.db.models.timestamp import TimestampMixin
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
from typing import Any
|
|
19
20
|
from typing import Literal
|
|
20
|
-
from typing import NamedTuple
|
|
21
21
|
from typing import TYPE_CHECKING
|
|
22
22
|
if TYPE_CHECKING:
|
|
23
23
|
import uuid
|
|
@@ -27,11 +27,6 @@ if TYPE_CHECKING:
|
|
|
27
27
|
from libres.db.models import Allocation
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
class Timespan(NamedTuple):
|
|
31
|
-
start: datetime
|
|
32
|
-
end: datetime
|
|
33
|
-
|
|
34
|
-
|
|
35
30
|
class Reservation(TimestampMixin, ORMBase, OtherModels):
|
|
36
31
|
"""Describes a pending or approved reservation.
|
|
37
32
|
|
|
@@ -8,18 +8,19 @@ from sqlalchemy.schema import Column
|
|
|
8
8
|
from sqlalchemy.schema import Index
|
|
9
9
|
from sqlalchemy.schema import ForeignKey
|
|
10
10
|
from sqlalchemy.orm import relationship
|
|
11
|
-
from sqlalchemy.orm import backref
|
|
12
11
|
|
|
13
12
|
from libres.modules.rasterizer import (
|
|
14
13
|
rasterize_start,
|
|
15
14
|
rasterize_end,
|
|
16
15
|
)
|
|
17
16
|
|
|
18
|
-
from libres.db.models import
|
|
17
|
+
from libres.db.models.allocation import Allocation
|
|
18
|
+
from libres.db.models.base import ORMBase
|
|
19
19
|
from libres.db.models.types import UUID, UTCDateTime
|
|
20
20
|
from libres.db.models.timestamp import TimestampMixin
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
from typing import Literal
|
|
23
24
|
from typing import TYPE_CHECKING
|
|
24
25
|
if TYPE_CHECKING:
|
|
25
26
|
import uuid
|
|
@@ -52,22 +53,25 @@ class ReservedSlot(TimestampMixin, ORMBase):
|
|
|
52
53
|
|
|
53
54
|
allocation_id: Column[int] = Column(
|
|
54
55
|
types.Integer(),
|
|
55
|
-
ForeignKey(
|
|
56
|
+
ForeignKey('allocations.id'),
|
|
56
57
|
nullable=False
|
|
57
58
|
)
|
|
58
59
|
|
|
60
|
+
# Reserved_slots are eagerly joined since we usually want both
|
|
61
|
+
# allocation and reserved_slots. There's barely a function which does
|
|
62
|
+
# not need to know about reserved slots when working with allocations.
|
|
59
63
|
allocation: relationship[Allocation] = relationship(
|
|
60
64
|
Allocation,
|
|
61
65
|
primaryjoin=Allocation.id == allocation_id,
|
|
66
|
+
back_populates='reserved_slots',
|
|
67
|
+
)
|
|
62
68
|
|
|
63
|
-
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
cascade='all, delete-orphan'
|
|
70
|
-
)
|
|
69
|
+
source_type: Column[Literal['reservation', 'blocker']] = Column(
|
|
70
|
+
types.Enum( # type:ignore[arg-type]
|
|
71
|
+
'reservation', 'blocker',
|
|
72
|
+
name='reserved_slot_source_type'
|
|
73
|
+
),
|
|
74
|
+
nullable=False
|
|
71
75
|
)
|
|
72
76
|
|
|
73
77
|
reservation_token: Column[uuid.UUID] = Column(
|
libres/db/queries.py
CHANGED
|
@@ -253,7 +253,7 @@ class Queries(ContextServicesMixin):
|
|
|
253
253
|
|
|
254
254
|
slots = self.session.query(ReservedSlot).filter(
|
|
255
255
|
ReservedSlot.reservation_token == token
|
|
256
|
-
)
|
|
256
|
+
).filter(ReservedSlot.source_type == 'reservation')
|
|
257
257
|
|
|
258
258
|
slots.delete('fetch')
|
|
259
259
|
|
|
@@ -340,6 +340,7 @@ class Queries(ContextServicesMixin):
|
|
|
340
340
|
reservations.with_entities(Reservation.token)
|
|
341
341
|
)
|
|
342
342
|
)
|
|
343
|
+
slots = slots.filter(ReservedSlot.source_type == 'reservation')
|
|
343
344
|
|
|
344
345
|
slots.delete('fetch')
|
|
345
346
|
reservations.delete('fetch')
|
libres/db/scheduler.py
CHANGED
|
@@ -10,7 +10,11 @@ from sqlalchemy.sql import and_, not_
|
|
|
10
10
|
from uuid import uuid4 as new_uuid, UUID
|
|
11
11
|
|
|
12
12
|
from libres.context.core import ContextServicesMixin
|
|
13
|
-
from libres.db.models import ORMBase
|
|
13
|
+
from libres.db.models import ORMBase
|
|
14
|
+
from libres.db.models import Allocation
|
|
15
|
+
from libres.db.models import ReservedSlot
|
|
16
|
+
from libres.db.models import Reservation
|
|
17
|
+
from libres.db.models import ReservationBlocker
|
|
14
18
|
from libres.db.queries import Queries
|
|
15
19
|
from libres.modules import errors
|
|
16
20
|
from libres.modules import events
|
|
@@ -195,6 +199,13 @@ class Scheduler(ContextServicesMixin):
|
|
|
195
199
|
|
|
196
200
|
return query
|
|
197
201
|
|
|
202
|
+
def managed_blockers(self) -> Query[ReservationBlocker]:
|
|
203
|
+
""" The blockers managed by this scheduler / resource. """
|
|
204
|
+
query = self.session.query(ReservationBlocker)
|
|
205
|
+
query = query.filter(ReservationBlocker.resource == self.resource)
|
|
206
|
+
|
|
207
|
+
return query
|
|
208
|
+
|
|
198
209
|
def extinguish_managed_records(self) -> None:
|
|
199
210
|
""" WARNING:
|
|
200
211
|
Completely removes any trace of the records managed by this scheduler.
|
|
@@ -202,6 +213,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
202
213
|
|
|
203
214
|
"""
|
|
204
215
|
self.managed_reservations().delete('fetch')
|
|
216
|
+
self.managed_blockers().delete('fetch')
|
|
205
217
|
self.managed_reserved_slots().delete('fetch')
|
|
206
218
|
self.managed_allocations().delete('fetch')
|
|
207
219
|
|
|
@@ -265,17 +277,43 @@ class Scheduler(ContextServicesMixin):
|
|
|
265
277
|
if id is not None:
|
|
266
278
|
groups = groups.filter(Reservation.id == id)
|
|
267
279
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
280
|
+
query: Query[Allocation] = self.managed_allocations()
|
|
281
|
+
query = query.join(ReservedSlot)
|
|
282
|
+
query = query.filter(Allocation.group.in_(groups))
|
|
283
|
+
query = query.filter(
|
|
284
|
+
ReservedSlot.reservation_token == token
|
|
285
|
+
)
|
|
286
|
+
query = query.filter(
|
|
287
|
+
ReservedSlot.source_type == 'reservation'
|
|
288
|
+
)
|
|
289
|
+
return query
|
|
271
290
|
|
|
272
|
-
|
|
291
|
+
def allocations_by_blocker(
|
|
292
|
+
self,
|
|
293
|
+
token: UUID,
|
|
294
|
+
id: int | None = None
|
|
295
|
+
) -> Query[Allocation]:
|
|
296
|
+
""" Returns the allocations for the blocker.
|
|
297
|
+
|
|
298
|
+
If you already have a blocker you can instead use target_allocations.
|
|
299
|
+
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
groups = self.managed_blockers()
|
|
303
|
+
groups = groups.with_entities(ReservationBlocker.target)
|
|
304
|
+
groups = groups.filter(ReservationBlocker.token == token)
|
|
305
|
+
|
|
306
|
+
if id is not None:
|
|
307
|
+
groups = groups.filter(ReservationBlocker.id == id)
|
|
308
|
+
|
|
309
|
+
query: Query[Allocation] = self.managed_allocations()
|
|
273
310
|
query = query.join(ReservedSlot)
|
|
311
|
+
query = query.filter(Allocation.group.in_(groups))
|
|
274
312
|
query = query.filter(
|
|
275
313
|
ReservedSlot.reservation_token == token
|
|
276
314
|
)
|
|
277
315
|
query = query.filter(
|
|
278
|
-
ReservedSlot.
|
|
316
|
+
ReservedSlot.source_type == 'blocker'
|
|
279
317
|
)
|
|
280
318
|
return query
|
|
281
319
|
|
|
@@ -957,6 +995,10 @@ class Scheduler(ContextServicesMixin):
|
|
|
957
995
|
reservations = self.managed_reservations()
|
|
958
996
|
reservations = reservations.with_entities(Reservation.target)
|
|
959
997
|
|
|
998
|
+
# all the blockers
|
|
999
|
+
blockers = self.managed_blockers()
|
|
1000
|
+
blockers = blockers.with_entities(ReservationBlocker.target)
|
|
1001
|
+
|
|
960
1002
|
# all the groups which are fully inside the required scope
|
|
961
1003
|
groups = self.managed_allocations().with_entities(Allocation.group)
|
|
962
1004
|
groups = groups.group_by(Allocation.group)
|
|
@@ -994,6 +1036,11 @@ class Scheduler(ContextServicesMixin):
|
|
|
994
1036
|
not_(Allocation.group.in_(reservations))
|
|
995
1037
|
)
|
|
996
1038
|
|
|
1039
|
+
# .. without the ones with blockers
|
|
1040
|
+
candidates = candidates.filter(
|
|
1041
|
+
not_(Allocation.group.in_(blockers))
|
|
1042
|
+
)
|
|
1043
|
+
|
|
997
1044
|
# .. including only the groups fully inside the required scope
|
|
998
1045
|
allocations = candidates.filter(Allocation.group.in_(groups))
|
|
999
1046
|
|
|
@@ -1324,6 +1371,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1324
1371
|
slot.end = slot_end
|
|
1325
1372
|
slot.resource = allocation.resource
|
|
1326
1373
|
slot.reservation_token = reservation.token
|
|
1374
|
+
slot.source_type = 'reservation'
|
|
1327
1375
|
|
|
1328
1376
|
# the slots are written with the allocation
|
|
1329
1377
|
allocation.reserved_slots.append(slot)
|
|
@@ -1416,7 +1464,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1416
1464
|
|
|
1417
1465
|
def change_email(self, token: UUID, new_email: str) -> None:
|
|
1418
1466
|
|
|
1419
|
-
for reservation in self.reservations_by_token(token)
|
|
1467
|
+
for reservation in self.reservations_by_token(token):
|
|
1420
1468
|
reservation.email = new_email
|
|
1421
1469
|
|
|
1422
1470
|
def change_reservation_data(
|
|
@@ -1425,7 +1473,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1425
1473
|
data: Any | None
|
|
1426
1474
|
) -> None:
|
|
1427
1475
|
|
|
1428
|
-
for reservation in self.reservations_by_token(token)
|
|
1476
|
+
for reservation in self.reservations_by_token(token):
|
|
1429
1477
|
reservation.data = data
|
|
1430
1478
|
|
|
1431
1479
|
def change_reservation_time_candidates(
|
|
@@ -1511,7 +1559,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1511
1559
|
if existing_reservation.end in ends:
|
|
1512
1560
|
return None
|
|
1513
1561
|
|
|
1514
|
-
# will
|
|
1562
|
+
# will raise a MultipleResultsFound exception if this is a group
|
|
1515
1563
|
if existing_reservation.status == 'approved':
|
|
1516
1564
|
allocation = self.allocations_by_reservation(token, id).one()
|
|
1517
1565
|
else:
|
|
@@ -1556,6 +1604,298 @@ class Scheduler(ContextServicesMixin):
|
|
|
1556
1604
|
|
|
1557
1605
|
return new_reservation
|
|
1558
1606
|
|
|
1607
|
+
@overload
|
|
1608
|
+
def add_blocker(
|
|
1609
|
+
self,
|
|
1610
|
+
dates: _dtrange | Collection[_dtrange],
|
|
1611
|
+
group: None = ...,
|
|
1612
|
+
reason: str | None = ...,
|
|
1613
|
+
token: UUID | None = ...
|
|
1614
|
+
) -> list[ReservationBlocker]: ...
|
|
1615
|
+
|
|
1616
|
+
@overload
|
|
1617
|
+
def add_blocker(
|
|
1618
|
+
self,
|
|
1619
|
+
dates: None,
|
|
1620
|
+
group: UUID,
|
|
1621
|
+
reason: str | None = ...,
|
|
1622
|
+
token: UUID | None = ...
|
|
1623
|
+
) -> list[ReservationBlocker]: ...
|
|
1624
|
+
|
|
1625
|
+
@overload
|
|
1626
|
+
def add_blocker(
|
|
1627
|
+
self,
|
|
1628
|
+
dates: None = ...,
|
|
1629
|
+
*,
|
|
1630
|
+
group: UUID,
|
|
1631
|
+
reason: str | None = ...,
|
|
1632
|
+
token: UUID | None = ...
|
|
1633
|
+
) -> list[ReservationBlocker]: ...
|
|
1634
|
+
|
|
1635
|
+
def add_blocker(
|
|
1636
|
+
self,
|
|
1637
|
+
dates: _dtrange | Collection[_dtrange] | None = None,
|
|
1638
|
+
group: UUID | None = None,
|
|
1639
|
+
reason: str | None = None,
|
|
1640
|
+
token: UUID | None = None
|
|
1641
|
+
) -> list[ReservationBlocker]:
|
|
1642
|
+
""" Adds a blocker to one or many allocations.
|
|
1643
|
+
|
|
1644
|
+
Returns a list of `ReservationBlocker`, compared to reservations
|
|
1645
|
+
blockers are added in a single step and don't require approval.
|
|
1646
|
+
|
|
1647
|
+
Blockers are intended for administrative purposes, where adding
|
|
1648
|
+
a reservation would be too cumbersome by comparison.
|
|
1649
|
+
|
|
1650
|
+
:dates:
|
|
1651
|
+
The dates to reserve. May either be a tuple of start/end datetimes
|
|
1652
|
+
or a list of such tuples.
|
|
1653
|
+
|
|
1654
|
+
:group:
|
|
1655
|
+
The allocation group to reserve. ``dates``and ``group`` are
|
|
1656
|
+
mutually exclusive.
|
|
1657
|
+
|
|
1658
|
+
:reason:
|
|
1659
|
+
The reason for blocking the targeted ranges.
|
|
1660
|
+
|
|
1661
|
+
"""
|
|
1662
|
+
|
|
1663
|
+
assert (dates or group) and not (dates and group)
|
|
1664
|
+
|
|
1665
|
+
if group:
|
|
1666
|
+
dates = self.allocation_dates_by_group(group)
|
|
1667
|
+
|
|
1668
|
+
assert dates is not None
|
|
1669
|
+
dates = self._prepare_dates(dates)
|
|
1670
|
+
timezone = self.timezone
|
|
1671
|
+
|
|
1672
|
+
# First, the request is checked for saneness. If any requested
|
|
1673
|
+
# date cannot be reserved the request as a whole fails.
|
|
1674
|
+
for start, end in dates:
|
|
1675
|
+
|
|
1676
|
+
# are the parameters valid?
|
|
1677
|
+
if not utils.is_valid_reservation_length(start, end, timezone):
|
|
1678
|
+
raise errors.ReservationTooLong
|
|
1679
|
+
|
|
1680
|
+
if start > end or (end - start).seconds < 5 * 60:
|
|
1681
|
+
raise errors.ReservationTooShort
|
|
1682
|
+
|
|
1683
|
+
# can all allocations be reserved?
|
|
1684
|
+
for allocation in self.allocations_in_range(start, end):
|
|
1685
|
+
|
|
1686
|
+
# start and end are not rasterized, so we need this check
|
|
1687
|
+
if not allocation.overlaps(start, end):
|
|
1688
|
+
continue
|
|
1689
|
+
|
|
1690
|
+
assert allocation.is_master
|
|
1691
|
+
|
|
1692
|
+
if not allocation.find_spot(start, end):
|
|
1693
|
+
raise errors.AlreadyReservedError
|
|
1694
|
+
|
|
1695
|
+
free = self.free_allocations_count(allocation, start, end)
|
|
1696
|
+
if free < allocation.quota:
|
|
1697
|
+
raise errors.AlreadyReservedError
|
|
1698
|
+
|
|
1699
|
+
if not allocation.contains(start, end):
|
|
1700
|
+
raise errors.TimerangeTooLong()
|
|
1701
|
+
|
|
1702
|
+
# ok, we're good to go
|
|
1703
|
+
if token is None:
|
|
1704
|
+
token = new_uuid()
|
|
1705
|
+
reserved_slots = []
|
|
1706
|
+
|
|
1707
|
+
def create_reserved_slots(
|
|
1708
|
+
allocation: Allocation,
|
|
1709
|
+
start: datetime,
|
|
1710
|
+
end: datetime,
|
|
1711
|
+
including_mirrors: bool = True
|
|
1712
|
+
) -> None:
|
|
1713
|
+
for slot_start, slot_end in allocation.all_slots(start, end):
|
|
1714
|
+
slot = ReservedSlot()
|
|
1715
|
+
slot.start = slot_start
|
|
1716
|
+
slot.end = slot_end
|
|
1717
|
+
slot.resource = allocation.resource
|
|
1718
|
+
slot.reservation_token = token
|
|
1719
|
+
slot.source_type = 'blocker'
|
|
1720
|
+
|
|
1721
|
+
# the slots are written with the allocation
|
|
1722
|
+
allocation.reserved_slots.append(slot)
|
|
1723
|
+
reserved_slots.append(slot)
|
|
1724
|
+
|
|
1725
|
+
# the allocation may be fake, make it real
|
|
1726
|
+
if allocation.is_transient:
|
|
1727
|
+
self.session.add(allocation)
|
|
1728
|
+
|
|
1729
|
+
if not including_mirrors or allocation.quota == 1:
|
|
1730
|
+
return
|
|
1731
|
+
|
|
1732
|
+
for mirror in self.allocation_mirrors_by_master(allocation):
|
|
1733
|
+
create_reserved_slots(
|
|
1734
|
+
mirror, start, end,
|
|
1735
|
+
including_mirrors=False
|
|
1736
|
+
)
|
|
1737
|
+
|
|
1738
|
+
# groups are reserved by group-identifier - so all members of a group
|
|
1739
|
+
# or none of them. As such there's no start / end date which is defined
|
|
1740
|
+
# implicitly by the allocation
|
|
1741
|
+
def new_blockers_by_group(
|
|
1742
|
+
group: UUID | None
|
|
1743
|
+
) -> Iterator[ReservationBlocker]:
|
|
1744
|
+
|
|
1745
|
+
if group:
|
|
1746
|
+
blocker = ReservationBlocker()
|
|
1747
|
+
blocker.token = token
|
|
1748
|
+
blocker.target = group
|
|
1749
|
+
blocker.target_type = 'group'
|
|
1750
|
+
blocker.resource = self.resource
|
|
1751
|
+
blocker.reason = reason
|
|
1752
|
+
|
|
1753
|
+
for allocation in self.allocations_by_group(group):
|
|
1754
|
+
create_reserved_slots(
|
|
1755
|
+
allocation,
|
|
1756
|
+
allocation._start,
|
|
1757
|
+
allocation._end
|
|
1758
|
+
)
|
|
1759
|
+
|
|
1760
|
+
yield blocker
|
|
1761
|
+
|
|
1762
|
+
# all other reservations are reserved by start/end date
|
|
1763
|
+
def new_blockers_by_dates(
|
|
1764
|
+
dates: list[tuple[datetime, datetime]]
|
|
1765
|
+
) -> Iterator[ReservationBlocker]:
|
|
1766
|
+
|
|
1767
|
+
already_reserved_groups = set()
|
|
1768
|
+
|
|
1769
|
+
for start, end in dates:
|
|
1770
|
+
for allocation in self.allocations_in_range(start, end):
|
|
1771
|
+
if allocation.group in already_reserved_groups:
|
|
1772
|
+
continue
|
|
1773
|
+
|
|
1774
|
+
if not allocation.overlaps(start, end):
|
|
1775
|
+
continue
|
|
1776
|
+
|
|
1777
|
+
# automatically reserve the whole group if the allocation
|
|
1778
|
+
# is part of a group
|
|
1779
|
+
if allocation.in_group:
|
|
1780
|
+
already_reserved_groups.add(allocation.group)
|
|
1781
|
+
|
|
1782
|
+
yield from new_blockers_by_group(allocation.group)
|
|
1783
|
+
else:
|
|
1784
|
+
blocker = ReservationBlocker()
|
|
1785
|
+
blocker.token = token
|
|
1786
|
+
blocker.start, blocker.end = rasterizer.rasterize_span(
|
|
1787
|
+
start, end, allocation.raster
|
|
1788
|
+
)
|
|
1789
|
+
blocker.timezone = allocation.timezone
|
|
1790
|
+
blocker.target = allocation.group
|
|
1791
|
+
blocker.target_type = 'allocation'
|
|
1792
|
+
blocker.resource = self.resource
|
|
1793
|
+
blocker.reason = reason
|
|
1794
|
+
|
|
1795
|
+
create_reserved_slots(allocation, start, end)
|
|
1796
|
+
|
|
1797
|
+
yield blocker
|
|
1798
|
+
|
|
1799
|
+
# create the blockers and reserved slots
|
|
1800
|
+
if group:
|
|
1801
|
+
blockers = list(new_blockers_by_group(group))
|
|
1802
|
+
else:
|
|
1803
|
+
blockers = list(new_blockers_by_dates(dates))
|
|
1804
|
+
|
|
1805
|
+
if not blockers:
|
|
1806
|
+
raise errors.InvalidReservationError
|
|
1807
|
+
|
|
1808
|
+
if not reserved_slots:
|
|
1809
|
+
raise errors.NotReservableError
|
|
1810
|
+
|
|
1811
|
+
for blocker in blockers:
|
|
1812
|
+
self.session.add(blocker)
|
|
1813
|
+
|
|
1814
|
+
return blockers
|
|
1815
|
+
|
|
1816
|
+
def remove_blocker(
|
|
1817
|
+
self,
|
|
1818
|
+
token: UUID,
|
|
1819
|
+
id: int | None = None
|
|
1820
|
+
) -> None:
|
|
1821
|
+
""" Removes all reserved slots of the given reservation blocker token.
|
|
1822
|
+
|
|
1823
|
+
The id is optional. If given, only the blocker with the given
|
|
1824
|
+
token AND id is removed.
|
|
1825
|
+
|
|
1826
|
+
"""
|
|
1827
|
+
|
|
1828
|
+
for slot in self.reserved_slots_by_blocker(token, id):
|
|
1829
|
+
self.session.delete(slot)
|
|
1830
|
+
|
|
1831
|
+
for blocker in self.blockers_by_token(token, id):
|
|
1832
|
+
self.session.delete(blocker)
|
|
1833
|
+
|
|
1834
|
+
# some allocations still reference reserved_slots if not for this
|
|
1835
|
+
self.session.expire_all()
|
|
1836
|
+
|
|
1837
|
+
def change_blocker_reason(
|
|
1838
|
+
self,
|
|
1839
|
+
token: UUID,
|
|
1840
|
+
new_reason: str | None,
|
|
1841
|
+
) -> None:
|
|
1842
|
+
|
|
1843
|
+
for blocker in self.blockers_by_token(token):
|
|
1844
|
+
blocker.reason = new_reason
|
|
1845
|
+
|
|
1846
|
+
def change_blocker(
|
|
1847
|
+
self,
|
|
1848
|
+
token: UUID,
|
|
1849
|
+
id: int,
|
|
1850
|
+
new_start: datetime,
|
|
1851
|
+
new_end: datetime,
|
|
1852
|
+
) -> ReservationBlocker | None:
|
|
1853
|
+
""" Allows to change the timespan of a blocker under certain
|
|
1854
|
+
conditions:
|
|
1855
|
+
|
|
1856
|
+
- The new timespan must be reservable inside the existing allocation.
|
|
1857
|
+
(So you cannot use this method to block another allocation)
|
|
1858
|
+
- The referenced allocation must not be in a group.
|
|
1859
|
+
|
|
1860
|
+
Returns the new blocker if a change was made and None instead.
|
|
1861
|
+
|
|
1862
|
+
"""
|
|
1863
|
+
|
|
1864
|
+
assert new_start and new_end
|
|
1865
|
+
|
|
1866
|
+
new_start, new_end = self._prepare_range(new_start, new_end)
|
|
1867
|
+
existing_blocker = self.blockers_by_token(token, id).one()
|
|
1868
|
+
|
|
1869
|
+
# if there's nothing to change, do not change
|
|
1870
|
+
if (
|
|
1871
|
+
existing_blocker.start == new_start
|
|
1872
|
+
and existing_blocker.end in (
|
|
1873
|
+
new_end,
|
|
1874
|
+
new_end - timedelta(microseconds=1)
|
|
1875
|
+
)
|
|
1876
|
+
):
|
|
1877
|
+
return None
|
|
1878
|
+
|
|
1879
|
+
# will raise a MultipleResultsFound exception if this is a group
|
|
1880
|
+
allocation = existing_blocker.target_allocations().one()
|
|
1881
|
+
|
|
1882
|
+
if not allocation.contains(new_start, new_end):
|
|
1883
|
+
raise errors.TimerangeTooLong()
|
|
1884
|
+
|
|
1885
|
+
old_reason = existing_blocker.reason
|
|
1886
|
+
|
|
1887
|
+
with self.begin_nested():
|
|
1888
|
+
self.remove_blocker(token, id)
|
|
1889
|
+
|
|
1890
|
+
new_blocker, = self.add_blocker(
|
|
1891
|
+
dates=(new_start, new_end),
|
|
1892
|
+
reason=old_reason,
|
|
1893
|
+
token=token
|
|
1894
|
+
)
|
|
1895
|
+
new_blocker.id = id
|
|
1896
|
+
|
|
1897
|
+
return new_blocker
|
|
1898
|
+
|
|
1559
1899
|
def search_allocations(
|
|
1560
1900
|
self,
|
|
1561
1901
|
start: datetime,
|
|
@@ -1629,8 +1969,6 @@ class Scheduler(ContextServicesMixin):
|
|
|
1629
1969
|
assert start
|
|
1630
1970
|
assert end
|
|
1631
1971
|
|
|
1632
|
-
start, end = self._prepare_range(start, end)
|
|
1633
|
-
|
|
1634
1972
|
assert whole_day in ('yes', 'no', 'any')
|
|
1635
1973
|
assert groups in ('yes', 'no', 'any')
|
|
1636
1974
|
|
|
@@ -1645,7 +1983,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1645
1983
|
else:
|
|
1646
1984
|
day_numbers = None
|
|
1647
1985
|
|
|
1648
|
-
query = self.allocations_in_range(start, end)
|
|
1986
|
+
query = self.allocations_in_range(*self._prepare_range(start, end))
|
|
1649
1987
|
query = query.order_by(Allocation._start)
|
|
1650
1988
|
|
|
1651
1989
|
allocations = []
|
|
@@ -1653,17 +1991,23 @@ class Scheduler(ContextServicesMixin):
|
|
|
1653
1991
|
known_groups = set()
|
|
1654
1992
|
known_ids = set()
|
|
1655
1993
|
|
|
1656
|
-
for allocation in query
|
|
1994
|
+
for allocation in query:
|
|
1657
1995
|
|
|
1658
1996
|
if not self.is_allocation_exposed(allocation):
|
|
1659
1997
|
continue
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
s = sedate.
|
|
1666
|
-
|
|
1998
|
+
allocation_start = allocation.display_start()
|
|
1999
|
+
# NOTE: We want the correct timezone, but we don't want the
|
|
2000
|
+
# date to be on the next day for a full-day reservation
|
|
2001
|
+
# so we skip the microsecond addition
|
|
2002
|
+
allocation_end = sedate.to_timezone(allocation.end, self.timezone)
|
|
2003
|
+
s = sedate.standardize_date(datetime.combine(
|
|
2004
|
+
allocation_start.date(),
|
|
2005
|
+
start.time()
|
|
2006
|
+
), self.timezone)
|
|
2007
|
+
e = sedate.standardize_date(datetime.combine(
|
|
2008
|
+
allocation_end.date(),
|
|
2009
|
+
end.time()
|
|
2010
|
+
), self.timezone)
|
|
1667
2011
|
|
|
1668
2012
|
if not allocation.overlaps(s, e):
|
|
1669
2013
|
continue
|
|
@@ -1784,6 +2128,23 @@ class Scheduler(ContextServicesMixin):
|
|
|
1784
2128
|
|
|
1785
2129
|
return targets
|
|
1786
2130
|
|
|
2131
|
+
def reserved_slots_by_type(
|
|
2132
|
+
self,
|
|
2133
|
+
token: UUID,
|
|
2134
|
+
type: Literal['reservation', 'blocker'] | None = None,
|
|
2135
|
+
) -> Query[ReservedSlot]:
|
|
2136
|
+
""" Returns all reserved slots for the given token.
|
|
2137
|
+
The type is also optional and can be used to restrict the type of
|
|
2138
|
+
reserved slot that's returned
|
|
2139
|
+
"""
|
|
2140
|
+
|
|
2141
|
+
assert token
|
|
2142
|
+
|
|
2143
|
+
query = self.managed_reserved_slots()
|
|
2144
|
+
if type is not None:
|
|
2145
|
+
query = query.filter(ReservedSlot.source_type == type)
|
|
2146
|
+
return query.filter(ReservedSlot.reservation_token == token)
|
|
2147
|
+
|
|
1787
2148
|
def reserved_slots_by_reservation(
|
|
1788
2149
|
self,
|
|
1789
2150
|
token: UUID,
|
|
@@ -1794,10 +2155,7 @@ class Scheduler(ContextServicesMixin):
|
|
|
1794
2155
|
specific reservation matching token and id.
|
|
1795
2156
|
"""
|
|
1796
2157
|
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
query = self.managed_reserved_slots()
|
|
1800
|
-
query = query.filter(ReservedSlot.reservation_token == token)
|
|
2158
|
+
query = self.reserved_slots_by_type(token, 'reservation')
|
|
1801
2159
|
|
|
1802
2160
|
if id is None:
|
|
1803
2161
|
return query
|
|
@@ -1806,6 +2164,25 @@ class Scheduler(ContextServicesMixin):
|
|
|
1806
2164
|
ids = allocations.with_entities(Allocation.id)
|
|
1807
2165
|
return query.filter(ReservedSlot.allocation_id.in_(ids))
|
|
1808
2166
|
|
|
2167
|
+
def reserved_slots_by_blocker(
|
|
2168
|
+
self,
|
|
2169
|
+
token: UUID,
|
|
2170
|
+
id: int | None = None
|
|
2171
|
+
) -> Query[ReservedSlot]:
|
|
2172
|
+
""" Returns all reserved slots of the given blocker.
|
|
2173
|
+
The id is optional and may be used only return the slots from a
|
|
2174
|
+
specific reservation matching token and id.
|
|
2175
|
+
"""
|
|
2176
|
+
|
|
2177
|
+
query = self.reserved_slots_by_type(token, 'blocker')
|
|
2178
|
+
|
|
2179
|
+
if id is None:
|
|
2180
|
+
return query
|
|
2181
|
+
else:
|
|
2182
|
+
allocations = self.allocations_by_blocker(token, id)
|
|
2183
|
+
ids = allocations.with_entities(Allocation.id)
|
|
2184
|
+
return query.filter(ReservedSlot.allocation_id.in_(ids))
|
|
2185
|
+
|
|
1809
2186
|
def reservations_by_group(self, group: UUID) -> Query[Reservation]:
|
|
1810
2187
|
tokens = self.managed_reservations().with_entities(Reservation.token)
|
|
1811
2188
|
tokens = tokens.filter(Reservation.target == group)
|
|
@@ -1840,3 +2217,22 @@ class Scheduler(ContextServicesMixin):
|
|
|
1840
2217
|
raise errors.InvalidReservationToken from None
|
|
1841
2218
|
|
|
1842
2219
|
return query
|
|
2220
|
+
|
|
2221
|
+
def blockers_by_token(
|
|
2222
|
+
self,
|
|
2223
|
+
token: UUID,
|
|
2224
|
+
id: int | None = None
|
|
2225
|
+
) -> Query[ReservationBlocker]:
|
|
2226
|
+
|
|
2227
|
+
query = self.managed_blockers()
|
|
2228
|
+
query = query.filter(ReservationBlocker.token == token)
|
|
2229
|
+
|
|
2230
|
+
if id:
|
|
2231
|
+
query = query.filter(ReservationBlocker.id == id)
|
|
2232
|
+
|
|
2233
|
+
try:
|
|
2234
|
+
query.first()
|
|
2235
|
+
except exc.NoResultFound:
|
|
2236
|
+
raise errors.InvalidReservationToken from None
|
|
2237
|
+
|
|
2238
|
+
return query
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: libres
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: A library to reserve things
|
|
5
5
|
Home-page: http://github.com/seantis/libres/
|
|
6
6
|
Author: Denis Krienbühl
|
|
@@ -142,6 +142,40 @@ After this, create a new release on Github.
|
|
|
142
142
|
Changelog
|
|
143
143
|
---------
|
|
144
144
|
|
|
145
|
+
0.10.0 (15.01.2026)
|
|
146
|
+
~~~~~~~~~~~~~~~~~~~
|
|
147
|
+
|
|
148
|
+
- Adds new entity `ReservationBlocker` for administrative blockers
|
|
149
|
+
of targeted allocations for the targeted timespans, this also ends
|
|
150
|
+
up adding a new column `source_type` to `ReservedSlot` which can be
|
|
151
|
+
added using the following recipe using an alembic `Operations` object::
|
|
152
|
+
|
|
153
|
+
operations.add_column(
|
|
154
|
+
'reserved_slots',
|
|
155
|
+
Column(
|
|
156
|
+
'source_type',
|
|
157
|
+
Enum(
|
|
158
|
+
'reservation', 'blocker',
|
|
159
|
+
name='reserved_slot_source_type'
|
|
160
|
+
),
|
|
161
|
+
nullable=False,
|
|
162
|
+
server_default='reservation'
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
operations.alter_column(
|
|
166
|
+
'reserved_slots',
|
|
167
|
+
'source_type',
|
|
168
|
+
server_default=None
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
0.9.1 (05.08.2025)
|
|
173
|
+
~~~~~~~~~~~~~~~~~~~
|
|
174
|
+
|
|
175
|
+
- Fixes bug in `Scheduler.search_allocations` when the searched
|
|
176
|
+
time range contains multiple DST <-> ST transitions.
|
|
177
|
+
[Daverball]
|
|
178
|
+
|
|
145
179
|
0.9.0 (23.05.2025)
|
|
146
180
|
~~~~~~~~~~~~~~~~~~~
|
|
147
181
|
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
libres/.gitignore,sha256=tyWYoDW7-zMdsfiJIVONRWJ5JmqJCTHzBOsi_rkIYZg,49
|
|
2
|
-
libres/__init__.py,sha256=
|
|
2
|
+
libres/__init__.py,sha256=9v-iLfER2w_g4fiTMUPafUATTCY4gPd72_57PqtTeog,260
|
|
3
3
|
libres/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
libres/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
libres/context/core.py,sha256=AZMmFVqTVSkK3pomhoamLWOeqynY23g_uf32yjhgrRQ,7353
|
|
6
6
|
libres/context/exposure.py,sha256=0krsky3XYplG7Uf5oLlQgdXp6XuEDjN2JGe3ljgLM14,287
|
|
7
|
-
libres/context/registry.py,sha256=
|
|
7
|
+
libres/context/registry.py,sha256=JfV-QccpDbAEq154aHu7_-hznr5kXYY58czZ-0-m8vA,4986
|
|
8
8
|
libres/context/session.py,sha256=bVvZ7cvSKffGrzxpDM7vKrGgwAPWRoS4No6HK1--xlg,2488
|
|
9
9
|
libres/context/settings.py,sha256=IkdG67QUUuk_sQMXiwsJgelrsbx6M-7f72uJBA2JoJA,1374
|
|
10
10
|
libres/db/__init__.py,sha256=Fks6T8aEacYVvXB9U1v182zKX7SuEUAkC5hTK50AuQk,145
|
|
11
|
-
libres/db/queries.py,sha256=
|
|
12
|
-
libres/db/scheduler.py,sha256=
|
|
13
|
-
libres/db/models/__init__.py,sha256=
|
|
14
|
-
libres/db/models/allocation.py,sha256=
|
|
11
|
+
libres/db/queries.py,sha256=EqsrKL5x5OlTWefvcESs5zg7ZyneXavTC-sGARX4OxM,11043
|
|
12
|
+
libres/db/scheduler.py,sha256=mPuA-48VWQvzHQVy0VsXz4Be_f1YDVlElXWjwGaI6hA,77373
|
|
13
|
+
libres/db/models/__init__.py,sha256=ESy82b2y6YX_D143QP2cns1MRsXxO32fQUvrc7DE6VY,408
|
|
14
|
+
libres/db/models/allocation.py,sha256=4n6zjHWUSzxICjkap1s0pQgDOMki1vkO4l1MI65Ynk8,30699
|
|
15
15
|
libres/db/models/base.py,sha256=Y2Lc60vJ1Du73D0sLozVNi1TfhtrQ3i_64NV7hsnc4g,117
|
|
16
|
-
libres/db/models/
|
|
17
|
-
libres/db/models/
|
|
18
|
-
libres/db/models/
|
|
16
|
+
libres/db/models/blocker.py,sha256=P30FLf-ILqT03ZHnbFBVykY37xFAuCeOglhIH6B_1-c,4488
|
|
17
|
+
libres/db/models/other.py,sha256=XRdyLo1gFY-Sg4iBSPBwOxeqZSpL5PerZISexcAQyDg,853
|
|
18
|
+
libres/db/models/reservation.py,sha256=NnsH1PL_Tu2zt-fHMl_KURfj0RsvJzCPj-SmE-GR86Q,5819
|
|
19
|
+
libres/db/models/reserved_slot.py,sha256=nVggmkPBB4oR1f5dEH-bsLggX5hAUNqfH7CApcRoApw,3263
|
|
20
|
+
libres/db/models/timespan.py,sha256=uo6Xjie5rzm8JklsdCp0CyjeUPYOSHO9tOZI3b1oShs,220
|
|
19
21
|
libres/db/models/timestamp.py,sha256=EsuxdQSUsPCkHb1F7HdrDevWZ5wDWerDFRIhqfhefNw,1301
|
|
20
22
|
libres/db/models/types/__init__.py,sha256=8fx7ecDasCKsCJxGjt4qP5BbDJCG88dJRa0nzD6IetY,186
|
|
21
23
|
libres/db/models/types/json_type.py,sha256=ko6K4YRWERWCpg0wYzuwJ_u-VLJ4K3j3ksJ7cGyiMzw,1137
|
|
@@ -26,8 +28,8 @@ libres/modules/errors.py,sha256=afcvKDfu7XYnGt6CfTG1gpmCaoMTPgtxwmTe2Cyz9qc,2146
|
|
|
26
28
|
libres/modules/events.py,sha256=-YHwFJKory8RSIRTths5LBn92sodEPkhARH_a1Xml3c,5299
|
|
27
29
|
libres/modules/rasterizer.py,sha256=-KYG03YkaPNnyohl_HrEJZqdHrhmGesbLgxxPahNTCc,3155
|
|
28
30
|
libres/modules/utils.py,sha256=W5Kuu9LhhUWZMKLAhw7DmQEuNYb9aXWpW_HXCn62lhU,1871
|
|
29
|
-
libres-0.
|
|
30
|
-
libres-0.
|
|
31
|
-
libres-0.
|
|
32
|
-
libres-0.
|
|
33
|
-
libres-0.
|
|
31
|
+
libres-0.10.0.dist-info/licenses/LICENSE,sha256=w1rojULT3naueSnr4r62MSQipL4VPtsfEcTFmSKpVuI,1069
|
|
32
|
+
libres-0.10.0.dist-info/METADATA,sha256=f3P6JcW2iBhZb666Qj1BDmttM6Z1YQiILrOGTsabres,10924
|
|
33
|
+
libres-0.10.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
34
|
+
libres-0.10.0.dist-info/top_level.txt,sha256=Exs6AnhZc0UTcsD5Ylyx1P89j19hJ4Dy13jxQyZwi3k,7
|
|
35
|
+
libres-0.10.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|