Skip to content
This repository was archived by the owner on Oct 6, 2025. It is now read-only.

Commit 25d834e

Browse files
chore: move Python quickstarts to new repo (#114)
1 parent fb12f93 commit 25d834e

File tree

164 files changed

+15227
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

164 files changed

+15227
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ __pycache__
55
/**/dist
66
/**/*.egg-info
77
/**/*-stubs
8+
.venv
89

910
# Eclipse, Netbeans and IntelliJ files
1011
/.*

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ than using [Timefold Solver for Java](https://github.com/TimefoldAI/timefold-sol
1616

1717
## Get started with Timefold Solver in Python
1818

19-
* [Clone the Quickstarts repository](https://github.com/TimefoldAI/timefold-solver-python)
19+
* [Clone Timefold Solver for Python repository](https://github.com/TimefoldAI/timefold-solver-python): `git clone https://github.com/TimefoldAI/timefold-solver-python.git`
20+
21+
* Navigate to the `quickstarts` directory and choose a quickstart: `cd timefold-solver-python/quickstarts/hello-world`
2022

2123
## Requirements
2224

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Employee Scheduling (Python)
2+
3+
Schedule shifts to employees, accounting for employee availability and shift skill requirements.
4+
5+
![Employee Scheduling Screenshot](./employee-scheduling-screenshot.png)
6+
7+
- [Prerequisites](#prerequisites)
8+
- [Run the application](#run-the-application)
9+
- [Test the application](#test-the-application)
10+
11+
> [!TIP]
12+
> <img src="https://docs.timefold.ai/_/img/models/employee-shift-scheduling.svg" align="right" width="50px" /> [Check out our off-the-shelf model for Employee Shift Scheduling](https://app.timefold.ai/models/employee-scheduling/v1). This model supports many additional constraints such as skills, pairing employees, fairness and more.
13+
14+
## Prerequisites
15+
16+
1. Install [Python 3.11 or 3.12](https://www.python.org/downloads/).
17+
18+
2. Install JDK 17+, for example with [Sdkman](https://sdkman.io):
19+
20+
```sh
21+
$ sdk install java
22+
```
23+
24+
## Run the application
25+
26+
1. Git clone the timefold-solver-python repo and navigate to this directory:
27+
28+
```sh
29+
$ git clone https://github.com/TimefoldAI/timefold-solver-python.git
30+
...
31+
$ cd timefold-solver-python/quickstarts/employee-scheduling
32+
```
33+
34+
2. Create a virtual environment:
35+
36+
```sh
37+
$ python -m venv .venv
38+
```
39+
40+
3. Activate the virtual environment:
41+
42+
```sh
43+
$ . .venv/bin/activate
44+
```
45+
46+
4. Install the application:
47+
48+
```sh
49+
$ pip install -e .
50+
```
51+
52+
5. Run the application:
53+
54+
```sh
55+
$ run-app
56+
```
57+
58+
6. Visit [http://localhost:8080](http://localhost:8080) in your browser.
59+
60+
7. Click on the **Solve** button.
61+
62+
## Test the application
63+
64+
1. Run tests:
65+
66+
```sh
67+
$ pytest
68+
```
69+
70+
## More information
71+
72+
Visit [timefold.ai](https://timefold.ai).
105 KB
Loading
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
[loggers]
2+
keys=root,timefold_solver
3+
4+
[handlers]
5+
keys=consoleHandler
6+
7+
[formatters]
8+
keys=simpleFormatter
9+
10+
[logger_root]
11+
level=INFO
12+
handlers=consoleHandler
13+
14+
[logger_timefold_solver]
15+
level=INFO
16+
qualname=timefold.solver
17+
handlers=consoleHandler
18+
propagate=0
19+
20+
[handler_consoleHandler]
21+
class=StreamHandler
22+
level=INFO
23+
formatter=simpleFormatter
24+
args=(sys.stdout,)
25+
26+
[formatter_simpleFormatter]
27+
class=uvicorn.logging.ColourizedFormatter
28+
format={levelprefix:<8} @ {name} : {message}
29+
style={
30+
use_colors=True
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
6+
[project]
7+
name = "employee_scheduling"
8+
version = "1.0.0"
9+
requires-python = ">=3.11"
10+
dependencies = [
11+
'timefold == 1.24.0b0',
12+
'fastapi == 0.111.0',
13+
'pydantic == 2.7.3',
14+
'uvicorn == 0.30.1',
15+
'pytest == 8.2.2',
16+
]
17+
18+
19+
[project.scripts]
20+
run-app = "employee_scheduling:main"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import uvicorn
2+
3+
from .rest_api import app
4+
5+
6+
def main():
7+
config = uvicorn.Config("employee_scheduling:app",
8+
port=8080,
9+
log_config="logging.conf",
10+
use_colors=True)
11+
server = uvicorn.Server(config)
12+
server.run()
13+
14+
15+
if __name__ == "__main__":
16+
main()
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
from timefold.solver.score import (constraint_provider, ConstraintFactory, Joiners, HardSoftDecimalScore, ConstraintCollectors)
2+
from datetime import datetime, date
3+
4+
from .domain import Employee, Shift
5+
6+
7+
def get_minute_overlap(shift1: Shift, shift2: Shift) -> int:
8+
return (min(shift1.end, shift2.end) - max(shift1.start, shift2.start)).total_seconds() // 60
9+
10+
11+
def is_overlapping_with_date(shift: Shift, dt: date) -> bool:
12+
return shift.start.date() == dt or shift.end.date() == dt
13+
14+
15+
def overlapping_in_minutes(first_start_datetime: datetime, first_end_datetime: datetime,
16+
second_start_datetime: datetime, second_end_datetime: datetime) -> int:
17+
latest_start = max(first_start_datetime, second_start_datetime)
18+
earliest_end = min(first_end_datetime, second_end_datetime)
19+
delta = (earliest_end - latest_start).total_seconds() / 60
20+
return max(0, delta)
21+
22+
23+
def get_shift_overlapping_duration_in_minutes(shift: Shift, dt: date) -> int:
24+
overlap = 0
25+
start_date_time = datetime.combine(dt, datetime.max.time())
26+
end_date_time = datetime.combine(dt, datetime.min.time())
27+
overlap += overlapping_in_minutes(start_date_time, end_date_time, shift.start, shift.end)
28+
return overlap
29+
30+
31+
@constraint_provider
32+
def define_constraints(constraint_factory: ConstraintFactory):
33+
return [
34+
# Hard constraints
35+
required_skill(constraint_factory),
36+
no_overlapping_shifts(constraint_factory),
37+
at_least_10_hours_between_two_shifts(constraint_factory),
38+
one_shift_per_day(constraint_factory),
39+
unavailable_employee(constraint_factory),
40+
# Soft constraints
41+
undesired_day_for_employee(constraint_factory),
42+
desired_day_for_employee(constraint_factory),
43+
balance_employee_shift_assignments(constraint_factory)
44+
]
45+
46+
47+
def required_skill(constraint_factory: ConstraintFactory):
48+
return (constraint_factory.for_each(Shift)
49+
.filter(lambda shift: shift.required_skill not in shift.employee.skills)
50+
.penalize(HardSoftDecimalScore.ONE_HARD)
51+
.as_constraint("Missing required skill")
52+
)
53+
54+
55+
def no_overlapping_shifts(constraint_factory: ConstraintFactory):
56+
return (constraint_factory
57+
.for_each_unique_pair(Shift,
58+
Joiners.equal(lambda shift: shift.employee.name),
59+
Joiners.overlapping(lambda shift: shift.start, lambda shift: shift.end))
60+
.penalize(HardSoftDecimalScore.ONE_HARD, get_minute_overlap)
61+
.as_constraint("Overlapping shift")
62+
)
63+
64+
65+
def at_least_10_hours_between_two_shifts(constraint_factory: ConstraintFactory):
66+
return (constraint_factory
67+
.for_each(Shift)
68+
.join(Shift,
69+
Joiners.equal(lambda shift: shift.employee.name),
70+
Joiners.less_than_or_equal(lambda shift: shift.end, lambda shift: shift.start)
71+
)
72+
.filter(lambda first_shift, second_shift:
73+
(second_shift.start - first_shift.end).total_seconds() // (60 * 60) < 10)
74+
.penalize(HardSoftDecimalScore.ONE_HARD,
75+
lambda first_shift, second_shift:
76+
600 - ((second_shift.start - first_shift.end).total_seconds() // 60))
77+
.as_constraint("At least 10 hours between 2 shifts")
78+
)
79+
80+
81+
def one_shift_per_day(constraint_factory: ConstraintFactory):
82+
return (constraint_factory
83+
.for_each_unique_pair(Shift,
84+
Joiners.equal(lambda shift: shift.employee.name),
85+
Joiners.equal(lambda shift: shift.start.date()))
86+
.penalize(HardSoftDecimalScore.ONE_HARD)
87+
.as_constraint("Max one shift per day")
88+
)
89+
90+
91+
def unavailable_employee(constraint_factory: ConstraintFactory):
92+
return (constraint_factory.for_each(Shift)
93+
.join(Employee, Joiners.equal(lambda shift: shift.employee, lambda employee: employee))
94+
.flatten_last(lambda employee: employee.unavailable_dates)
95+
.filter(lambda shift, unavailable_date: is_overlapping_with_date(shift, unavailable_date))
96+
.penalize(HardSoftDecimalScore.ONE_HARD,
97+
lambda shift, unavailable_date: get_shift_overlapping_duration_in_minutes(shift,
98+
unavailable_date))
99+
.as_constraint("Unavailable employee")
100+
)
101+
102+
103+
def undesired_day_for_employee(constraint_factory: ConstraintFactory):
104+
return (constraint_factory.for_each(Shift)
105+
.join(Employee, Joiners.equal(lambda shift: shift.employee, lambda employee: employee))
106+
.flatten_last(lambda employee: employee.undesired_dates)
107+
.filter(lambda shift, undesired_date: is_overlapping_with_date(shift, undesired_date))
108+
.penalize(HardSoftDecimalScore.ONE_SOFT,
109+
lambda shift, undesired_date: get_shift_overlapping_duration_in_minutes(shift, undesired_date))
110+
.as_constraint("Undesired day for employee")
111+
)
112+
113+
114+
def desired_day_for_employee(constraint_factory: ConstraintFactory):
115+
return (constraint_factory.for_each(Shift)
116+
.join(Employee, Joiners.equal(lambda shift: shift.employee, lambda employee: employee))
117+
.flatten_last(lambda employee: employee.desired_dates)
118+
.filter(lambda shift, desired_date: is_overlapping_with_date(shift, desired_date))
119+
.reward(HardSoftDecimalScore.ONE_SOFT,
120+
lambda shift, desired_date: get_shift_overlapping_duration_in_minutes(shift, desired_date))
121+
.as_constraint("Desired day for employee")
122+
)
123+
124+
125+
def balance_employee_shift_assignments(constraint_factory: ConstraintFactory):
126+
return (constraint_factory.for_each(Shift)
127+
.group_by(lambda shift: shift.employee, ConstraintCollectors.count())
128+
.complement(Employee, lambda e: 0) # Include all employees which are not assigned to any shift.
129+
.group_by(ConstraintCollectors.load_balance(lambda employee, shift_count: employee,
130+
lambda employee, shift_count: shift_count))
131+
.penalize_decimal(HardSoftDecimalScore.ONE_SOFT, lambda load_balance: load_balance.unfairness())
132+
.as_constraint("Balance employee shift assignments")
133+
)
134+

0 commit comments

Comments
 (0)