44import collections
55import dataclasses
66import datetime
7+ import itertools
78import logging
9+ import math
810from pathlib import Path
911from typing import Iterator
1012
@@ -77,6 +79,30 @@ def generate_additional_rotations(
7779 least_recent_assignees .extend (people_on_this_rotation )
7880
7981
82+ def calculate_rotations_to_cover (
83+ when : datetime .datetime ,
84+ rotation_length_weeks : int ,
85+ current_rotations : list [rotations .Rotation ],
86+ ) -> int :
87+ """Calculates how many additional rotations are needed to cover up to `when`."""
88+ if not current_rotations :
89+ raise ValueError ("No existing rotations to extend from." )
90+
91+ last_rotation = current_rotations [- 1 ]
92+ rotation_length = datetime .timedelta (weeks = rotation_length_weeks )
93+ end_of_last_rotation = last_rotation .start_time + rotation_length
94+
95+ if end_of_last_rotation >= when :
96+ return 0
97+
98+ delta = when - end_of_last_rotation
99+ # Use total seconds to account for any non-day components of `delta`.
100+ days_needed = math .ceil (delta .total_seconds () / (24 * 60 * 60 ))
101+ weeks_needed = days_needed / 7
102+ rotations_needed = int (math .ceil (weeks_needed / rotation_length_weeks ))
103+ return rotations_needed
104+
105+
80106def parse_args () -> argparse .Namespace :
81107 parser = argparse .ArgumentParser (
82108 description = "Extend a rotation with additional members."
@@ -107,12 +133,6 @@ def parse_args() -> argparse.Namespace:
107133 than overwriting the existing file.
108134 """ ,
109135 )
110- parser .add_argument (
111- "--num-rotations" ,
112- type = int ,
113- default = 5 ,
114- help = "Number of rotations to add. Default is %(default)d." ,
115- )
116136 parser .add_argument (
117137 "--people-per-rotation" ,
118138 type = int ,
@@ -124,6 +144,21 @@ def parse_args() -> argparse.Namespace:
124144 action = "store_true" ,
125145 help = "Enable debug logging." ,
126146 )
147+
148+ rotation_group = parser .add_mutually_exclusive_group ()
149+ rotation_group .add_argument (
150+ "--num-rotations" ,
151+ type = int ,
152+ help = "Number of rotations to add, defaults to 5." ,
153+ )
154+ rotation_group .add_argument (
155+ "--ensure-weeks" ,
156+ type = int ,
157+ help = """
158+ Ensure the rotation schedule covers at least this many weeks into
159+ the future.
160+ """ ,
161+ )
127162 return parser .parse_args ()
128163
129164
@@ -136,24 +171,50 @@ def main() -> None:
136171 )
137172
138173 dry_run : bool = opts .dry_run
139- num_rotations : int = opts .num_rotations
140174 people_per_rotation : int = opts .people_per_rotation
141175 rotation_file_path : Path = opts .rotation_file
142176 rotation_length_weeks : int = opts .rotation_length_weeks
143177 rotation_members_file_path : Path = opts .rotation_members_file
144178
179+ now = datetime .datetime .now (tz = datetime .timezone .utc )
145180 members_file = rotations .RotationMembersFile .parse_file (rotation_members_file_path )
146181 current_rotation = rotations .RotationFile .parse_file (rotation_file_path )
147182
183+ # Determine number of rotations based on flags
184+ if opts .num_rotations is not None :
185+ num_rotations_to_add : int = opts .num_rotations
186+ elif opts .ensure_weeks is not None :
187+ ensure_weeks : int = opts .ensure_weeks
188+ num_rotations_to_add = calculate_rotations_to_cover (
189+ now + datetime .timedelta (weeks = ensure_weeks ),
190+ rotation_length_weeks ,
191+ current_rotation .rotations ,
192+ )
193+ if num_rotations_to_add == 0 :
194+ logging .info (
195+ "Current rotations already cover the next %d weeks; no new rotations needed." ,
196+ ensure_weeks ,
197+ )
198+ return
199+ logging .info (
200+ "Ensuring %d weeks of coverage with %d rotations (%d weeks per rotation)" ,
201+ ensure_weeks ,
202+ num_rotations_to_add ,
203+ rotation_length_weeks ,
204+ )
205+ else :
206+ # Default to 5 rotations if neither flag is specified
207+ num_rotations_to_add = 5
208+
148209 rotation_generator = generate_additional_rotations (
149210 current_rotation .rotations ,
150211 members_file .members ,
151212 people_per_rotation ,
152213 rotation_length_weeks ,
153- now = datetime . datetime . now ( tz = datetime . timezone . utc ) ,
214+ now = now ,
154215 )
155216
156- extra_rotations = [ x for x , _ in zip ( rotation_generator , range ( num_rotations ))]
217+ extra_rotations = list ( itertools . islice ( rotation_generator , num_rotations_to_add ))
157218 new_rotations = current_rotation .rotations + extra_rotations
158219 new_rotations_file = dataclasses .replace (current_rotation , rotations = new_rotations )
159220
0 commit comments