Compare commits

...
2 Commits
Author SHA1 Message Date
Ross b9edf36103 continue fixing tests for scip 2023-05-28 09:01:35 +01:00
Ross 3f78ecc671 update a few things 2023-05-23 14:48:12 +01:00
11 changed files with 386 additions and 90 deletions
+4 -1
View File
@@ -9,4 +9,7 @@ no nights.log
no twighlights.log
no weekends.log
logs/
.env/
.env/
*.db
web/rota
+1 -1
View File
@@ -17,7 +17,7 @@
"name": "Python: Run",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/run.py",
"program": "${workspaceFolder}/gen.py",
"console": "integratedTerminal"
},
]
+106 -42
View File
@@ -2,7 +2,7 @@ from calendar import week
import datetime
from distutils.log import debug
import itertools
from typing import Dict, Iterable, List, Sequence, Tuple, Set, Any, Type
from typing import Dict, Iterable, List, Sequence, Tuple, Set, Any, Type, Literal
import time
from pydantic import BaseModel, Extra, constr
@@ -65,9 +65,21 @@ SHIFT_BOUNDS = {
"weekend_count": (0, 60),
}
VALID_SHIFT_CONSTRAINTS = Literal[
"night",
"pre",
"post",
"balance_across_groups",
"limit_grade_number",
"minimum_grade_number",
"max_shifts_per_week",
"require_remote_site_presence",
"require_remote_site_presence_week",
]
class ShiftConstraint(BaseModel):
name: str
name: VALID_SHIFT_CONSTRAINTS
options: bool | int | Dict | tuple = False
@@ -79,7 +91,7 @@ class SingleShift(BaseModel):
balance_across_groups (requires block assignment)
postclear(2) / preclear(2)
post / pre clear
require_remote_site_presence_week:
options: (site, required_number)
@@ -180,9 +192,16 @@ class RotaBuilder(object):
SHIFT_BOUNDS=SHIFT_BOUNDS,
):
console.print(Panel(f"""[white]
console.print(
Panel(
f"""[white]
{locals()}
""", style="green", title="Generating Rota"), justify="left")
""",
style="green",
title="Generating Rota",
),
justify="left",
)
self.SHIFT_BOUNDS = SHIFT_BOUNDS
self.shifts: List[SingleShift] = [] # type List[SingleShift]
@@ -191,7 +210,6 @@ class RotaBuilder(object):
self.use_shift_balance_extra = use_shift_balance_extra
self.use_bank_holiday_extra = use_bank_holiday_extra
self.workers: list[Worker] = []
# self.night_blocks = ["weekday", "weekend", "none"]
@@ -231,14 +249,16 @@ class RotaBuilder(object):
"avoid_shifts_by_grades": [],
}
self.results = None
self.ignore_valid_shifts = False
self.set_rota_dates(start_date, weeks_to_rota)
def set_rota_dates(self, start_date: datetime.datetime, weeks_to_rota: int):
self.run_start_time: datetime.datetime | None = None
self.run_end_time: datetime.datetime | None = None
def set_rota_dates(self, start_date: datetime.date, weeks_to_rota: int):
self.weeks_to_rota = weeks_to_rota
@@ -293,25 +313,32 @@ class RotaBuilder(object):
else:
self.opt = SolverFactory(solver)
console.print("Solving")
if use_neos:
solver_manager = SolverManagerFactory("neos") # Solve using neos server
# results = solver_manager.solve(Rota.model, opt=opt, logfile="test.log")
results = solver_manager.solve(
self.model, keepfiles=True, tee=True, opt=self.opt, logfile="test.log"
)
else:
results = self.opt.solve(
self.model,
tee=True,
options=options,
# options={
# "threads": 10,
# },
logfile="test.log",
)
try:
console.print("Solving")
if use_neos:
solver_manager = SolverManagerFactory("neos") # Solve using neos server
# results = solver_manager.solve(Rota.model, opt=opt, logfile="test.log")
results = solver_manager.solve(
self.model,
keepfiles=True,
tee=True,
opt=self.opt,
logfile="test.log",
)
else:
results = self.opt.solve(
self.model,
tee=True,
options=options,
# options={
# "threads": 10,
# },
logfile="test.log",
keepfiles=True,
)
except KeyboardInterrupt:
return
pass
self.results = results
@@ -321,8 +348,8 @@ class RotaBuilder(object):
console.print(f"Attempting each shift individually")
self.solve_shifts_individually(options)
if not results.solver.status:
sys.exit(0)
# if not results.solver.status:
# sys.exit(0)
def solve_shifts_by_block(self, options, block_length: int, folder="block_shifts"):
shifts = self.shifts
@@ -336,19 +363,23 @@ class RotaBuilder(object):
for block in range(0, no_blocks):
start_time = time.time()
start_date = original_start_date + datetime.timedelta(weeks=block)
self.set_rota_dates(start_date = start_date,weeks_to_rota=block_length)
console.print(f"Testing block: start_date - {self.start_date} , length {self.weeks_to_rota} weeks")
self.set_rota_dates(start_date=start_date, weeks_to_rota=block_length)
console.print(
f"Testing block: start_date - {self.start_date} , length {self.weeks_to_rota} weeks"
)
self.clear_shifts()
self.add_shifts(*shifts)
self.build_and_solve(options)
self.export_rota_to_html(filename=f"{start_date}_{self.weeks_to_rota}", folder=folder)
self.export_rota_to_html(
filename=f"{start_date}_{self.weeks_to_rota}", folder=folder
)
end_time = time.time()
time_taken = end_time - start_time
outcomes[f"{start_date}_{self.weeks}"] = (
self.results.solver.status,
self.results.solver.termination_condition,
time_taken
time_taken,
)
console.print(outcomes)
@@ -380,6 +411,9 @@ class RotaBuilder(object):
solve=True,
debug_if_fail: bool = False,
):
self.run_start_time = datetime.datetime.now()
self.build_shifts()
self.build_workers()
self.build_model()
@@ -387,6 +421,8 @@ class RotaBuilder(object):
if solve:
self.solve_model(options=options, debug_if_fail=debug_if_fail)
self.run_end_time = datetime.datetime.now()
def build_model(self):
# Initialize model
self.model = ConcreteModel()
@@ -1010,7 +1046,9 @@ class RotaBuilder(object):
# - self.model.night_per_site_t2[week, block, site]
# == self.model.night_per_site[week, block, site] - 1
# )
for site in track(self.sites, description="Generating site balance constraints..."):
for site in track(
self.sites, description="Generating site balance constraints..."
):
for shift in self.get_shifts_with_constraint("balance_across_groups"):
if site in shift.sites:
block = shift.name
@@ -1122,7 +1160,9 @@ class RotaBuilder(object):
# Most of our constraints apply per worker
for worker in self.workers:
console.rule(f"Generate worker constraints: [bold blue]{worker.name}[/bold blue]")
console.rule(
f"Generate worker constraints: [bold blue]{worker.name}[/bold blue]"
)
logging.debug(f"Generate worker constraints: {worker.name}")
if self.constraint_options["minimise_shift_diffs"]:
@@ -1148,7 +1188,9 @@ class RotaBuilder(object):
except ValueError:
# Occurs if there are no shifts within a defined block
# TODO: test if this breaks (and we should check rathar than except)
logging.debug(f"Failed to constrain max_shifts_per_month for worker: {worker.name}")
logging.debug(
f"Failed to constrain max_shifts_per_month for worker: {worker.name}"
)
pass
for constraint_dict in self.constraint_options["avoid_shifts_by_grades"]:
@@ -1213,7 +1255,9 @@ class RotaBuilder(object):
# Balance shifts within required limits (set by balance_offset)
# If balance_offset is too restrictive (for the rota length)
# no solution will be possible
for shift in track(self.get_shifts(), description="Generate shift balance constraints"):
for shift in track(
self.get_shifts(), description="Generate shift balance constraints"
):
if (
worker.site in shift.sites
): # Each site specfies which sites self.workers can fullfill it
@@ -1359,6 +1403,9 @@ class RotaBuilder(object):
# As the objective is to minimise t1+t2 and t1 and t2 are positive reals
# t1+t2 approximates the absolute target (which otherwise requires a quadratic solver)
# TODO: quadratic implementation so perfect solutions will be chosen
# NOTE: as this balances across the sum of the worker's shifts
# they can cancel each other out if a high weighting is given to multiple shifts
if self.constraint_options["balance_shifts_over_workers"]:
self.model.constraints.add(
self.model.worker_shift_count_t1[worker.id]
@@ -1548,10 +1595,15 @@ class RotaBuilder(object):
self.constraint_options["max_night_frequency"]
):
# Ignore excluded weeks
if set(week_blocks).intersection(set(self.constraint_options["max_night_frequency_week_exclusions"])):
if set(week_blocks).intersection(
set(
self.constraint_options[
"max_night_frequency_week_exclusions"
]
)
):
continue
if self.get_shifts_with_constraint("night"):
# Prevent nights more than once every n weeks
self.model.constraints.add(
@@ -1834,7 +1886,9 @@ class RotaBuilder(object):
# def get_pre_may(week_days, max_pre)
for n in track(range(len(weeks_days)), description="Generate week/day constraints"):
for n in track(
range(len(weeks_days)), description="Generate week/day constraints"
):
week, day = weeks_days[n]
@@ -2039,6 +2093,9 @@ class RotaBuilder(object):
)
)
# NOTE: due to the way that this is implemented, if a
# shift spans 7 days you may get >7 allocations in a row
# as it only checks for a different shift allocation
for constraint_shift in self.get_shifts_with_constraints(
"pre",
):
@@ -2464,7 +2521,7 @@ class RotaBuilder(object):
"""
self.shifts.append(shift)
def add_shifts(self, *shifts: SingleShift ) -> None:
def add_shifts(self, *shifts: SingleShift) -> None:
"""Add multiple shifts
Returns:
@@ -2605,7 +2662,7 @@ class RotaBuilder(object):
Returns:
list: contains tuple of all possible week / day / shift combinations
"""
"""
week_day_shifts = self.week_day_shift_product
if week is not None:
@@ -2968,6 +3025,13 @@ class RotaBuilder(object):
timetable = []
if self.run_start_time is not None and self.run_end_time is not None:
timetable.append(
f"<div>Start time: <span id='run_start_time'>{self.run_start_time:%Y-%m-%d %H:%M:%S%z}</span></div>"
f"<div>End time: <span id='run_end_time'>{self.run_end_time:%Y-%m-%d %H:%M:%S%z}</span></div>"
f"<div>Elapsed: <span id='run_elapsed_time'>{self.run_end_time-self.run_start_time}</span></div>"
)
timetable.append(
f"<h2>Rota start date: {self.start_date.isoformat()} ({self.weeks[-1]} weeks)</h2>"
)
+1 -1
View File
@@ -77,7 +77,7 @@ class Worker(BaseModel):
not_available_to_work: list[NotAvailableToWork] = []
pref_not_to_work: list[PreferenceNotToWork] = []
work_requests: list[WorkRequests] = []
remote_site: str = ("plymouth",) # We set a default proc_site
remote_site: str = "plymouth" # We set a default proc_site
previous_shifts: dict = {}
shift_balance_extra: dict = {}
bank_holiday_extra: int = 0
+2 -6
View File
@@ -1,7 +1,3 @@
parallel/minnthreads = 5
parallel/maxnthreads = 10
#limits/gap = 0.005
limits/solutions = -1
limits/gap = 0
limits/time = 20000
#limits/time = 100
+15 -13
View File
@@ -1,5 +1,6 @@
import datetime
import pytest
from pytest import approx
from rota.shifts import NoWorkers, RotaBuilder, SingleShift, days
from rota.workers import Worker
@@ -61,9 +62,9 @@ class TestBalancing:
for worker_name in shift_summary:
worker_shifts = shift_summary[worker_name]
worker = Rota.get_worker_by_name(worker_name)
assert worker_shifts["a"] in (22, 23, 24, 25)
assert worker_shifts["b"] in (22, 23, 24, 25)
#worker = Rota.get_worker_by_name(worker_name)
assert worker_shifts["a"] in (approx(22), approx(23), approx(24), approx(25))
assert worker_shifts["b"] in (approx(22), approx(23), approx(24), approx(25))
def test_weighted_shift_balancing(self):
Rota = generate_basic_rota(20)
@@ -74,10 +75,10 @@ class TestBalancing:
name="a",
length=12.5,
days=days,
balance_weighting=4,
balance_weighting=5,
workers_required=1,
force_as_block=False,
constraint=[{"name": "preclear2"}, {"name": "postclear2"}],
constraint=[{"name": "pre","options": "2"}, {"name": "post","options": "2"}],
),
SingleShift(
sites=("group1", "group2"),
@@ -105,7 +106,8 @@ class TestBalancing:
worker_shifts = shift_summary[worker_name]
worker = Rota.get_worker_by_name(worker_name)
assert worker_shifts["a"] in (46, 47, 48)
assert worker_shifts["a"] in (approx(46), approx(47), approx(48))
#assert worker_shifts["b"] in (46, 47, 48)
# assert worker_shifts["b"] in (22, 23, 24, 25)
def test_weighted_shift_balancing2(self):
@@ -127,7 +129,7 @@ class TestBalancing:
name="b",
length=12.5,
days=days,
balance_weighting=4,
balance_weighting=8,
workers_required=1,
force_as_block=False,
),
@@ -141,7 +143,7 @@ class TestBalancing:
),
)
Rota.build_and_solve(options={"ratio": 0.001})
Rota.build_and_solve(options={"ratio": 0.000})
Rota.export_rota_to_html("basic_balancing_weighted_shifts2")
shift_summary = Rota.get_shift_summary_dict()
@@ -151,7 +153,7 @@ class TestBalancing:
worker = Rota.get_worker_by_name(worker_name)
# assert worker_shifts["a"] in (46,47,48)
# assert worker_shifts["c"] in (46,47,48)
assert worker_shifts["b"] in (52, 53, 54, 55)
assert worker_shifts["b"] in (approx(53), approx(54), approx(55))
def test_weighted_shift_balancing3(self):
Rota = generate_basic_rota(23)
@@ -172,7 +174,7 @@ class TestBalancing:
name="b",
length=12.5,
days=days,
balance_weighting=4,
balance_weighting=1,
workers_required=1,
force_as_block=False,
),
@@ -181,13 +183,13 @@ class TestBalancing:
name="c",
length=12.5,
days=days[0],
balance_weighting=4,
balance_weighting=8,
workers_required=1,
force_as_block=False,
),
)
Rota.build_and_solve(options={"ratio": 0.000})
Rota.build_and_solve(options={"ratio": 0.0001})
Rota.export_rota_to_html("basic_balancing_weighted_shifts3")
shift_summary = Rota.get_shift_summary_dict()
@@ -197,7 +199,7 @@ class TestBalancing:
worker = Rota.get_worker_by_name(worker_name)
# assert worker_shifts["a"] in (46,47,48)
assert worker_shifts["c"] in (7, 8)
assert worker_shifts["b"] in (52, 53, 54, 55)
#assert worker_shifts["b"] in (53, 54)
def test_weighted_shift_balancing4(self):
Rota = generate_basic_rota(10)
+58
View File
@@ -0,0 +1,58 @@
from sqlalchemy.orm import Session
import models, schemas
def get_shift(db: Session, shift_id: int):
return (
db.query(models.SingleShift).filter(models.SingleShift.id == shift_id).first()
)
def get_shift_by_name(db: Session, name: str):
return db.query(models.SingleShift).filter(models.SingleShift.name == name).first()
def get_shifts(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.SingleShift).offset(skip).limit(limit).all()
def create_shift(db: Session, shift: schemas.SingleShiftCreate):
shift = models.SingleShift(**shift.dict())
db.add(shift)
db.commit()
db.refresh(shift)
return shift
def create_worker(db: Session, worker: schemas.WorkerCreate):
worker = models.Worker(**worker.dict())
db.add(worker)
db.commit()
db.refresh(worker)
return worker
def get_worker(db: Session, worker_id: int):
return (
db.query(models.Worker).filter(models.Worker.id == worker_id).first()
)
def get_worker_by_name(db: Session, name: str):
return db.query(models.Worker).filter(models.Worker.name == name).first()
def get_workers(db: Session, skip: int = 0, limit: int = 100):
return db.query(models.Worker).offset(skip).limit(limit).all()
#def get_shift_constraints(db: Session, skip: int = 0, limit: int = 100):
# return db.query(models.ShiftConstraint).offset(skip).limit(limit).all()
#
#
#def create_shift_constraints(
# db: Session, item: schemas.ShiftConstraintCreate, constraint_id: int
#):
# db_item = models.ShiftConstraint(**item.dict(), constraint_id=constraint_id)
# db.add(db_item)
# db.commit()
# db.refresh(db_item)
# return db_item
+15
View File
@@ -0,0 +1,15 @@
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db"
engine = create_engine(
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
+87 -14
View File
@@ -1,16 +1,21 @@
import uvicorn
from fastapi import FastAPI, Query
from pydantic import BaseModel
from fastapi_sqlalchemy import DBSessionMiddleware, db
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import Session
import os
from dotenv import load_dotenv
from rota.shifts import SingleShift, ShiftConstraint
from web.models import SingleShift as ModelSingleShift
from web.models import ShiftConstraint as ModelShiftConstraint
from models import SingleShift as ModelSingleShift
#from models import ShiftConstraint as ModelShiftConstraint
from database import SessionLocal, engine
import crud, models, schemas
#from rota.wokers
@@ -19,21 +24,89 @@ load_dotenv('../.env')
app = FastAPI()
app.add_middleware(DBSessionMiddleware, db_url="sqlite:///test.db")
# Dependency
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.post('/shifts/', response_model=SingleShift)
async def shifts(shift: ModelSingleShift):
db_shift = ModelSingleShift(name=shift.name, length=shift.length)
db.session.add(db_shift)
db.session.commit()
#app.add_middleware(DBSessionMiddleware, db_url="sqlite:///test.db")
models.Base.metadata.create_all(bind=engine)
@app.post("/shifts/", response_model=schemas.SingleShift)
def create_shift(shift: schemas.SingleShiftCreate, db: Session = Depends(get_db)):
db_shift = crud.get_shift_by_name(db, name=shift.name)
if db_shift:
raise HTTPException(status_code=400, detail="Shift name already exists")
return crud.create_shift(db=db, shift=shift)
@app.get("/shifts/", response_model=list[schemas.SingleShift])
def read_shifts(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
shifts = crud.get_shifts(db, skip=skip, limit=limit)
return shifts
@app.get("/shifts/{shift_id}", response_model=schemas.SingleShift)
def read_shift(shift_id: int, db: Session = Depends(get_db)):
db_shift = crud.get_shifts(db, shift_id=shift_id)
if db_shift is None:
raise HTTPException(status_code=404, detail="Shift not found")
return db_shift
@app.get("/shifts/")
async def shifts():
shifts = db.session.query(ModelSingleShift).all()
@app.post("/workers/", response_model=schemas.Worker)
def create_worker(worker: schemas.WorkerCreate, db: Session = Depends(get_db)):
db_worker = crud.get_worker_by_name(db, name=worker.name)
if db_worker:
raise HTTPException(status_code=400, detail="worker name already exists")
return crud.create_worker(db=db, worker=worker)
return shifts
@app.get("/workers/", response_model=list[schemas.Worker])
def read_workers(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
workers = crud.get_workers(db, skip=skip, limit=limit)
return workers
@app.get("/workers/{worker_id}", response_model=schemas.Worker)
def read_worker(worker_id: int, db: Session = Depends(get_db)):
db_worker = crud.get_workers(db, worker_id=worker_id)
if db_worker is None:
raise HTTPException(status_code=404, detail="worker not found")
return db_worker
#@app.post("/users/{user_id}/items/", response_model=schemas.Item)
#def create_item_for_user(
# user_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db)
#):
# return crud.create_user_item(db=db, item=item, user_id=user_id)
#
#
#@app.get("/items/", response_model=list[schemas.Item])
#def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
# items = crud.get_items(db, skip=skip, limit=limit)
# return items
#@app.post("/shifts/")
#async def create_shift(shift: SingleShift):
# return shift
#@app.post('/shifts/', response_model=SingleShift)
#async def shifts(shift: ModelSingleShift):
# db_shift = ModelSingleShift(name=shift.name, length=shift.length)
# db.session.add(db_shift)
# db.session.commit()
# return db_shift
#@app.get("/shifts/")
#async def shifts():
# shifts = db.session.query(ModelSingleShift).all()
#
# return shifts
@app.get("/")
+65 -12
View File
@@ -1,31 +1,84 @@
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Float, Boolean
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import (
Column,
DateTime,
ForeignKey,
Integer,
String,
Float,
Boolean,
JSON,
)
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
Base = declarative_base()
from database import Base
class Worker(Base):
__tablename__ = "worker"
name= Column(String)
site= Column(String)
grade= Column(Integer)
id=Column(Integer, primary_key=True, index=True)
fte= Column(Integer)
nwds= Column(JSON)
start_date= Column(DateTime)
end_date= Column(DateTime)
oop= Column(JSON)
not_available_to_work= Column(JSON)
pref_not_to_work= Column(JSON)
work_requests= Column(JSON)
remote_site= Column(String)
previous_shifts= Column(JSON)
shift_balance_extra=Column(JSON)
bank_holiday_extra= Column(Integer)
pair= Column(String)
class SingleShift(Base):
__tablename__ = 'single_shift'
id = Column(Integer, primary_key=True, index=True)
__tablename__ = "single_shift"
id = Column(Integer, primary_key=True, index=True)
name = Column(String)
length = Column(Float)
days = Column(JSON)
sites = Column(JSON)
balance_offset = Column(Float, nullable=True)
balance_weighting = Column(Float)
workers_required = Column(Float)
rota_on_nwds = Column(Boolean)
assign_as_block = Column(Boolean)
force_as_block = Column(Boolean)
force_as_block_unless_nwd = Column(Boolean)
hard_constrain_shift = Column(Boolean)
bank_holidays_only = Column(Boolean)
constraint_id = Column(Integer, ForeignKey('shift_constraint.id'))
constraint = relationship('ShiftConstraint')
#constraint = relationship("ShiftConstraint", back_populates="shifts")
constraint = Column(JSON)
# This should never be used from here (automatically created by the pydantic model)
constraint_options = Column(JSON)
constraints = Column(JSON)
#sites = relationship("Site", secondary="site_shift_link_table")
#class ShiftConstraint(Base):
# __tablename__ = "shift_constraint"
# id = Column(Integer, primary_key=True)
# name = Column(String)
# options = Column(Integer, nullable=True)
#
# shifts = relationship("SingleShift", back_populates="constraint")
#
# shift_id = Column(Integer, ForeignKey("single_shift.id"))
class ShiftConstraint(Base):
__tablename__ = 'shift_constraint'
id = Column(Integer, primary_key=True)
name = Column(String)
options = Column(Integer, nullable=True)
#class SiteShiftLinkTable(Base):
# __tablename__ = "site_shift_link_table"
# shift_id = Column(Integer, ForeignKey("single_shift.id"), primary_key=True)
# site_id = Column(Integer, ForeignKey("sites.id"), primary_key=True)
#class Site(Base):
# __tablename__ = "sites"
# id = Column(Integer, primary_key=True)
# name = Column(String)
#
# shifts = relationship(SingleShift, secondary="site_shift_link_table")
+32
View File
@@ -0,0 +1,32 @@
from rota.shifts import SingleShift as SingleShiftBase, ShiftConstraint as ShiftConstraintBase
from rota.workers import Worker as WorkerBase
class SingleShift(SingleShiftBase):
id: int
class Config:
orm_mode = True
class SingleShiftCreate(SingleShiftBase):
pass
#class ShiftConstraint(ShiftConstraintBase):
# id: int
#
# class Config:
# orm_mode = True
#
#class ShiftConstraintCreate(ShiftConstraintBase):
# pass
class Worker(WorkerBase):
pass
class Config:
orm_mode = True
class WorkerCreate(WorkerBase):
pass