libres 0.9.1__tar.gz → 0.10.0__tar.gz
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-0.9.1 → libres-0.10.0}/HISTORY.rst +27 -0
- {libres-0.9.1/src/libres.egg-info → libres-0.10.0}/PKG-INFO +28 -1
- {libres-0.9.1 → libres-0.10.0}/pyproject.toml +1 -2
- {libres-0.9.1 → libres-0.10.0}/src/libres/__init__.py +2 -2
- {libres-0.9.1 → libres-0.10.0}/src/libres/db/models/__init__.py +3 -1
- {libres-0.9.1 → libres-0.10.0}/src/libres/db/models/allocation.py +69 -19
- libres-0.10.0/src/libres/db/models/blocker.py +164 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres/db/models/other.py +2 -1
- {libres-0.9.1 → libres-0.10.0}/src/libres/db/models/reservation.py +2 -7
- {libres-0.9.1 → libres-0.10.0}/src/libres/db/models/reserved_slot.py +15 -11
- libres-0.10.0/src/libres/db/models/timespan.py +12 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres/db/queries.py +2 -1
- {libres-0.9.1 → libres-0.10.0}/src/libres/db/scheduler.py +405 -13
- {libres-0.9.1 → libres-0.10.0/src/libres.egg-info}/PKG-INFO +28 -1
- {libres-0.9.1 → libres-0.10.0}/src/libres.egg-info/SOURCES.txt +3 -0
- {libres-0.9.1 → libres-0.10.0}/tests/test_allocation.py +61 -0
- {libres-0.9.1 → libres-0.10.0}/tests/test_reserved_slot.py +3 -0
- {libres-0.9.1 → libres-0.10.0}/tests/test_scheduler.py +21 -6
- libres-0.10.0/tests/test_scheduler_blockers.py +496 -0
- {libres-0.9.1 → libres-0.10.0}/LICENSE +0 -0
- {libres-0.9.1 → libres-0.10.0}/MANIFEST.in +0 -0
- {libres-0.9.1 → libres-0.10.0}/README.rst +0 -0
- {libres-0.9.1 → libres-0.10.0}/docs/Makefile +0 -0
- {libres-0.9.1 → libres-0.10.0}/docs/_static/custom.css +0 -0
- {libres-0.9.1 → libres-0.10.0}/docs/_static/favicon.ico +0 -0
- {libres-0.9.1 → libres-0.10.0}/docs/_static/logo.svg +0 -0
- {libres-0.9.1 → libres-0.10.0}/docs/api.rst +0 -0
- {libres-0.9.1 → libres-0.10.0}/docs/concepts.rst +0 -0
- {libres-0.9.1 → libres-0.10.0}/docs/conf.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/docs/customizations.rst +0 -0
- {libres-0.9.1 → libres-0.10.0}/docs/faq.rst +0 -0
- {libres-0.9.1 → libres-0.10.0}/docs/index.rst +0 -0
- {libres-0.9.1 → libres-0.10.0}/docs/requirements.txt +0 -0
- {libres-0.9.1 → libres-0.10.0}/docs/under_the_hood.rst +0 -0
- {libres-0.9.1 → libres-0.10.0}/setup.cfg +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres/.gitignore +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres/context/__init__.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres/context/core.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres/context/exposure.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres/context/registry.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres/context/session.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres/context/settings.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres/db/__init__.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres/db/models/base.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres/db/models/timestamp.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres/db/models/types/__init__.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres/db/models/types/json_type.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres/db/models/types/utcdatetime.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres/db/models/types/uuid_type.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres/modules/__init__.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres/modules/errors.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres/modules/events.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres/modules/rasterizer.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres/modules/utils.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres/py.typed +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres.egg-info/dependency_links.txt +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres.egg-info/not-zip-safe +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres.egg-info/requires.txt +0 -0
- {libres-0.9.1 → libres-0.10.0}/src/libres.egg-info/top_level.txt +0 -0
- {libres-0.9.1 → libres-0.10.0}/tests/test_registry.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/tests/test_reservation.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/tests/test_session.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/tests/test_test.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/tests/test_types.py +0 -0
- {libres-0.9.1 → libres-0.10.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
Changelog
|
|
2
2
|
---------
|
|
3
3
|
|
|
4
|
+
0.10.0 (15.01.2026)
|
|
5
|
+
~~~~~~~~~~~~~~~~~~~
|
|
6
|
+
|
|
7
|
+
- Adds new entity `ReservationBlocker` for administrative blockers
|
|
8
|
+
of targeted allocations for the targeted timespans, this also ends
|
|
9
|
+
up adding a new column `source_type` to `ReservedSlot` which can be
|
|
10
|
+
added using the following recipe using an alembic `Operations` object::
|
|
11
|
+
|
|
12
|
+
operations.add_column(
|
|
13
|
+
'reserved_slots',
|
|
14
|
+
Column(
|
|
15
|
+
'source_type',
|
|
16
|
+
Enum(
|
|
17
|
+
'reservation', 'blocker',
|
|
18
|
+
name='reserved_slot_source_type'
|
|
19
|
+
),
|
|
20
|
+
nullable=False,
|
|
21
|
+
server_default='reservation'
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
operations.alter_column(
|
|
25
|
+
'reserved_slots',
|
|
26
|
+
'source_type',
|
|
27
|
+
server_default=None
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
4
31
|
0.9.1 (05.08.2025)
|
|
5
32
|
~~~~~~~~~~~~~~~~~~~
|
|
6
33
|
|
|
@@ -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,33 @@ 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
|
+
|
|
145
172
|
0.9.1 (05.08.2025)
|
|
146
173
|
~~~~~~~~~~~~~~~~~~~
|
|
147
174
|
|
|
@@ -11,7 +11,7 @@ branch = true
|
|
|
11
11
|
source = ["src"]
|
|
12
12
|
|
|
13
13
|
[tool.bumpversion]
|
|
14
|
-
current_version = "0.
|
|
14
|
+
current_version = "0.10.0"
|
|
15
15
|
commit = true
|
|
16
16
|
message = "Release {new_version}"
|
|
17
17
|
tag = true
|
|
@@ -157,7 +157,6 @@ ignore = [
|
|
|
157
157
|
"UP009",
|
|
158
158
|
"UP012",
|
|
159
159
|
"UP032",
|
|
160
|
-
"UP038",
|
|
161
160
|
]
|
|
162
161
|
unfixable = []
|
|
163
162
|
external = ["TC"]
|
|
@@ -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'
|
|
@@ -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
|
)
|
|
@@ -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
|
|
@@ -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
|
|
|
@@ -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(
|
|
@@ -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')
|