From 07781442d13b49c390a15b48d343a870fce2c25c Mon Sep 17 00:00:00 2001 From: huynv44 Date: Wed, 11 Dec 2024 16:08:30 +0700 Subject: [PATCH] Update: add tests for all showcases in the README.md, README.md > fix code syntaxes, ShiftBuilder > shifts_daily > rename to daily_shifts, ShiftBuilder > add_days_off_range> add a list of dates, instead of single date. Feature: CONTRIBUTING.md > docs to guide through the development andpublishing process --- CONTRIBUTING.md | 18 ++++ README.md | 21 +++-- pyshiftsla/daily_shifts.py | 3 + pyshiftsla/shifts_builder.py | 36 ++++---- tests/test_readme_showcases.py | 164 +++++++++++++++++++++++++++++++++ tests/test_sla_calculation.py | 2 +- 6 files changed, 214 insertions(+), 30 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 tests/test_readme_showcases.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..555ba97 --- /dev/null +++ b/CONTRIBUTING.md @@ -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 +``` \ No newline at end of file diff --git a/README.md b/README.md index df617bd..1c1ce74 100644 --- a/README.md +++ b/README.md @@ -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( @@ -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"), @@ -76,17 +77,17 @@ 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 ``` @@ -94,7 +95,7 @@ generated_shiftrange.get(date(2024,7,4)) # None - 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), @@ -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( @@ -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` diff --git a/pyshiftsla/daily_shifts.py b/pyshiftsla/daily_shifts.py index 0d24f9a..a58c056 100644 --- a/pyshiftsla/daily_shifts.py +++ b/pyshiftsla/daily_shifts.py @@ -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) diff --git a/pyshiftsla/shifts_builder.py b/pyshiftsla/shifts_builder.py index c583bc8..7757e1d 100644 --- a/pyshiftsla/shifts_builder.py +++ b/pyshiftsla/shifts_builder.py @@ -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({}) @@ -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, @@ -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 diff --git a/tests/test_readme_showcases.py b/tests/test_readme_showcases.py new file mode 100644 index 0000000..36a5958 --- /dev/null +++ b/tests/test_readme_showcases.py @@ -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), + ] diff --git a/tests/test_sla_calculation.py b/tests/test_sla_calculation.py index 6dd8ddf..5ca14fd 100644 --- a/tests/test_sla_calculation.py +++ b/tests/test_sla_calculation.py @@ -3,4 +3,4 @@ def test_default_shiftsbuilder(): default = ShiftsBuilder() - assert isinstance(default.shifts_daily, DailyShift) + assert isinstance(default.daily_shifts, DailyShift)