Skip to content

Commit

Permalink
Merge pull request #4 from DurianDan/add_contributing_docs_fix_pyshif…
Browse files Browse the repository at this point in the history
…tsla

Resolve ShiftBuilder > add_days_off_range, Fix guides in README, and add CONTRIBUTING docs.
  • Loading branch information
DurianDan authored Dec 11, 2024
2 parents d48a162 + 0778144 commit 7a51d6e
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 30 deletions.
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)

0 comments on commit 7a51d6e

Please sign in to comment.