libres 0.9.1__tar.gz → 0.10.1__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.
Files changed (65) hide show
  1. {libres-0.9.1 → libres-0.10.1}/HISTORY.rst +49 -0
  2. {libres-0.9.1/src/libres.egg-info → libres-0.10.1}/PKG-INFO +50 -1
  3. {libres-0.9.1 → libres-0.10.1}/pyproject.toml +1 -2
  4. {libres-0.9.1 → libres-0.10.1}/src/libres/__init__.py +2 -2
  5. {libres-0.9.1 → libres-0.10.1}/src/libres/db/models/__init__.py +3 -1
  6. {libres-0.9.1 → libres-0.10.1}/src/libres/db/models/allocation.py +71 -21
  7. libres-0.10.1/src/libres/db/models/blocker.py +164 -0
  8. {libres-0.9.1 → libres-0.10.1}/src/libres/db/models/other.py +2 -1
  9. {libres-0.9.1 → libres-0.10.1}/src/libres/db/models/reservation.py +5 -9
  10. {libres-0.9.1 → libres-0.10.1}/src/libres/db/models/reserved_slot.py +15 -11
  11. libres-0.10.1/src/libres/db/models/timespan.py +12 -0
  12. {libres-0.9.1 → libres-0.10.1}/src/libres/db/models/types/json_type.py +1 -0
  13. {libres-0.9.1 → libres-0.10.1}/src/libres/db/queries.py +2 -1
  14. {libres-0.9.1 → libres-0.10.1}/src/libres/db/scheduler.py +405 -13
  15. {libres-0.9.1 → libres-0.10.1/src/libres.egg-info}/PKG-INFO +50 -1
  16. {libres-0.9.1 → libres-0.10.1}/src/libres.egg-info/SOURCES.txt +3 -0
  17. {libres-0.9.1 → libres-0.10.1}/tests/test_allocation.py +61 -0
  18. {libres-0.9.1 → libres-0.10.1}/tests/test_reserved_slot.py +3 -0
  19. {libres-0.9.1 → libres-0.10.1}/tests/test_scheduler.py +21 -6
  20. libres-0.10.1/tests/test_scheduler_blockers.py +496 -0
  21. {libres-0.9.1 → libres-0.10.1}/LICENSE +0 -0
  22. {libres-0.9.1 → libres-0.10.1}/MANIFEST.in +0 -0
  23. {libres-0.9.1 → libres-0.10.1}/README.rst +0 -0
  24. {libres-0.9.1 → libres-0.10.1}/docs/Makefile +0 -0
  25. {libres-0.9.1 → libres-0.10.1}/docs/_static/custom.css +0 -0
  26. {libres-0.9.1 → libres-0.10.1}/docs/_static/favicon.ico +0 -0
  27. {libres-0.9.1 → libres-0.10.1}/docs/_static/logo.svg +0 -0
  28. {libres-0.9.1 → libres-0.10.1}/docs/api.rst +0 -0
  29. {libres-0.9.1 → libres-0.10.1}/docs/concepts.rst +0 -0
  30. {libres-0.9.1 → libres-0.10.1}/docs/conf.py +0 -0
  31. {libres-0.9.1 → libres-0.10.1}/docs/customizations.rst +0 -0
  32. {libres-0.9.1 → libres-0.10.1}/docs/faq.rst +0 -0
  33. {libres-0.9.1 → libres-0.10.1}/docs/index.rst +0 -0
  34. {libres-0.9.1 → libres-0.10.1}/docs/requirements.txt +0 -0
  35. {libres-0.9.1 → libres-0.10.1}/docs/under_the_hood.rst +0 -0
  36. {libres-0.9.1 → libres-0.10.1}/setup.cfg +0 -0
  37. {libres-0.9.1 → libres-0.10.1}/src/libres/.gitignore +0 -0
  38. {libres-0.9.1 → libres-0.10.1}/src/libres/context/__init__.py +0 -0
  39. {libres-0.9.1 → libres-0.10.1}/src/libres/context/core.py +0 -0
  40. {libres-0.9.1 → libres-0.10.1}/src/libres/context/exposure.py +0 -0
  41. {libres-0.9.1 → libres-0.10.1}/src/libres/context/registry.py +0 -0
  42. {libres-0.9.1 → libres-0.10.1}/src/libres/context/session.py +0 -0
  43. {libres-0.9.1 → libres-0.10.1}/src/libres/context/settings.py +0 -0
  44. {libres-0.9.1 → libres-0.10.1}/src/libres/db/__init__.py +0 -0
  45. {libres-0.9.1 → libres-0.10.1}/src/libres/db/models/base.py +0 -0
  46. {libres-0.9.1 → libres-0.10.1}/src/libres/db/models/timestamp.py +0 -0
  47. {libres-0.9.1 → libres-0.10.1}/src/libres/db/models/types/__init__.py +0 -0
  48. {libres-0.9.1 → libres-0.10.1}/src/libres/db/models/types/utcdatetime.py +0 -0
  49. {libres-0.9.1 → libres-0.10.1}/src/libres/db/models/types/uuid_type.py +0 -0
  50. {libres-0.9.1 → libres-0.10.1}/src/libres/modules/__init__.py +0 -0
  51. {libres-0.9.1 → libres-0.10.1}/src/libres/modules/errors.py +0 -0
  52. {libres-0.9.1 → libres-0.10.1}/src/libres/modules/events.py +0 -0
  53. {libres-0.9.1 → libres-0.10.1}/src/libres/modules/rasterizer.py +0 -0
  54. {libres-0.9.1 → libres-0.10.1}/src/libres/modules/utils.py +0 -0
  55. {libres-0.9.1 → libres-0.10.1}/src/libres/py.typed +0 -0
  56. {libres-0.9.1 → libres-0.10.1}/src/libres.egg-info/dependency_links.txt +0 -0
  57. {libres-0.9.1 → libres-0.10.1}/src/libres.egg-info/not-zip-safe +0 -0
  58. {libres-0.9.1 → libres-0.10.1}/src/libres.egg-info/requires.txt +0 -0
  59. {libres-0.9.1 → libres-0.10.1}/src/libres.egg-info/top_level.txt +0 -0
  60. {libres-0.9.1 → libres-0.10.1}/tests/test_registry.py +0 -0
  61. {libres-0.9.1 → libres-0.10.1}/tests/test_reservation.py +0 -0
  62. {libres-0.9.1 → libres-0.10.1}/tests/test_session.py +0 -0
  63. {libres-0.9.1 → libres-0.10.1}/tests/test_test.py +0 -0
  64. {libres-0.9.1 → libres-0.10.1}/tests/test_types.py +0 -0
  65. {libres-0.9.1 → libres-0.10.1}/tests/test_utils.py +0 -0
