Refactor datetime and timezone utils
This commit is contained in:
157
shared/dt_utils/business.py
Normal file
157
shared/dt_utils/business.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
Business Domain DateTime Logic
|
||||
|
||||
Business-specific datetime operations including business hours,
|
||||
business days, and tenant-specific datetime handling.
|
||||
"""
|
||||
|
||||
from datetime import datetime, time, timedelta
|
||||
from typing import Optional
|
||||
from zoneinfo import ZoneInfo
|
||||
import structlog
|
||||
|
||||
from .constants import DEFAULT_TIMEZONE, BUSINESS_HOURS_DEFAULT
|
||||
from .timezone import get_current_datetime_in_tz, combine_date_time_in_tz, convert_from_utc
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
def is_business_hours(
|
||||
dt: Optional[datetime] = None,
|
||||
tz: str = DEFAULT_TIMEZONE,
|
||||
start_hour: int = BUSINESS_HOURS_DEFAULT["start_hour"],
|
||||
end_hour: int = BUSINESS_HOURS_DEFAULT["end_hour"],
|
||||
include_weekends: bool = BUSINESS_HOURS_DEFAULT["include_weekends"]
|
||||
) -> bool:
|
||||
"""
|
||||
Check if datetime is within business hours.
|
||||
|
||||
Args:
|
||||
dt: Datetime to check (defaults to now in timezone)
|
||||
tz: IANA timezone string
|
||||
start_hour: Business hours start (24h format)
|
||||
end_hour: Business hours end (24h format)
|
||||
include_weekends: Whether to include weekends as business days
|
||||
|
||||
Returns:
|
||||
True if within business hours, False otherwise
|
||||
"""
|
||||
if dt is None:
|
||||
dt = get_current_datetime_in_tz(tz)
|
||||
elif dt.tzinfo is None:
|
||||
zone = ZoneInfo(tz)
|
||||
dt = dt.replace(tzinfo=zone)
|
||||
else:
|
||||
dt = convert_from_utc(dt, tz)
|
||||
|
||||
if not include_weekends and dt.weekday() >= 5:
|
||||
return False
|
||||
|
||||
return start_hour <= dt.hour < end_hour
|
||||
|
||||
|
||||
def next_business_day(
|
||||
from_dt: Optional[datetime] = None,
|
||||
target_time: time = time(9, 0),
|
||||
tz: str = DEFAULT_TIMEZONE
|
||||
) -> datetime:
|
||||
"""
|
||||
Get next business day at specific time in timezone.
|
||||
|
||||
Args:
|
||||
from_dt: Starting datetime (defaults to now in timezone)
|
||||
target_time: Time to schedule (e.g., time(9, 0) for 9 AM)
|
||||
tz: IANA timezone string
|
||||
|
||||
Returns:
|
||||
Next business day at target_time in specified timezone
|
||||
"""
|
||||
if from_dt is None:
|
||||
current = get_current_datetime_in_tz(tz)
|
||||
else:
|
||||
current = convert_from_utc(from_dt, tz)
|
||||
|
||||
next_day = current.date()
|
||||
next_datetime = combine_date_time_in_tz(next_day, target_time, tz)
|
||||
|
||||
if current.time() < target_time:
|
||||
next_datetime = combine_date_time_in_tz(current.date(), target_time, tz)
|
||||
|
||||
while next_datetime.weekday() >= 5:
|
||||
next_day = next_datetime.date() + timedelta(days=1)
|
||||
next_datetime = combine_date_time_in_tz(next_day, target_time, tz)
|
||||
|
||||
return next_datetime
|
||||
|
||||
|
||||
def get_business_day_range(
|
||||
start: datetime,
|
||||
end: datetime,
|
||||
tz: str = DEFAULT_TIMEZONE
|
||||
) -> list[datetime]:
|
||||
"""
|
||||
Get list of business days (weekdays) between start and end dates.
|
||||
|
||||
Args:
|
||||
start: Start datetime
|
||||
end: End datetime
|
||||
tz: IANA timezone string
|
||||
|
||||
Returns:
|
||||
List of datetime objects for each business day
|
||||
"""
|
||||
if start.tzinfo is None:
|
||||
start = start.replace(tzinfo=ZoneInfo(tz))
|
||||
if end.tzinfo is None:
|
||||
end = end.replace(tzinfo=ZoneInfo(tz))
|
||||
|
||||
business_days = []
|
||||
current = start
|
||||
|
||||
while current <= end:
|
||||
if current.weekday() < 5:
|
||||
business_days.append(current)
|
||||
current += timedelta(days=1)
|
||||
|
||||
return business_days
|
||||
|
||||
|
||||
def get_tenant_now(tenant_tz: str = DEFAULT_TIMEZONE) -> datetime:
|
||||
"""
|
||||
Get current datetime for tenant's timezone.
|
||||
|
||||
Args:
|
||||
tenant_tz: Tenant's IANA timezone string
|
||||
|
||||
Returns:
|
||||
Current datetime in tenant's timezone
|
||||
"""
|
||||
return get_current_datetime_in_tz(tenant_tz)
|
||||
|
||||
|
||||
def is_tenant_business_hours(
|
||||
tenant_tz: str = DEFAULT_TIMEZONE,
|
||||
dt: Optional[datetime] = None,
|
||||
config: Optional[dict] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Check if it's currently business hours for tenant.
|
||||
|
||||
Args:
|
||||
tenant_tz: Tenant's IANA timezone string
|
||||
dt: Optional datetime to check (defaults to now)
|
||||
config: Optional business hours configuration override
|
||||
|
||||
Returns:
|
||||
True if within tenant's business hours, False otherwise
|
||||
"""
|
||||
if config is None:
|
||||
config = BUSINESS_HOURS_DEFAULT
|
||||
|
||||
return is_business_hours(
|
||||
dt=dt,
|
||||
tz=tenant_tz,
|
||||
start_hour=config.get("start_hour", BUSINESS_HOURS_DEFAULT["start_hour"]),
|
||||
end_hour=config.get("end_hour", BUSINESS_HOURS_DEFAULT["end_hour"]),
|
||||
include_weekends=config.get("include_weekends", BUSINESS_HOURS_DEFAULT["include_weekends"])
|
||||
)
|
||||
Reference in New Issue
Block a user