codex_services.booking.slot_master
slot_master
Booking engine — chain service scheduling with backtracking.
Classes
BookingEngineError
Bases: Exception
Base exception of the booking engine.
All other exceptions inherit from it. A Django view can catch BookingEngineError for unified handling of all engine errors.
Example
try: result = service.book(...) except BookingEngineError as e: messages.error(request, str(e)) return redirect("booking:wizard")
Source code in src/codex_services/booking/_shared/exceptions.py
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | |
ChainBuildError
Bases: BookingEngineError
The engine could not assemble a chain for all services in the request.
Differs from NoAvailabilityError: here the chain was PARTIALLY assembled, but not to the end (constraint violation, incompatible services, etc.).
Used when
- max_chain_duration_minutes is exceeded
- Services are incompatible (future: excludes/tags)
- group_size > available parallel slots
Attributes:
| Name | Type | Description |
|---|---|---|
failed_at_index |
Index of the service (in service_requests) where assembly failed. |
|
reason |
Technical reason (for logs). |
Example
raise ChainBuildError( failed_at_index=2, reason="max_chain_duration_minutes=180 exceeded at service 'Coloring'", )
Source code in src/codex_services/booking/_shared/exceptions.py
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 | |
InvalidBookingDateError
Bases: BookingEngineError
Booking date is invalid.
Example reasons
- Date in the past
- Date beyond max_advance_days
- Salon is closed on this day
Attributes:
| Name | Type | Description |
|---|---|---|
booking_date |
Problematic date. |
|
reason |
Human-readable explanation. |
Example
raise InvalidBookingDateError( booking_date=date(2020, 1, 1), reason="Date in the past", )
Source code in src/codex_services/booking/_shared/exceptions.py
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | |
InvalidServiceDurationError
Bases: BookingEngineError
Incorrect service duration.
Raised if duration_minutes <= 0 or exceeds the explicit maximum.
Attributes:
| Name | Type | Description |
|---|---|---|
service_id |
ID of the problematic service. |
|
duration_minutes |
The passed duration value. |
Example
raise InvalidServiceDurationError(service_id="5", duration_minutes=0)
str(e) -> "Service 5: incorrect duration 0 min."
Source code in src/codex_services/booking/_shared/exceptions.py
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | |
NoAvailabilityError
Bases: BookingEngineError
The engine found no schedule options for the request.
Raised when ChainFinder.find() returns an empty EngineResult. Translated to a user-friendly message in the Django view.
Attributes:
| Name | Type | Description |
|---|---|---|
booking_date |
Date searched for. |
|
service_ids |
List of service IDs from the request. |
Example
raise NoAvailabilityError( booking_date=date(2024, 5, 10), service_ids=["5", "12"], )
str(e) -> "No free slots on 10.05.2024 for the selected services."
Source code in src/codex_services/booking/_shared/exceptions.py
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | |
ResourceNotAvailableError
Bases: BookingEngineError
Specific resource is not available for booking.
Used in RESOURCE_LOCKED mode when the selected resource does not work on this day or their schedule is empty.
Attributes:
| Name | Type | Description |
|---|---|---|
resource_id |
Resource's ID. |
|
booking_date |
Date attempted to book. |
Example
raise ResourceNotAvailableError(resource_id="3", booking_date=date(2024,5,10))
str(e) -> "Resource unavailable on 10.05.2024."
Source code in src/codex_services/booking/_shared/exceptions.py
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 | |
SlotAlreadyBookedError
Bases: BookingEngineError
Slot was free during display but became booked by the time of confirmation.
Race condition: client A and client B are viewing the same slot simultaneously. A clicks "Book" first -> B receives this error.
Attributes:
| Name | Type | Description |
|---|---|---|
resource_id |
Resource ID. |
|
service_id |
Service ID. |
|
booking_date |
Booking date. |
|
slot_time |
Selected time (string "HH:MM"). |
Example
raise SlotAlreadyBookedError( resource_id="3", service_id="5", booking_date=date(2024, 5, 10), slot_time="14:00", )
str(e) -> "Slot 14:00 on 10.05.2024 was booked. Please choose another time."
Source code in src/codex_services/booking/_shared/exceptions.py
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 | |
AvailabilityProvider
Bases: Protocol
Full availability provider — the main adapter contract. Implement this protocol for each framework adapter (Django, SQLAlchemy, etc.).
Source code in src/codex_services/booking/_shared/interfaces.py
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | |
Functions
build_resources_availability(resource_ids, target_date, cache_ttl=300, exclude_appointment_ids=None)
Build availability for a single date. Return {resource_id: ResourceAvailability}.
Source code in src/codex_services/booking/_shared/interfaces.py
45 46 47 48 49 50 51 52 53 | |
build_availability_batch(resource_ids, start_date, end_date)
Build availability for a date range in a single batch. Must avoid N+1 queries — group appointments in memory. Return {date: {resource_id: ResourceAvailability}}.
Source code in src/codex_services/booking/_shared/interfaces.py
55 56 57 58 59 60 61 62 63 64 65 66 | |
BusySlotsProvider
Bases: Protocol
Provides busy time slots for resources.
Source code in src/codex_services/booking/_shared/interfaces.py
29 30 31 32 33 34 35 36 | |
Functions
get_busy_intervals(resource_ids, target_date)
Return {resource_id: [(start, end), ...]} of busy times.
Source code in src/codex_services/booking/_shared/interfaces.py
32 33 34 35 36 | |
ScheduleProvider
Bases: Protocol
Provides working schedules for resources.
Source code in src/codex_services/booking/_shared/interfaces.py
17 18 19 20 21 22 23 24 25 26 | |
Functions
get_working_hours(resource_id, target_date)
Return (start, end) of working day or None if day off.
Source code in src/codex_services/booking/_shared/interfaces.py
20 21 22 | |
get_break_interval(resource_id, target_date)
Return (start, end) of break or None.
Source code in src/codex_services/booking/_shared/interfaces.py
24 25 26 | |
ChainFinder
Finds combinations of time slots for N services (booking chains).
Core algorithm: recursive backtracking. Iterates over possible resources and their free windows for each service. Checks for the absence of conflicts with already assigned services in the chain.
Examples:
1 service, any free resource:
finder = ChainFinder(step_minutes=30)
request = BookingEngineRequest(
service_requests=[
ServiceRequest(service_id="5", duration_minutes=60,
possible_resource_ids=["1", "2"])
],
booking_date=date(2024, 5, 10),
mode=BookingMode.SINGLE_DAY,
)
result = finder.find(request, resources_availability)
# result.solutions -- list of BookingChainSolution
# result.get_unique_start_times() -> ["09:00", "09:30", "10:00", ...]
2 services on the same day:
request = BookingEngineRequest(
service_requests=[svc_task_1, svc_task_2],
booking_date=date(2024, 5, 10),
mode=BookingMode.SINGLE_DAY,
)
result = finder.find(request, availability)
Booking a specific resource (RESOURCE_LOCKED):
# Just pass possible_resource_ids=[locked_resource_id]
# and mode=BookingMode.RESOURCE_LOCKED
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
step_minutes
|
int
|
Grid slot step (30 min by default). |
30
|
min_start
|
datetime | None
|
Minimum acceptable start time of the first service. None = no restriction (e.g., in tests). |
None
|
Source code in src/codex_services/booking/slot_master/chain_finder.py
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 | |
Functions
find(request, resources_availability, max_solutions=50, max_unique_starts=None)
Unified engine entry point. Delegates to the appropriate mode.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
BookingEngineRequest
|
Input request with services, date, and mode. |
required |
resources_availability
|
dict[str, MasterAvailability]
|
Dictionary {resource_id: MasterAvailability}. Usually prepared by an AvailabilityAdapter. Keys -- strings (resource identifiers). |
required |
max_solutions
|
int
|
Maximum number of options the engine will return. Does not affect correctness -- only completeness. |
50
|
max_unique_starts
|
int | None
|
Stop after finding N unique start times (based on items[0].start_time). None = no limit. Example: max_unique_starts=8 -> only the closest 8 slots, even if there are 16 in a day. Halves engine iterations. |
None
|
Returns:
| Type | Description |
|---|---|
EngineResult
|
EngineResult with found solutions. |
EngineResult
|
solutions sorted by the start time of the first service. |
Source code in src/codex_services/booking/slot_master/chain_finder.py
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | |
find_nearest(request, get_availability_for_date, search_from, search_days=60, max_solutions_per_day=1)
Searches for the first day with available slots in the search_days range.
Used for
- Rebooking: resource is sick -> find a new date for N appointments
- Waitlist: closest free slot to notify the client
- MULTI_DAY planning: find the first day the chain fits
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
BookingEngineRequest
|
Request (booking_date will be replaced for each checked day). |
required |
get_availability_for_date
|
Callable[[date], dict[str, MasterAvailability]]
|
callable(date) -> dict[str, MasterAvailability]. Called for each checked day. Wraps DjangoAvailabilityAdapter in the Django layer. |
required |
search_from
|
date
|
Date to start the search from (inclusive). |
required |
search_days
|
int
|
Maximum days to check. Defaults to 60. |
60
|
max_solutions_per_day
|
int
|
How many solutions to look for per day. 1 = fast mode (stop at the first one). |
1
|
Returns:
| Type | Description |
|---|---|
EngineResult
|
EngineResult of the first day with solutions. |
EngineResult
|
If nothing found in search_days — EngineResult(solutions=[]). |
Example (Django layer): adapter = DjangoAvailabilityAdapter() resource_ids = [...]
def get_avail(d):
return adapter.build_resources_availability(resource_ids, d)
result = finder.find_nearest(request, get_avail, search_from=date.today())
if result.has_solutions:
print(result.best.starts_at) # date and time of the new slot
Source code in src/codex_services/booking/slot_master/chain_finder.py
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 | |
BookingChainSolution
Bases: BaseDTO
One complete solution for the entire request (set of slots for all services).
Found by the engine. Guarantees no conflicts between services and respects all resource availability constraints.
Fields
items (list[SingleServiceSolution]): List of slots in the order of service execution.
score (float): Quality score of the solution (higher is better). Can be influenced by preferred resources, idle time, or resource reuse.
Example
solution = BookingChainSolution(items=[slot1, slot2], score=10.0)
print(f"Booking span: {solution.span_minutes} minutes")
Source code in src/codex_services/booking/slot_master/dto.py
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 | |
Attributes
starts_at
property
Return the start time of the first service in the chain.
ends_at
property
Return the end time of the last service (excluding gap).
span_minutes
property
Return total time from the start of the first to the end of the last service.
Functions
to_display()
Convert the solution into a dictionary for UI/serialization.
Returns:
| Name | Type | Description |
|---|---|---|
Dict |
dict[str, Any]
|
{service_id: {resource_id, start, end}, ...} |
Source code in src/codex_services/booking/slot_master/dto.py
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 | |
BookingEngineRequest
Bases: BookingRequest
Input request describing the entire desired booking chain.
Orchestrates multiple ServiceRequests into a single search task. Inherits booking_date from BookingRequest.
Fields
service_requests (list[ServiceRequest]): List of services to book. Order is critical for SINGLE_DAY mode — the engine will schedule them in the specified sequence. Minimum 1 service required.
booking_date (date): Target date. Inherited from BookingRequest. Used for SINGLE_DAY and RESOURCE_LOCKED. In MULTI_DAY mode, this represents the date of the first service.
mode (BookingMode): Engine operating strategy. Default is SINGLE_DAY.
overlap_allowed (bool): Allow parallel execution of services by different resources. False (default) — each subsequent service starts only after the previous one (plus its gap) ends. True — resources can work independently; services may start simultaneously if resources are available.
group_size (int): DEPRECATED. Use duplication of ServiceRequest with parallel_group.
max_chain_duration_minutes (int | None): Maximum total duration of the entire booking (from start of first to end of last service). None = no limit.
days_gap (list[int] | None): Day offsets for each service. Used strictly in MULTI_DAY mode.
Example
BookingEngineRequest(
service_requests=[svc_1, svc_2],
booking_date=date(2024, 5, 10),
mode=BookingMode.SINGLE_DAY,
overlap_allowed=True
)
Source code in src/codex_services/booking/slot_master/dto.py
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | |
Attributes
total_duration_minutes
property
Calculate total duration of all services without gap pauses.
total_block_minutes
property
Calculate total blocking time including pauses between services. Used for quick checks: does the chain fit into a given window.
EngineResult
Bases: BaseDTO
Engine work result containing all discovered solutions.
Fields
mode (BookingMode): The search strategy used. solutions (list[BookingChainSolution]): Found valid schedule options.
Example
result = ChainFinder().find(request, availability)
if result.has_solutions:
print(f"Best start: {result.best.starts_at}")
Source code in src/codex_services/booking/slot_master/dto.py
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 | |
Attributes
has_solutions
property
Return True if at least one solution was found.
best
property
Return the primary solution. - Without scorer: earliest by start time. - After scoring: the option with the highest score.
best_scored
property
Return the option with the maximum score among all solutions. Differs from 'best' if the solution list hasn't been sorted yet.
Functions
get_unique_start_times()
Return unique start times of the first service for UI grid display.
Returns:
| Type | Description |
|---|---|
list[str]
|
List[str]: ["09:00", "09:30", ...] |
Source code in src/codex_services/booking/slot_master/dto.py
366 367 368 369 370 371 372 373 374 | |
MasterAvailability
Bases: ResourceAvailability
Available time windows of a resource for the slot-master booking type.
Inherits resource_id and free_windows from ResourceAvailability. Adds slot-master-specific fields: buffer_between_minutes and work_start.
Fields
resource_id (str): Resource identifier (inherited). free_windows (list[tuple[datetime, datetime]]): List of (start, end) tuples (inherited). buffer_between_minutes (int): Minimum buffer required between bookings. work_start (datetime | None): Shift start anchor for slot alignment.
Example
MasterAvailability(
resource_id="resource_1",
free_windows=[(datetime(2024,5,10,9,0), datetime(2024,5,10,12,0))],
buffer_between_minutes=10,
)
Source code in src/codex_services/booking/slot_master/dto.py
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | |
ServiceRequest
Bases: BaseDTO
Request for a single service within a booking chain.
The engine treats this as an atomic requirement. It operates with abstract resource IDs (possible_resource_ids) rather than specific business entities.
Fields
service_id (str): Unique identifier of the service. String for universality (can be "5", "uuid-xxx", "task-1" — doesn't matter).
duration_minutes (int): Duration of the service in minutes. Must be > 0.
min_gap_after_minutes (int): Minimum gap (minutes) after this service before the next one in the chain. Default is 0 (no gap). Example: if there is a cooling down period of 30 mins → min_gap_after_minutes=30.
possible_resource_ids (list[str]): List of resource IDs capable of performing this service. The engine chooses an available resource from this list. For RESOURCE_LOCKED mode, this should contain exactly one element.
parallel_group (str | None): Tag for parallel execution group. Services with the same parallel_group can be performed simultaneously by different resources (if overlap_allowed=True is set in the request). None = service is performed independently (standard sequential behavior).
Example
ServiceRequest(
service_id="5",
duration_minutes=60,
possible_resource_ids=["1", "3", "7"],
)
Source code in src/codex_services/booking/slot_master/dto.py
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | |
Attributes
total_block_minutes
property
Return total time that blocks the resource: duration + gap after.
SingleServiceSolution
Bases: BookingSolution
Found slot for a single service in a booking chain.
Inherits resource_id, start_time, end_time from BookingSolution. Adds service_id and gap_end_time.
Fields
service_id (str): Reference to the original ServiceRequest.service_id. resource_id (str): Identifier of the resource assigned to this service (inherited). start_time (datetime): Scheduled start time of the service (inherited). end_time (datetime): Scheduled completion time (excluding gap) (inherited). gap_end_time (datetime): End of the blocking period (end_time + gap).
Example
slot = SingleServiceSolution(
service_id="5",
resource_id="1",
start_time=datetime(2024, 5, 10, 10, 0),
end_time=datetime(2024, 5, 10, 11, 0),
gap_end_time=datetime(2024, 5, 10, 11, 15),
)
Source code in src/codex_services/booking/slot_master/dto.py
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 | |
WaitlistEntry
Bases: BaseDTO
Notification data for a nearest available slot for waitlisted clients.
Used when a desired slot was unavailable, but an alternative was found (e.g., via find_nearest() or a background worker).
Fields
available_date (date): Date of the found alternative slot. available_time (str): Start time of the first service ("HH:MM"). solution (BookingChainSolution): Complete schedule details. days_from_request (int): Delta from original request date for ranking.
Example
Worker detects a cancellation:
result = finder.find_nearest(request, search_from=original_date) if result.has_solutions: entry = WaitlistEntry.from_engine_result(result, original_date) notify_client(entry)
Source code in src/codex_services/booking/slot_master/dto.py
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 | |
Functions
from_engine_result(result, original_date)
classmethod
Factory method to create a WaitlistEntry from an EngineResult.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
result
|
EngineResult
|
The engine's output. |
required |
original_date
|
date
|
Original requested date for delta calculation. |
required |
Returns:
| Type | Description |
|---|---|
WaitlistEntry | None
|
WaitlistEntry or None if no solutions exist. |
Source code in src/codex_services/booking/slot_master/dto.py
411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 | |
BookingMode
Bases: StrEnum
Operating mode for ChainFinder.
SINGLE_DAY
All services from the request must fit within a single day. The common mode — multiple services in one visit.
MULTI_DAY
Each service can be scheduled for a different day (Stub).
RESOURCE_LOCKED
Booking for a specific resource (e.g., from their personal page). The engine relies entirely on the provided resource constraints.
Example
mode = BookingMode.SINGLE_DAY
Source code in src/codex_services/booking/slot_master/modes.py
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | |
BookingScorer
Evaluates engine solutions and returns an EngineResult with populated scores.
Usage
scorer = BookingScorer( weights=ScoringWeights(preferred_resource_bonus=15.0), preferred_resource_ids=["3", "7"], ) ranked = scorer.score(result)
Best solution by score (not just the earliest):
print(ranked.best.score) print(ranked.best.to_display())
All solutions sorted by score (highest -> first):
for solution in ranked.solutions: print(solution.score, solution.starts_at)
Source code in src/codex_services/booking/slot_master/scorer.py
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | |
Functions
__init__(weights=None, preferred_resource_ids=None)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
weights
|
ScoringWeights | None
|
Criteria weights. None = ScoringWeights() with defaults. |
None
|
preferred_resource_ids
|
list[str] | None
|
List of IDs for preferred resources. Must be strings (str(resource.pk)). On match -> preferred_resource_bonus applied. |
None
|
Source code in src/codex_services/booking/slot_master/scorer.py
91 92 93 94 95 96 97 98 99 100 101 102 103 104 | |
score(result)
Populates a score for each solution and returns a re-sorted EngineResult.
Sorting: descending by score (best = first = result.best). Solutions with the same score are sorted by starts_at (earlier = better).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
result
|
EngineResult
|
EngineResult from ChainFinder.find(). |
required |
Returns:
| Type | Description |
|---|---|
EngineResult
|
New EngineResult (using frozen=True -> model_copy) with populated scores |
EngineResult
|
and solutions sorted by score DESC. |
EngineResult
|
If result is empty, it is returned unchanged. |
Source code in src/codex_services/booking/slot_master/scorer.py
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | |
ScoringWeights
dataclass
Weights for solution evaluation criteria. Configurable per project.
All weights are additive: total score = sum of applicable bonuses. Higher score = more preferred solution.
Fields
preferred_resource_bonus (float): Bonus for each service with a preferred resource. Passed via BookingScorer(preferred_resource_ids=[...]). Example: client always visits Anya -> preferred_resource_ids=["3"].
same_resource_bonus (float): Bonus if one resource performs multiple services. Reduces the number of times the client must switch resources.
min_idle_bonus_per_hour (float): Bonus for each hour of minimized idle time between services. Encourages compact chains.
early_slot_penalty_per_hour (float): Penalty for each hour from the start of the workday to the first service. Encourages earlier slots (less index = better). Negative value is not needed here — subtracted automatically.
Example (encourage early times AND preferred resource): ScoringWeights( preferred_resource_bonus=20.0, early_slot_penalty_per_hour=1.0, )
Source code in src/codex_services/booking/slot_master/scorer.py
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | |
Functions
find_nearest_slots(request_data, get_availability_fn, *, search_from=None, search_days=60, step_minutes=30)
Search for the nearest available booking slots across multiple days.
Iterates days starting from search_from until a day with solutions is found.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request_data
|
dict[str, Any]
|
Dict matching :class: |
required |
get_availability_fn
|
Callable[[date], list[dict[str, Any]]]
|
Callable |
required |
search_from
|
date | None
|
Start date (inclusive). Defaults to :func: |
None
|
search_days
|
int
|
Max number of days to search. Defaults to 60. |
60
|
step_minutes
|
int
|
Slot grid step in minutes. Defaults to 30. |
30
|
Returns:
| Name | Type | Description |
|---|---|---|
Serialised |
dict[str, Any] | None
|
class: |
dict[str, Any] | None
|
matching day, or |
Example::
def get_availability(d):
# return your per-day availability data as list of dicts
return [...]
result = find_nearest_slots(
request_data={...},
get_availability_fn=get_availability,
search_from=date.today(),
)
if result:
print(result["solutions"][0]["items"][0]["start_time"])
Source code in src/codex_services/booking/slot_master/api.py
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | |
find_slots(request_data, resources_availability, *, step_minutes=30, max_solutions=50, scoring_weights=None, preferred_resource_ids=None)
Find available booking slots for one day.
Validates input via Pydantic, runs :class:ChainFinder, optionally scores results.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request_data
|
dict[str, Any]
|
Dict matching :class: |
required |
resources_availability
|
list[dict[str, Any]]
|
List of dicts matching
:class: |
required |
step_minutes
|
int
|
Slot grid step in minutes. Defaults to 30. |
30
|
max_solutions
|
int
|
Max solutions returned by the engine. Defaults to 50. |
50
|
scoring_weights
|
dict[str, Any] | None
|
Optional dict with :class: |
None
|
preferred_resource_ids
|
list[str] | None
|
Resource IDs to boost in scoring.
Only effective when |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
Serialised |
dict[str, Any]
|
class: |
dict[str, Any]
|
Key |
|
dict[str, Any]
|
Returns an empty list under |
Raises:
| Type | Description |
|---|---|
ValidationError
|
If |
NotImplementedError
|
If the request uses an unsupported mode (e.g. MULTI_DAY or group bookings). |
Example::
result = find_slots(
request_data={
"service_requests": [
{"service_id": "s1", "duration_minutes": 60,
"possible_resource_ids": ["m1"]},
],
"booking_date": "2024-05-15",
},
resources_availability=[
{"resource_id": "m1",
"free_windows": [["2024-05-15T09:00:00", "2024-05-15T18:00:00"]]},
],
)
for chain in result["solutions"]:
print(chain["items"][0]["start_time"])
Source code in src/codex_services/booking/slot_master/api.py
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | |