211 lines
6.6 KiB
Python
211 lines
6.6 KiB
Python
|
|
"""
|
||
|
|
Demo Date Offset Utilities
|
||
|
|
Provides functions for adjusting dates during demo session cloning
|
||
|
|
to ensure all temporal data is relative to the demo session creation time
|
||
|
|
"""
|
||
|
|
|
||
|
|
from datetime import datetime, timezone, timedelta
|
||
|
|
from typing import Optional
|
||
|
|
|
||
|
|
|
||
|
|
# Base reference date for all demo seed data
|
||
|
|
# All seed scripts should use this as the "logical seed date"
|
||
|
|
BASE_REFERENCE_DATE = datetime(2025, 1, 15, 12, 0, 0, tzinfo=timezone.utc)
|
||
|
|
|
||
|
|
|
||
|
|
def adjust_date_for_demo(
|
||
|
|
original_date: Optional[datetime],
|
||
|
|
session_created_at: datetime,
|
||
|
|
base_reference_date: datetime = BASE_REFERENCE_DATE
|
||
|
|
) -> Optional[datetime]:
|
||
|
|
"""
|
||
|
|
Adjust a date from seed data to be relative to demo session creation time
|
||
|
|
|
||
|
|
This ensures that demo data appears fresh and relevant regardless of when
|
||
|
|
the demo session is created. For example, expiration dates that were "15 days
|
||
|
|
from seed date" will become "15 days from session creation date".
|
||
|
|
|
||
|
|
Args:
|
||
|
|
original_date: The original date from the seed data (or None)
|
||
|
|
session_created_at: When the demo session was created
|
||
|
|
base_reference_date: The logical date when seed data was created (default: 2025-01-15)
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Adjusted date relative to session creation, or None if original_date was None
|
||
|
|
|
||
|
|
Example:
|
||
|
|
# Seed data created on 2025-01-15
|
||
|
|
# Stock expiration: 2025-01-30 (15 days from seed date)
|
||
|
|
# Demo session created: 2025-10-16
|
||
|
|
# Result: 2025-10-31 (15 days from session date)
|
||
|
|
|
||
|
|
>>> original = datetime(2025, 1, 30, 12, 0, tzinfo=timezone.utc)
|
||
|
|
>>> session = datetime(2025, 10, 16, 10, 0, tzinfo=timezone.utc)
|
||
|
|
>>> adjusted = adjust_date_for_demo(original, session)
|
||
|
|
>>> print(adjusted)
|
||
|
|
2025-10-31 10:00:00+00:00
|
||
|
|
"""
|
||
|
|
if original_date is None:
|
||
|
|
return None
|
||
|
|
|
||
|
|
# Ensure timezone-aware datetimes
|
||
|
|
if original_date.tzinfo is None:
|
||
|
|
original_date = original_date.replace(tzinfo=timezone.utc)
|
||
|
|
if session_created_at.tzinfo is None:
|
||
|
|
session_created_at = session_created_at.replace(tzinfo=timezone.utc)
|
||
|
|
if base_reference_date.tzinfo is None:
|
||
|
|
base_reference_date = base_reference_date.replace(tzinfo=timezone.utc)
|
||
|
|
|
||
|
|
# Calculate offset from base reference
|
||
|
|
offset = original_date - base_reference_date
|
||
|
|
|
||
|
|
# Apply offset to session creation date
|
||
|
|
return session_created_at + offset
|
||
|
|
|
||
|
|
|
||
|
|
def adjust_date_relative_to_now(
|
||
|
|
days_offset: int,
|
||
|
|
hours_offset: int = 0,
|
||
|
|
reference_time: Optional[datetime] = None
|
||
|
|
) -> datetime:
|
||
|
|
"""
|
||
|
|
Create a date relative to now (or a reference time) with specified offset
|
||
|
|
|
||
|
|
Useful for creating dates during cloning without needing to store seed dates.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
days_offset: Number of days to add (negative for past dates)
|
||
|
|
hours_offset: Number of hours to add (negative for past times)
|
||
|
|
reference_time: Reference datetime (defaults to now)
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Calculated datetime
|
||
|
|
|
||
|
|
Example:
|
||
|
|
>>> # Create a date 7 days in the future
|
||
|
|
>>> future = adjust_date_relative_to_now(days_offset=7)
|
||
|
|
>>> # Create a date 3 days in the past
|
||
|
|
>>> past = adjust_date_relative_to_now(days_offset=-3)
|
||
|
|
"""
|
||
|
|
if reference_time is None:
|
||
|
|
reference_time = datetime.now(timezone.utc)
|
||
|
|
elif reference_time.tzinfo is None:
|
||
|
|
reference_time = reference_time.replace(tzinfo=timezone.utc)
|
||
|
|
|
||
|
|
return reference_time + timedelta(days=days_offset, hours=hours_offset)
|
||
|
|
|
||
|
|
|
||
|
|
def calculate_expiration_date(
|
||
|
|
received_date: datetime,
|
||
|
|
shelf_life_days: int
|
||
|
|
) -> datetime:
|
||
|
|
"""
|
||
|
|
Calculate expiration date based on received date and shelf life
|
||
|
|
|
||
|
|
Args:
|
||
|
|
received_date: When the product was received
|
||
|
|
shelf_life_days: Number of days until expiration
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Calculated expiration datetime
|
||
|
|
"""
|
||
|
|
if received_date.tzinfo is None:
|
||
|
|
received_date = received_date.replace(tzinfo=timezone.utc)
|
||
|
|
|
||
|
|
return received_date + timedelta(days=shelf_life_days)
|
||
|
|
|
||
|
|
|
||
|
|
def get_days_until_expiration(
|
||
|
|
expiration_date: datetime,
|
||
|
|
reference_date: Optional[datetime] = None
|
||
|
|
) -> int:
|
||
|
|
"""
|
||
|
|
Calculate number of days until expiration
|
||
|
|
|
||
|
|
Args:
|
||
|
|
expiration_date: The expiration datetime
|
||
|
|
reference_date: Reference datetime (defaults to now)
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Number of days until expiration (negative if already expired)
|
||
|
|
"""
|
||
|
|
if reference_date is None:
|
||
|
|
reference_date = datetime.now(timezone.utc)
|
||
|
|
elif reference_date.tzinfo is None:
|
||
|
|
reference_date = reference_date.replace(tzinfo=timezone.utc)
|
||
|
|
|
||
|
|
if expiration_date.tzinfo is None:
|
||
|
|
expiration_date = expiration_date.replace(tzinfo=timezone.utc)
|
||
|
|
|
||
|
|
delta = expiration_date - reference_date
|
||
|
|
return delta.days
|
||
|
|
|
||
|
|
|
||
|
|
def is_expiring_soon(
|
||
|
|
expiration_date: datetime,
|
||
|
|
threshold_days: int = 3,
|
||
|
|
reference_date: Optional[datetime] = None
|
||
|
|
) -> bool:
|
||
|
|
"""
|
||
|
|
Check if a product is expiring soon
|
||
|
|
|
||
|
|
Args:
|
||
|
|
expiration_date: The expiration datetime
|
||
|
|
threshold_days: Number of days to consider as "soon" (default: 3)
|
||
|
|
reference_date: Reference datetime (defaults to now)
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
True if expiring within threshold_days, False otherwise
|
||
|
|
"""
|
||
|
|
days_until = get_days_until_expiration(expiration_date, reference_date)
|
||
|
|
return 0 <= days_until <= threshold_days
|
||
|
|
|
||
|
|
|
||
|
|
def is_expired(
|
||
|
|
expiration_date: datetime,
|
||
|
|
reference_date: Optional[datetime] = None
|
||
|
|
) -> bool:
|
||
|
|
"""
|
||
|
|
Check if a product is expired
|
||
|
|
|
||
|
|
Args:
|
||
|
|
expiration_date: The expiration datetime
|
||
|
|
reference_date: Reference datetime (defaults to now)
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
True if expired, False otherwise
|
||
|
|
"""
|
||
|
|
days_until = get_days_until_expiration(expiration_date, reference_date)
|
||
|
|
return days_until < 0
|
||
|
|
|
||
|
|
|
||
|
|
def adjust_multiple_dates(
|
||
|
|
dates_dict: dict,
|
||
|
|
session_created_at: datetime,
|
||
|
|
base_reference_date: datetime = BASE_REFERENCE_DATE
|
||
|
|
) -> dict:
|
||
|
|
"""
|
||
|
|
Adjust multiple dates in a dictionary
|
||
|
|
|
||
|
|
Args:
|
||
|
|
dates_dict: Dictionary with datetime values to adjust
|
||
|
|
session_created_at: When the demo session was created
|
||
|
|
base_reference_date: The logical date when seed data was created
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
Dictionary with adjusted dates (preserves None values)
|
||
|
|
|
||
|
|
Example:
|
||
|
|
>>> dates = {
|
||
|
|
... 'expiration_date': datetime(2025, 1, 30, tzinfo=timezone.utc),
|
||
|
|
... 'received_date': datetime(2025, 1, 15, tzinfo=timezone.utc),
|
||
|
|
... 'optional_date': None
|
||
|
|
... }
|
||
|
|
>>> session = datetime(2025, 10, 16, tzinfo=timezone.utc)
|
||
|
|
>>> adjusted = adjust_multiple_dates(dates, session)
|
||
|
|
"""
|
||
|
|
return {
|
||
|
|
key: adjust_date_for_demo(value, session_created_at, base_reference_date)
|
||
|
|
for key, value in dates_dict.items()
|
||
|
|
}
|