@@ -1,6 +1,55 @@
1
1
  Changelog
2
2
  ---------
3
3
 
4
+ 0.10.1 (21.01.2026)
5
+ ~~~~~~~~~~~~~~~~~~~
6
+
7
+ - Adds proper support for SQLAlchemy 1.4. As a result of this
8
+ `Allocation.type` and `Reservation.type` are no longer nullable
9
+ and have a default value of 'generic', you may use the following
10
+ recipe using an alembic `Operations` object to migrate existing
11
+ databases::
12
+
13
+ context.operations.execute("""
14
+ UPDATE allocations
15
+ SET type = 'generic'
16
+ WHERE type IS NULL;
17
+ """)
18
+ context.operations.alter_column('allocations', 'type', nullable=False)
19
+ context.operations.execute("""
20
+ UPDATE reservations
21
+ SET type = 'generic'
22
+ WHERE type IS NULL;
23
+ """)
24
+ context.operations.alter_column('reservations', 'type', nullable=False)
25
+
26
+ 0.10.0 (15.01.2026)
27
+ ~~~~~~~~~~~~~~~~~~~
28
+
29
+ - Adds new entity `ReservationBlocker` for administrative blockers
30
+ of targeted allocations for the targeted timespans, this also ends
31
+ up adding a new column `source_type` to `ReservedSlot` which can be
32
+ added using the following recipe using an alembic `Operations` object::
33
+
34
+ operations.add_column(
35
+ 'reserved_slots',
36
+ Column(
37
+ 'source_type',
38
+ Enum(
39
+ 'reservation', 'blocker',
40
+ name='reserved_slot_source_type'
41
+ ),
42
+ nullable=False,
43
+ server_default='reservation'
44
+ )
45
+ )
46
+ operations.alter_column(
47
+ 'reserved_slots',
48
+ 'source_type',
49
+ server_default=None
50
+ )
51
+
52
+
4
53
  0.9.1 (05.08.2025)
5
54
  ~~~~~~~~~~~~~~~~~~~
6
55
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: libres
3
- Version: 0.9.1
3
+ Version: 0.10.1
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,55 @@ After this, create a new release on Github.
142
142
  Changelog
143
143
  ---------
144
144
 
