Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
# Changelog

## 0.5.0

- Added external BusyPeriod conflicts for calendar/application-provided busy intervals.
- Added machine-readable AvailabilityResult diagnostic codes.
- Added idempotency keys for retried reservations.
- Prevented direct non-pool reservations from bypassing active holds.
- Added database-backed BookingStore guidance and framework integration patterns.
- Added performance benchmark guidance.
- Expanded documentation for required multi-resource availability.

## 0.4.0

- Added temporary booking holds and hold storage abstractions.
- Added first-available, round-robin, and least-loaded resource allocation.
- Added hold and resource-pool regression coverage.
- Added runnable examples for holds and resource pools.

## 0.2.0

- Added recurring weekly appointment series with rollback on failed occurrence.
Expand Down
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ RecurrenceRule(

## Project status

Current version: **0.3.0**
Current version: **0.5.0**

Slotify is currently in alpha. The API may evolve before 1.0.0, so pin the version in production applications and review the changelog when upgrading.

Expand All @@ -445,3 +445,28 @@ MIT License
## Author

Kalash Gulati


### External calendar conflicts

Normalize conflicts from an external calendar in your application and pass them to BookingPolicy:

~~~python
from slotify import BookingPolicy, BusyPeriod

policy = BookingPolicy(
busy_periods=(
BusyPeriod(start=calendar_start, end=calendar_end, reason="External event"),
),
)
~~~

### Idempotent API retries

Use an idempotency key for requests that may be retried:

~~~python
booking = engine.reserve(slot, idempotency_key=request_id)
~~~

A production BookingStore should enforce the same key atomically in its database.
23 changes: 16 additions & 7 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,30 @@ Slotify is currently in alpha. This roadmap is a guide rather than a promise; pr
- Recurrence exclusions
- Expanded tests and documentation

## v0.5.0 completed

- External busy-period conflicts
- Machine-readable availability diagnostics
- Idempotent reservation keys
- Stronger hold enforcement on direct reservations
- Database-backed store guidance
- Django, FastAPI, and Flask integration patterns
- Availability performance benchmark guidance
- Required multi-resource availability documentation

## Current focus

- Keep scheduling behavior predictable and timezone-safe.
- Expand DST and recurrence edge-case coverage.
- Improve database-backed BookingStore examples.
- Add performance benchmarks for large availability ranges.
- Expand DST, recurrence, concurrency, and store-contract coverage.
- Collect real-world scheduling use cases and bug reports.
- Measure performance with realistic workloads before optimizing.

## Exploring

- PostgreSQL/SQLAlchemy BookingStore examples
- Calendar conflict adapters
- Additional framework examples
- Reference PostgreSQL store implementations
- Calendar provider adapters maintained outside the core package
- Additional resource routing strategies only when real use cases require them
- Performance improvements for large resource pools
- More advanced routing and resource-selection strategies

## Scope

Expand Down
47 changes: 47 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,53 @@ RecurrenceRule(
weekdays is valid for weekly recurrence. day_of_month is used for monthly/yearly recurrence. When a monthly day does not exist in a month, the last valid day of that month is used. count counts emitted occurrences, so excluded dates do not consume the count.


## v0.5.0 external busy periods

Use BusyPeriod for conflicts supplied by another calendar or application:

~~~python
from slotify import BookingPolicy, BusyPeriod

policy = BookingPolicy(
busy_periods=(
BusyPeriod(
start=calendar_start,
end=calendar_end,
reason="External calendar event",
),
),
)
~~~

Busy periods are evaluated alongside Slotify's own booking rules.

## v0.5.0 availability diagnostics

AvailabilityResult includes a machine-readable code in addition to the human-readable reason.

Common codes include:

~~~text
available
policy
hold_conflict
capacity_conflict
resource_unavailable
~~~

## v0.5.0 idempotent reservations

Pass an idempotency key when an API request may be retried:

~~~python
booking = engine.reserve(
slot,
idempotency_key="payment-attempt-123",
)
~~~

A store should enforce idempotency atomically in production.

## v0.4.0 holds

Temporary checkout holds are represented by BookingHold.
Expand Down
38 changes: 38 additions & 0 deletions docs/benchmarks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Performance Benchmarks

Slotify should be measured with the scheduling configuration your application actually uses.

A useful benchmark should vary:

- number of days scanned
- slots per day
- number of resources
- number of existing bookings
- number of holds
- timezone/DST complexity

## Simple local benchmark

The following measures a repeated availability query without adding a benchmark dependency:

~~~python
from time import perf_counter
from slotify import AvailabilityEngine, SlotGenerator

generator = SlotGenerator(
start="09:00",
end="17:00",
duration=30,
timezone="Asia/Kolkata",
)
engine = AvailabilityEngine(generator)

start = perf_counter()
for _ in range(100):
engine.available_slots("2026-10-05", "2026-10-30")
elapsed = perf_counter() - start

print(f"{elapsed:.4f}s for 100 queries")
~~~

Treat this as a smoke benchmark, not a universal performance claim. Production performance depends heavily on the application store, resource count, database, and workload.
60 changes: 60 additions & 0 deletions docs/database-stores.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Database-backed Stores

Slotify deliberately does not require Django, SQLAlchemy, or a database. The core package defines the BookingStore protocol so your application can choose its persistence layer.

## Production rule

The final reservation must be atomic in your database.

For PostgreSQL, a production store normally uses a transaction plus database-level protection for the booking/resource/time interval. The exact implementation depends on your schema and whether you use Django ORM, SQLAlchemy, or another database layer.

Your store should implement:

~~~python
class MyBookingStore:
def reserve(self, booking):
# transaction + database conflict protection
...
~~~

## Django

Keep Slotify objects at the service boundary:

~~~python
def reserve_slot(provider, slot):
engine = get_provider_engine(provider)
return engine.reserve(slot, idempotency_key="checkout-123")
~~~

Persist the returned Booking in your Django model inside the same application-level transaction strategy used by your store.

## SQLAlchemy

The same design works with SQLAlchemy:

~~~python
class SQLAlchemyBookingStore:
def reserve(self, booking):
# use Session transaction and database constraints
...
~~~

Slotify does not import SQLAlchemy, so applications that do not use it pay no dependency cost.

## Idempotency

Use an idempotency key for retried API requests:

~~~python
booking = engine.reserve(
slot,
idempotency_key=request_id,
)
~~~

The built-in in-memory store remembers the key. A database-backed store should also enforce uniqueness atomically so concurrent requests with the same key cannot create duplicates.

## Holds

The same production rule applies to HoldStore. A checkout hold and its final confirmation need shared durable storage and concurrency protection when multiple application workers can handle the same customer.
45 changes: 45 additions & 0 deletions docs/framework-examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Framework Examples

Slotify is framework-independent. These patterns show where it fits.

## Django / DRF

Use Slotify in a service layer and return serialized Slot or Booking values from your API.

~~~python
def available_slots(provider, date):
engine = get_provider_engine(provider)
return engine.available_slots(date)
~~~

See the Django integration guide for a complete example.

## FastAPI

The same service can be called from a FastAPI route:

~~~python
from fastapi import FastAPI

app = FastAPI()

@app.get("/providers/{provider_id}/slots")
def slots(provider_id: str, date: str):
engine = get_provider_engine(provider_id)
return [slot.to_dict() for slot in engine.available_slots(date)]
~~~

FastAPI owns HTTP, authentication, dependency injection, and persistence. Slotify owns scheduling rules.

## Flask

A Flask route can use the same service:

~~~python
@app.get("/providers/<provider_id>/slots")
def slots(provider_id):
engine = get_provider_engine(provider_id)
return {"slots": [slot.to_dict() for slot in engine.available_slots(request.args["date"])]}
~~~

No framework integration package is required.
14 changes: 14 additions & 0 deletions docs/resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,17 @@ These strategies are allocation helpers. Your application should still filter re
Resource selection does not replace concurrency control.

Two requests can observe the same resource as available. The final reservation in your BookingStore must be atomic.


## Required resources

For appointments that require multiple resources together, use resource_ids:

~~~python
AvailabilityEngine(
generator,
resource_ids=("doctor-1", "room-4"),
)
~~~

A booking protects all listed resources. Another booking conflicts when its protected interval overlaps and it shares at least one required resource. This provides the core availability intersection needed for provider + room + equipment style appointments.
17 changes: 15 additions & 2 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ python -c "import slotify; print(slotify.__version__)"
Expected for this documentation release:

~~~text
0.2.0
0.5.0
~~~

## Run your tests
Expand Down Expand Up @@ -181,7 +181,7 @@ Slotify is currently in alpha.
For production, pin the version that you have tested:

~~~text
slotify-scheduling==0.2.0
slotify-scheduling==0.5.0
~~~

Review the changelog before upgrading.
Expand All @@ -208,3 +208,16 @@ Before deploying a system built with Slotify, verify:
[ ] payment and notification logic is outside Slotify
[ ] package version is pinned
~~~


## External calendar conflicts

Test BusyPeriod values from Google Calendar, Outlook, your own calendar service, or another source as ordinary scheduling conflicts. Keep the external adapter in your application and pass normalized BusyPeriod objects into Slotify.

## Idempotency

For retried HTTP/payment requests, test that the same idempotency key returns the same Booking and that different keys still obey normal conflict rules.

## Availability diagnostics

Use AvailabilityResult.code for API-level error handling and AvailabilityResult.reason for logs or user-facing explanations.
3 changes: 3 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,14 @@ nav:
- Examples: examples.md
- Integrations:
- Django / DRF: django.md
- Framework Examples: framework-examples.md
- Reference:
- API Guide: api.md
- Timezones & DST: timezones.md
- Production:
- Testing & Production: testing.md
- Database-backed Stores: database-stores.md
- Performance Benchmarks: benchmarks.md
- FAQ: faq.md
- Changelog: changelog.md
- Migrating from v0.2/v0.3: migration.md
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "slotify-scheduling"
version = "0.4.0"
version = "0.5.0"
description = "Timezone-aware Python scheduling and appointment slot engine for availability and booking systems."
readme = "README.md"
requires-python = ">=3.10"
Expand Down
Loading
Loading