Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve ShiftBuilder > add_days_off_range, Fix guides in README, and add CONTRIBUTING docs. #4

Merged
merged 1 commit into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 18 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Contributing to pyshiftsla
Thank you for your interest in contributing! Here's how you can help:

## Development setup
1. Clone the repository
2. Publish your branch
3. Install dependencies with [poetry](https://python-poetry.org/docs/): `poetry install`
4. Make some local changes
5. Test with `pytest -v`
6. Run pre commit hooks: `pre-commit run --all`
7. Push to your branch
8. Make Pull request

## To Publish To PyPi
```bash
source .env
poetry publish --build --username $PIPY_USERNAME --password $PIPY_PASSWORD
```
21 changes: 11 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@

#### 2. Configuration for `ShiftsBuilder`
```python
from pysla.shifts_builder import ShiftsBuilder, Shift, DateRange
from pyshiftsla.shifts_builder import ShiftsBuilder, DateRange, DailyShift, ShiftRange
from pyshiftsla.shift import Shift
from datetime import time, date

US_WOMAN_LIVING_IN_VIETNAM_MATERNITY_LEAVE_4MONTHS_2024 = ShiftsBuilder(
Expand All @@ -43,9 +44,9 @@ US_WOMAN_LIVING_IN_VIETNAM_MATERNITY_LEAVE_4MONTHS_2024 = ShiftsBuilder(
DateRange.fromstr("20240902-20240903"), #VIETNAM_INDEPENDENCE_DAY
#VIETNAMESE_LUNAR_NEW_YEAR,
# Lunar DateRange will automatically turn into Solar DateRange
DateRange.fromstr("20240101-20240105", calendar_type="lunar")
DateRange.fromstr("20240101-20240105", calendar_type="lunar"),
#VIETNAM_LUNAR_HUNG_KINGS_FESTIVAL
DateRange.fromstr("20240310", calendar_type="lunar")
DateRange.fromstr("20240310", calendar_type="lunar"),

# 4 months of maternity leave
DateRange.fromstr("20240801-20241201"),
Expand Down Expand Up @@ -76,25 +77,25 @@ generated_shiftrange = ( # ShiftRange for 2024
)
# Get Shifts in New Year 2024-01-01
generated_shiftrange[date(2024,1,1)]
# [Shift(start=datetime.time(13, 30), end=datetime.time(14, 30))]
# [Shift(start=time(13, 30), end=time(14, 30))]

# Get Shifts in a random working day
generated_shiftrange[date(2024,2,8)]
# [
# Shift(start=datetime.time(8, 30), end=datetime.time(11, 45)),
# Shift(start=datetime.time(13, 30), end=datetime.time(18, 0))
# Shift(start=time(8, 30), end=time(11, 45)),
# Shift(start=time(13, 30), end=time(18, 0))
# ]

# Get Shifts in a day off (holiday leave) 2024-07-04
generated_shiftrange[date(2024,7,4)] # KeyError: datetime.date(2024, 7, 4)
generated_shiftrange[date(2024,7,4)] # KeyError: date(2024, 7, 4)
generated_shiftrange.get(date(2024,7,4)) # None
```

#### 4. Calculate SLA (service-level agreement)
- The result will be in `Milliseconds`:
```python
sla_millis = (
US_WOMAN_lIVING_IN_VIETNAM_MATERNITY_LEAVE_4MONTHS_2024
US_WOMAN_LIVING_IN_VIETNAM_MATERNITY_LEAVE_4MONTHS_2024
.calculate_sla(
start_deal=datetime(2024, 1, 1, 14),
end_deal=datetime(2024, 1, 2, 9, 30),
Expand All @@ -109,7 +110,7 @@ sla_hours = sla_millis/(1000*60*60) # 1.5 hours

### Global `ShiftsBuilder` config for your company/team
```python
from pysla.shifts_builder import ShiftsBuilder, Shift
from pyshiftsla.shifts_builder import ShiftsBuilder, Shift
from datetime import time

COMPANY_SHIFTS_BUILDER = ShiftsBuilder(
Expand All @@ -127,7 +128,7 @@ employee_takes_4_days_off = (
.add_days_off_range(
[
DateRange.fromstr("20241204-20241205"), # 2days off
DateRange.fromstr("20241000-20241001") # 2days off
DateRange.fromstr("20240930-20241001") # 2days off
],
inplace=False # if `False` will return a copied `ShiftsBuilder`,
# if `True`, will change `COMPANY_SHIFTS_BUILDER` and return `None`
Expand Down
3 changes: 3 additions & 0 deletions pyshiftsla/daily_shifts.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ def __getitem__(self, idx: int) -> Shift | None:
)
return self.root[idx]

def get_shifts_num(self) -> int:
return len(self.root)

@classmethod
def sorted_shifts_start(cls, shifts: List[Shift]) -> SORTED_SHIFTS_START:
sorted_shifts = sorted(shifts, key=lambda x: x.start)
Expand Down
36 changes: 17 additions & 19 deletions pyshiftsla/shifts_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@ class ShiftsBuilder(BaseModel):
"""
`Shifts` configuration for a single `employee/team/firm`. Use method `build_shifts_from_daterange` for generating `Shift`s based on parsed config. Use method `calculate_sla` for calculating sla based on generated `Shift`s

To generate shifts, the order of priorities is `special_shifts` > `days_off` > `shifts_daily` + `workdays_weekly`
To generate shifts, the order of priorities is `special_shifts` > `days_off` > `daily_shifts` + `workdays_weekly`

:param workdays_weekly: indexes of work days in a week, default is from Monday to Friday [0,1,2,3,4]
:param shifts_daily: default `Shifts` in a typical workday.
:param daily_shifts: default `Shifts` in a typical workday.
:param days_off: List of days off, can be *lunar* or *solar* days off
:param special_shifts: special `Shifts` of a *specific date*
"""

workdays_weekly: WEEKDAYS = COMMON_WORKDAYS_IN_WEEK
shifts_daily: DailyShift = COMMON_DAILY_SHIFTS
daily_shifts: DailyShift = COMMON_DAILY_SHIFTS
days_off_ranges: List[DateRange | date] = []
special_shifts: ShiftRange = ShiftRange({})

Expand Down Expand Up @@ -82,42 +82,40 @@ def get_days_off(self) -> Set[date]:
def partial_config_copy(
self,
workdays_weekly: WEEKDAYS | None = None,
shifts_daily: DailyShift | None = None,
daily_shifts: DailyShift | None = None,
days_off_ranges: List[DateRange | date] | None = None,
special_shifts: ShiftRange | None = None,
) -> "ShiftsBuilder":
return ShiftsBuilder(
shifts_daily=shifts_daily if shifts_daily else self.shifts_daily,
days_off_ranges=days_off_ranges
if days_off_ranges
else self.days_off_ranges,
workdays_weekly=workdays_weekly
if workdays_weekly
else self.workdays_weekly,
daily_shifts=daily_shifts if daily_shifts else self.daily_shifts,
days_off_ranges=(
days_off_ranges if days_off_ranges else self.days_off_ranges
),
workdays_weekly=(
workdays_weekly if workdays_weekly else self.workdays_weekly
),
special_shifts=special_shifts
if special_shifts
else self.special_shifts,
)

def add_days_off_range(
self, days_off_range: DateRange | date, inplace: bool = False
self, days_off_range: List[DateRange | date], inplace: bool = False
) -> Optional["ShiftsBuilder"]:
if inplace:
self.days_off_ranges.append(days_off_range)
self.days_off_ranges.extend(days_off_range)
return
return self.partial_config_copy(
days_off_ranges=self.days_off_ranges + [days_off_range]
days_off_ranges=self.days_off_ranges + days_off_range
)

def update_workday_weekly(
self, workdays: WEEKDAYS, inplace: bool = False
) -> Optional["ShiftsBuilder"]:
if inplace:
self.workdays_weekly.update(workdays)
self.workdays_weekly = workdays
return
return self.partial_config_copy(
workdays_weekly=self.workdays_weekly.union(workdays)
)
return self.partial_config_copy(workdays_weekly=workdays)

def update_special_shifts(
self,
Expand Down Expand Up @@ -151,7 +149,7 @@ def build_shifts_from_daterange(
"""
workdays = self.get_workdays(from_date, to_date)
self._generated_shifts = ShiftRange(
{workday: self.shifts_daily for workday in workdays}
{workday: self.daily_shifts for workday in workdays}
)
self._generated_shifts.update(self.special_shifts)
return self._generated_shifts
Expand Down
164 changes: 164 additions & 0 deletions tests/test_readme_showcases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
from pyshiftsla.shifts_builder import (
ShiftsBuilder,
DateRange,
DailyShift,
ShiftRange,
)
from pyshiftsla.shift import Shift
from datetime import time, date, datetime
import pytest


def test_readme_first_showcase_us_woman():
US_WOMAN_LIVING_IN_VIETNAM_MATERNITY_LEAVE_4MONTHS_2024 = ShiftsBuilder(
workdays_weekly=[0, 1, 2, 3, 4], # weekday indexes: Monday to Friday
daily_shifts=DailyShift(
[
# Usual Morning shift: 8:30 to 11:45
# Created using a string, easier to read but slower
Shift.fromstr("08301145"),
# Usual Afternoon shift: 13:30 to 18:00
# Parsed the timestamps straight to parameters, faster
Shift(start=time(13, 30), end=time(18)),
]
),
days_off_ranges=[ # you can parse `date` or `DateRange`
date(2024, 1, 1), # SOLAR_NEW_YEAR
date(2024, 7, 4), # US_INDEPENDENCE_DAY
DateRange.fromstr("20240430-20240501"), # VIETNAM_VICTORY_DAY
DateRange.fromstr("20240902-20240903"), # VIETNAM_INDEPENDENCE_DAY
# VIETNAMESE_LUNAR_NEW_YEAR,
# Lunar DateRange will automatically turn into Solar DateRange
DateRange.fromstr("20240101-20240105", calendar_type="lunar"),
# VIETNAM_LUNAR_HUNG_KINGS_FESTIVAL
DateRange.fromstr("20240310", calendar_type="lunar"),
# 4 months of maternity leave
DateRange.fromstr("20240801-20241201"),
date(2024, 2, 9), # custom days off
date(2024, 6, 3), # custom days off
date(2024, 12, 10), # custom days off
],
special_shifts=ShiftRange(
{
# Urgent overtime in the Solar New Year
# 13:30 to 14:30
date(2024, 1, 1): DailyShift([Shift.fromstr("13301430")]),
}
),
)
generated_shiftrange = ( # ShiftRange for 2024
US_WOMAN_LIVING_IN_VIETNAM_MATERNITY_LEAVE_4MONTHS_2024.build_shifts_from_daterange(
from_date=date(2024, 1, 1), to_date=date(2024, 12, 30)
)
)
# Get Shifts in New Year 2024-01-01
new_years_date = generated_shiftrange[date(2024, 1, 1)]
assert new_years_date.get_shifts_num() == 1
assert (
new_years_date[0].compare(Shift(start=time(13, 30), end=time(14, 30)))
== "equal"
)

random_working_day = generated_shiftrange[date(2024, 2, 8)]
assert random_working_day.get_shifts_num() == 2
assert (
random_working_day[0].compare(
Shift(start=time(8, 30), end=time(11, 45))
)
== "equal"
)
assert (
random_working_day[1].compare(
Shift(start=time(13, 30), end=time(18, 0))
)
== "equal"
)

# Get Shifts in a day off (holiday leave) 2024-07-04
with pytest.raises(KeyError):
# KeyError: datetime.date(2024, 7, 4)
generated_shiftrange[date(2024, 7, 4)]


def test_readme_second_showcase_us_woman_sla():
US_WOMAN_LIVING_IN_VIETNAM_MATERNITY_LEAVE_4MONTHS_2024 = ShiftsBuilder(
workdays_weekly=[0, 1, 2, 3, 4], # weekday indexes: Monday to Friday
daily_shifts=DailyShift(
[
# Usual Morning shift: 8:30 to 11:45
# Created using a string, easier to read but slower
Shift.fromstr("08301145"),
# Usual Afternoon shift: 13:30 to 18:00
# Parsed the timestamps straight to parameters, faster
Shift(start=time(13, 30), end=time(18)),
]
),
days_off_ranges=[ # you can parse `date` or `DateRange`
date(2024, 1, 1), # SOLAR_NEW_YEAR
date(2024, 7, 4), # US_INDEPENDENCE_DAY
DateRange.fromstr("20240430-20240501"), # VIETNAM_VICTORY_DAY
DateRange.fromstr("20240902-20240903"), # VIETNAM_INDEPENDENCE_DAY
# VIETNAMESE_LUNAR_NEW_YEAR,
# Lunar DateRange will automatically turn into Solar DateRange
DateRange.fromstr("20240101-20240105", calendar_type="lunar"),
# VIETNAM_LUNAR_HUNG_KINGS_FESTIVAL
DateRange.fromstr("20240310", calendar_type="lunar"),
# 4 months of maternity leave
DateRange.fromstr("20240801-20241201"),
date(2024, 2, 9), # custom days off
date(2024, 6, 3), # custom days off
date(2024, 12, 10), # custom days off
],
special_shifts=ShiftRange(
{
# Urgent overtime in the Solar New Year
# 13:30 to 14:30
date(2024, 1, 1): DailyShift([Shift.fromstr("13301430")]),
}
),
)
sla_millis = (
US_WOMAN_LIVING_IN_VIETNAM_MATERNITY_LEAVE_4MONTHS_2024.calculate_sla(
start_deal=datetime(2024, 1, 1, 14),
end_deal=datetime(2024, 1, 2, 9, 30),
use_generated_shifts=True, # for faster execution,
# reuse the cached results from `build_shifts_from_daterange`,
# if "False", set re-generated `ShiftsRange` from `start_deal` to `end_deal`
)
)
assert sla_millis / (1000 * 60 * 60) == 1.5


def test_readme_third_showcase_global_shift_builder():
COMPANY_SHIFTS_BUILDER = ShiftsBuilder(
daily_shifts=DailyShift(
[
Shift.fromstr(
"08301145"
), # morning shift, created using a string, easier to read but slower
Shift(
start=time(13, 30), end=time(18)
), # afternoon shift, parsed the timestamps straight to parameters, faster
]
),
workdays_weekly=[0, 1, 2, 3, 4], # Monday to Friday
)
employee_takes_4_days_off = COMPANY_SHIFTS_BUILDER.add_days_off_range(
[
DateRange.fromstr("20241204-20241205"), # 2days off
DateRange.fromstr("20240930-20241001"), # 2days off
],
inplace=False, # if `False` will return a copied `ShiftsBuilder`,
# if `True`, will change `COMPANY_SHIFTS_BUILDER` and return `None`
)
assert len(COMPANY_SHIFTS_BUILDER.days_off_ranges) == 0
assert employee_takes_4_days_off is not None
assert len(employee_takes_4_days_off.days_off_ranges) == 2
assert employee_takes_4_days_off.days_off_ranges[0].dates == [
date(2024, 12, 4),
date(2024, 12, 5),
]
assert employee_takes_4_days_off.days_off_ranges[1].dates == [
date(2024, 9, 30),
date(2024, 10, 1),
]
2 changes: 1 addition & 1 deletion tests/test_sla_calculation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@

def test_default_shiftsbuilder():
default = ShiftsBuilder()
assert isinstance(default.shifts_daily, DailyShift)
assert isinstance(default.daily_shifts, DailyShift)
Loading