145
+ 0.10.1 (21.01.2026)
146
+ ~~~~~~~~~~~~~~~~~~~
147
+
148
+ - Adds proper support for SQLAlchemy 1.4. As a result of this
149
+ `Allocation.type` and `Reservation.type` are no longer nullable
150
+ and have a default value of 'generic', you may use the following
151
+ recipe using an alembic `Operations` object to migrate existing
152
+ databases::
153
+
154
+ context.operations.execute("""
155
+ UPDATE allocations
156
+ SET type = 'generic'
157
+ WHERE type IS NULL;
158
+ """)
159
+ context.operations.alter_column('allocations', 'type', nullable=False)
160
+ context.operations.execute("""
161
+ UPDATE reservations
162
+ SET type = 'generic'
163
+ WHERE type IS NULL;
164
+ """)
165
+ context.operations.alter_column('reservations', 'type', nullable=False)
166
+
167
+ 0.10.0 (15.01.2026)
168
+ ~~~~~~~~~~~~~~~~~~~
169
+
170
+ - Adds new entity `ReservationBlocker` for administrative blockers
171
+ of targeted allocations for the targeted timespans, this also ends
172
+ up adding a new column `source_type` to `ReservedSlot` which can be
173
+ added using the following recipe using an alembic `Operations` object::
174
+
175
+ operations.add_column(
176
+ 'reserved_slots',
177
+ Column(
178
+ 'source_type',
179
+ Enum(
180
+ 'reservation', 'blocker',
181
+ name='reserved_slot_source_type'
182
+ ),
183
+ nullable=False,
184
+ server_default='reservation'
185
+ )
186
+ )
187
+ operations.alter_column(
188
+ 'reserved_slots',
189
+ 'source_type',
190
+ server_default=None
191
+ )
192
+
193
+
145
194
  0.9.1 (05.08.2025)
146
195
  ~~~~~~~~~~~~~~~~~~~
147
196
 
@@ -11,7 +11,7 @@ branch = true
11
11
  source = ["src"]
12
12
 
13
13
  [tool.bumpversion]
14
- current_version = "0.9.1"
14
+ current_version = "0.10.1"
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.9.1'
8
+ __version__ = '0.10.1'
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
@@ -83,7 +84,7 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
83
84
  resource: Column[uuid.UUID] = Column(UUID(), nullable=False)
84
85
 
85
86
  #: the polymorphic type of the allocation
86
- type: Column[str | None] = Column(types.Text(), nullable=True)
87
+ type: Column[str] = Column(types.Text(), nullable=False, default='generic')
87
88
 
88
89
  #: resource of which this allocation is a mirror. If the mirror_of
89
90
  #: attribute equals the resource, this is a real resource
@@ -136,9 +137,15 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
136
137
  nullable=False
137
138
  )
138
139
 
139
- if TYPE_CHECKING:
140
- # forward declare backref
141
- reserved_slots: list[ReservedSlot]
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'),
@@ -146,7 +153,7 @@ class Allocation(TimestampMixin, ORMBase, OtherModels):
146
153
  )
147
154
 
148
155
  __mapper_args__ = {
149
- 'polymorphic_identity': None,
156
+ 'polymorphic_identity': 'generic',
150
157
  'polymorphic_on': type
151
158
  }
152
159
 
@@ -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
- used = len(self.reserved_slots)
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 == used:
523
+ if total == blocked:
524
+ # if everything is blocked this allocation is unavailable
508
525
  return 0.0
509
526
 
510
- if used == 0:
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 - (float(used) / float(total) * 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
- # reduce the set of reserved slots by the hour we skipped
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
- used = sum(
551
- 1 for r in self.reserved_slots
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
- used = len(self.reserved_slots)
556
- # add one hour's worth of reserved slots
557
- used += 60 // self.raster
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 used == 0:
560
- return 100.0
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 == used:
609
+ if total == reserved:
563
610
  return 0.0
564
611
 
565
- return 100.0 - (float(used) / float(total) * 100.0)
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 accesible?
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
 
@@ -65,7 +60,8 @@ class Reservation(TimestampMixin, ORMBase, OtherModels):
65
60
 
66
61
  type: Column[str | None] = Column(
67
62
  types.Text(),
68
- nullable=True
63
+ nullable=False,
64
+ default='generic'
69
65
  )
70
66
 
71
67
  resource: Column[uuid.UUID] = Column(
@@ -122,7 +118,7 @@ class Reservation(TimestampMixin, ORMBase, OtherModels):
122
118
  )
123
119
 
124
120
  __mapper_args__ = {
125
- 'polymorphic_identity': None,
121
+ 'polymorphic_identity': 'generic',
126
122
  'polymorphic_on': type
127
123
  }
128
124
 
@@ -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 ORMBase, Allocation
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(Allocation.id),
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
- # Reserved_slots are eagerly joined since we usually want both
64
- # allocation and reserved_slots. There's barely a function which does
65
- # not need to know about reserved slots when working with allocation.
66
- backref=backref(
67
- 'reserved_slots',
68
- lazy='joined',
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(
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ from typing import NamedTuple
5
+ from typing import TYPE_CHECKING
6
+ if TYPE_CHECKING:
7
+ from datetime import datetime
8
+
9
+
10
+ class Timespan(NamedTuple):
11
+ start: datetime
12
+ end: datetime
@@ -25,6 +25,7 @@ class JSON(_Base):
25
25
  """
26
26
 
27
27
  impl = JSONB
28
+ cache_ok = True
28
29
 
29
30
  def process_bind_param( # type:ignore[override]
30
31
  self,
@@ -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')