Refactor datetime and timezone utils

This commit is contained in:
Urtzi Alfaro
2025-10-12 23:16:04 +02:00
parent 7556a00db7
commit 96ad5c6692
11 changed files with 731 additions and 362 deletions

112
shared/dt_utils/__init__.py Normal file
View File

@@ -0,0 +1,112 @@
"""
DateTime and Timezone Utilities
Modern, clean datetime and timezone handling for the bakery system.
Uses Python's built-in zoneinfo (Python 3.9+) for timezone operations.
Public API:
Core operations (from core.py):
- utc_now, now_in_timezone
- to_utc, to_timezone
- ensure_aware, ensure_naive
- parse_iso, format_iso, format_custom
- is_aware, validate_timezone
Timezone operations (from timezone.py):
- get_timezone_info, get_utc_offset_hours
- list_supported_timezones
- get_current_date_in_tz, get_current_datetime_in_tz
- combine_date_time_in_tz
- convert_to_utc, convert_from_utc
Business logic (from business.py):
- is_business_hours, next_business_day
- get_business_day_range
- get_tenant_now, is_tenant_business_hours
Constants (from constants.py):
- DEFAULT_TIMEZONE
- SUPPORTED_TIMEZONES
- BUSINESS_HOURS_DEFAULT
"""
from .core import (
utc_now,
now_in_timezone,
to_utc,
to_timezone,
ensure_aware,
ensure_naive,
parse_iso,
format_iso,
format_custom,
is_aware,
validate_timezone,
)
from .timezone import (
get_timezone_info,
get_utc_offset_hours,
list_supported_timezones,
get_current_date_in_tz,
get_current_datetime_in_tz,
combine_date_time_in_tz,
convert_to_utc,
convert_from_utc,
)
from .business import (
is_business_hours,
next_business_day,
get_business_day_range,
get_tenant_now,
is_tenant_business_hours,
)
from .constants import (
DEFAULT_TIMEZONE,
SUPPORTED_TIMEZONES,
BUSINESS_HOURS_DEFAULT,
ISO_FORMAT,
DATE_FORMAT,
TIME_FORMAT,
DATETIME_FORMAT,
)
__all__ = [
# Core operations
"utc_now",
"now_in_timezone",
"to_utc",
"to_timezone",
"ensure_aware",
"ensure_naive",
"parse_iso",
"format_iso",
"format_custom",
"is_aware",
"validate_timezone",
# Timezone operations
"get_timezone_info",
"get_utc_offset_hours",
"list_supported_timezones",
"get_current_date_in_tz",
"get_current_datetime_in_tz",
"combine_date_time_in_tz",
"convert_to_utc",
"convert_from_utc",
# Business logic
"is_business_hours",
"next_business_day",
"get_business_day_range",
"get_tenant_now",
"is_tenant_business_hours",
# Constants
"DEFAULT_TIMEZONE",
"SUPPORTED_TIMEZONES",
"BUSINESS_HOURS_DEFAULT",
"ISO_FORMAT",
"DATE_FORMAT",
"TIME_FORMAT",
"DATETIME_FORMAT",
]

157
shared/dt_utils/business.py Normal file
View 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"])
)

View File

@@ -0,0 +1,33 @@
"""
DateTime and Timezone Constants
Configuration constants for datetime and timezone operations across the bakery system.
"""
DEFAULT_TIMEZONE = "Europe/Madrid"
SUPPORTED_TIMEZONES = {
"Europe/Madrid",
"Europe/London",
"Europe/Paris",
"Europe/Berlin",
"America/New_York",
"America/Chicago",
"America/Los_Angeles",
"Asia/Tokyo",
"Asia/Shanghai",
"Australia/Sydney",
"UTC"
}
BUSINESS_HOURS_DEFAULT = {
"start_hour": 8,
"end_hour": 20,
"include_weekends": False
}
ISO_FORMAT = "%Y-%m-%dT%H:%M:%S"
ISO_FORMAT_WITH_TZ = "%Y-%m-%dT%H:%M:%S%z"
DATE_FORMAT = "%Y-%m-%d"
TIME_FORMAT = "%H:%M:%S"
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"

168
shared/dt_utils/core.py Normal file
View File

@@ -0,0 +1,168 @@
"""
Core DateTime Utilities
Low-level datetime operations for the bakery system.
All functions use Python's built-in zoneinfo for timezone handling.
"""
from datetime import datetime, timezone
from typing import Optional
from zoneinfo import ZoneInfo
from .constants import DEFAULT_TIMEZONE, ISO_FORMAT
def utc_now() -> datetime:
"""
Get current datetime in UTC with timezone awareness.
Returns:
Current UTC datetime
"""
return datetime.now(timezone.utc)
def now_in_timezone(tz: str = DEFAULT_TIMEZONE) -> datetime:
"""
Get current datetime in specified timezone.
Args:
tz: IANA timezone string (e.g., "Europe/Madrid")
Returns:
Current datetime in the specified timezone
"""
return datetime.now(ZoneInfo(tz))
def to_utc(dt: datetime) -> datetime:
"""
Convert datetime to UTC.
Args:
dt: Datetime to convert (if naive, assumes UTC)
Returns:
Datetime in UTC timezone
"""
if dt.tzinfo is None:
return dt.replace(tzinfo=timezone.utc)
return dt.astimezone(timezone.utc)
def to_timezone(dt: datetime, tz: str) -> datetime:
"""
Convert datetime to specified timezone.
Args:
dt: Datetime to convert (if naive, assumes UTC)
tz: Target IANA timezone string
Returns:
Datetime in target timezone
"""
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(ZoneInfo(tz))
def ensure_aware(dt: datetime, default_tz: str = "UTC") -> datetime:
"""
Ensure a datetime is timezone-aware.
Args:
dt: Datetime to check
default_tz: Timezone to apply if datetime is naive (default: UTC)
Returns:
Timezone-aware datetime
"""
if dt.tzinfo is None:
tz = ZoneInfo(default_tz)
return dt.replace(tzinfo=tz)
return dt
def ensure_naive(dt: datetime) -> datetime:
"""
Remove timezone information from a datetime.
Args:
dt: Datetime to process
Returns:
Timezone-naive datetime
"""
if dt.tzinfo is not None:
return dt.replace(tzinfo=None)
return dt
def parse_iso(dt_str: str) -> datetime:
"""
Parse ISO format datetime string.
Args:
dt_str: ISO format datetime string
Returns:
Parsed datetime (timezone-aware if timezone info present)
"""
return datetime.fromisoformat(dt_str)
def format_iso(dt: datetime) -> str:
"""
Format datetime as ISO string.
Args:
dt: Datetime to format
Returns:
ISO format string
"""
return dt.isoformat()
def format_custom(dt: datetime, format_str: str = ISO_FORMAT) -> str:
"""
Format datetime with custom format string.
Args:
dt: Datetime to format
format_str: strftime format string
Returns:
Formatted datetime string
"""
return dt.strftime(format_str)
def is_aware(dt: datetime) -> bool:
"""
Check if datetime is timezone-aware.
Args:
dt: Datetime to check
Returns:
True if timezone-aware, False otherwise
"""
return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
def validate_timezone(tz: str) -> bool:
"""
Validate if timezone string is valid.
Args:
tz: IANA timezone string to validate
Returns:
True if valid, False otherwise
"""
try:
ZoneInfo(tz)
return True
except Exception:
return False

160
shared/dt_utils/timezone.py Normal file
View File

@@ -0,0 +1,160 @@
"""
Timezone Operations
Timezone metadata and conversion utilities.
"""
from datetime import datetime, date, time
from zoneinfo import ZoneInfo
from typing import Optional
import structlog
from .constants import DEFAULT_TIMEZONE, SUPPORTED_TIMEZONES
logger = structlog.get_logger()
def get_timezone_info(tz: str) -> ZoneInfo:
"""
Get ZoneInfo object for timezone.
Args:
tz: IANA timezone string
Returns:
ZoneInfo object
Raises:
ZoneInfoNotFoundError: If timezone is invalid
"""
return ZoneInfo(tz)
def get_utc_offset_hours(tz: str) -> float:
"""
Get current UTC offset for timezone in hours.
Args:
tz: IANA timezone string
Returns:
UTC offset in hours (e.g., +2.0 for CEST)
"""
try:
zone = ZoneInfo(tz)
now = datetime.now(zone)
offset_seconds = now.utcoffset().total_seconds()
return offset_seconds / 3600
except Exception as e:
logger.warning(f"Could not get offset for {tz}", error=str(e))
return 0.0
def list_supported_timezones() -> list[str]:
"""
Get list of supported timezones.
Returns:
List of IANA timezone strings
"""
return sorted(SUPPORTED_TIMEZONES)
def get_current_date_in_tz(tz: str = DEFAULT_TIMEZONE) -> date:
"""
Get current date in specified timezone.
Args:
tz: IANA timezone string
Returns:
Current date in the specified timezone
"""
try:
zone = ZoneInfo(tz)
return datetime.now(zone).date()
except Exception as e:
logger.warning(f"Invalid timezone {tz}, using default", error=str(e))
return datetime.now(ZoneInfo(DEFAULT_TIMEZONE)).date()
def get_current_datetime_in_tz(tz: str = DEFAULT_TIMEZONE) -> datetime:
"""
Get current datetime in specified timezone.
Args:
tz: IANA timezone string
Returns:
Current datetime in the specified timezone
"""
try:
zone = ZoneInfo(tz)
return datetime.now(zone)
except Exception as e:
logger.warning(f"Invalid timezone {tz}, using default", error=str(e))
return datetime.now(ZoneInfo(DEFAULT_TIMEZONE))
def combine_date_time_in_tz(
target_date: date,
target_time: time,
tz: str = DEFAULT_TIMEZONE
) -> datetime:
"""
Combine date and time in specified timezone.
Args:
target_date: Date component
target_time: Time component
tz: IANA timezone string
Returns:
Datetime combining date and time in specified timezone
"""
try:
zone = ZoneInfo(tz)
return datetime.combine(target_date, target_time, tzinfo=zone)
except Exception as e:
logger.warning(f"Invalid timezone {tz}, using default", error=str(e))
zone = ZoneInfo(DEFAULT_TIMEZONE)
return datetime.combine(target_date, target_time, tzinfo=zone)
def convert_to_utc(dt: datetime) -> datetime:
"""
Convert datetime to UTC.
Args:
dt: Datetime to convert (must be timezone-aware)
Returns:
Datetime in UTC timezone
"""
if dt.tzinfo is None:
logger.warning("Converting naive datetime to UTC, assuming UTC")
return dt.replace(tzinfo=ZoneInfo("UTC"))
return dt.astimezone(ZoneInfo("UTC"))
def convert_from_utc(dt: datetime, target_timezone: str) -> datetime:
"""
Convert UTC datetime to target timezone.
Args:
dt: UTC datetime
target_timezone: Target IANA timezone string
Returns:
Datetime in target timezone
"""
if dt.tzinfo is None:
dt = dt.replace(tzinfo=ZoneInfo("UTC"))
try:
zone = ZoneInfo(target_timezone)
return dt.astimezone(zone)
except Exception as e:
logger.warning(f"Invalid timezone {target_timezone}, using default", error=str(e))
zone = ZoneInfo(DEFAULT_TIMEZONE)
return dt.astimezone(zone)

View File

@@ -1,71 +0,0 @@
"""
DateTime utilities for microservices
"""
from datetime import datetime, timezone, timedelta
from typing import Optional
import pytz
def utc_now() -> datetime:
"""Get current UTC datetime"""
return datetime.now(timezone.utc)
def madrid_now() -> datetime:
"""Get current Madrid datetime"""
madrid_tz = pytz.timezone('Europe/Madrid')
return datetime.now(madrid_tz)
def to_utc(dt: datetime) -> datetime:
"""Convert datetime to UTC"""
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(timezone.utc)
def to_madrid(dt: datetime) -> datetime:
"""Convert datetime to Madrid timezone"""
madrid_tz = pytz.timezone('Europe/Madrid')
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(madrid_tz)
def format_datetime(dt: datetime, format_str: str = "%Y-%m-%d %H:%M:%S") -> str:
"""Format datetime as string"""
return dt.strftime(format_str)
def parse_datetime(dt_str: str, format_str: str = "%Y-%m-%d %H:%M:%S") -> datetime:
"""Parse datetime from string"""
return datetime.strptime(dt_str, format_str)
def is_business_hours(dt: Optional[datetime] = None) -> bool:
"""Check if datetime is during business hours (9 AM - 6 PM Madrid time)"""
if dt is None:
dt = madrid_now()
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
madrid_dt = to_madrid(dt)
# Check if it's a weekday (Monday=0, Sunday=6)
if madrid_dt.weekday() >= 5: # Weekend
return False
# Check if it's business hours
return 9 <= madrid_dt.hour < 18
def next_business_day(dt: Optional[datetime] = None) -> datetime:
"""Get next business day"""
if dt is None:
dt = madrid_now()
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
madrid_dt = to_madrid(dt)
# Add days until we reach a weekday
while madrid_dt.weekday() >= 5: # Weekend
madrid_dt += timedelta(days=1)
# Set to 9 AM
return madrid_dt.replace(hour=9, minute=0, second=0, microsecond=0)

View File

@@ -1,276 +0,0 @@
# shared/utils/timezone_helper.py
"""
Timezone Utility Helper for Bakery Management System
Provides timezone-aware date/time utilities for accurate scheduling across
different geographic locations. All schedulers should use these utilities
to ensure consistent behavior.
"""
from datetime import datetime, date, time
from typing import Optional
from zoneinfo import ZoneInfo
import structlog
logger = structlog.get_logger()
class TimezoneHelper:
"""Helper class for timezone-aware operations"""
DEFAULT_TIMEZONE = "Europe/Madrid"
VALID_TIMEZONES = {
"Europe/Madrid", "Europe/London", "Europe/Paris", "Europe/Berlin",
"America/New_York", "America/Chicago", "America/Los_Angeles",
"Asia/Tokyo", "Asia/Shanghai", "Australia/Sydney",
"UTC"
}
@classmethod
def get_current_date_in_timezone(cls, timezone_str: str) -> date:
"""
Get current date in specified timezone
Args:
timezone_str: IANA timezone string (e.g., "Europe/Madrid")
Returns:
Current date in the specified timezone
"""
try:
tz = ZoneInfo(timezone_str)
return datetime.now(tz).date()
except Exception as e:
logger.warning(f"Invalid timezone {timezone_str}, using default",
error=str(e))
return datetime.now(ZoneInfo(cls.DEFAULT_TIMEZONE)).date()
@classmethod
def get_current_datetime_in_timezone(cls, timezone_str: str) -> datetime:
"""
Get current datetime in specified timezone
Args:
timezone_str: IANA timezone string
Returns:
Current datetime in the specified timezone
"""
try:
tz = ZoneInfo(timezone_str)
return datetime.now(tz)
except Exception as e:
logger.warning(f"Invalid timezone {timezone_str}, using default",
error=str(e))
return datetime.now(ZoneInfo(cls.DEFAULT_TIMEZONE))
@classmethod
def combine_date_time_in_timezone(
cls,
target_date: date,
target_time: time,
timezone_str: str
) -> datetime:
"""
Combine date and time in specified timezone
Args:
target_date: Date component
target_time: Time component
timezone_str: IANA timezone string
Returns:
Datetime combining date and time in specified timezone
"""
try:
tz = ZoneInfo(timezone_str)
return datetime.combine(target_date, target_time, tzinfo=tz)
except Exception as e:
logger.warning(f"Invalid timezone {timezone_str}, using default",
error=str(e))
tz = ZoneInfo(cls.DEFAULT_TIMEZONE)
return datetime.combine(target_date, target_time, tzinfo=tz)
@classmethod
def convert_to_utc(cls, dt: datetime) -> datetime:
"""
Convert datetime to UTC
Args:
dt: Datetime to convert (must be timezone-aware)
Returns:
Datetime in UTC timezone
"""
if dt.tzinfo is None:
logger.warning("Converting naive datetime to UTC, assuming UTC")
return dt.replace(tzinfo=ZoneInfo("UTC"))
return dt.astimezone(ZoneInfo("UTC"))
@classmethod
def convert_from_utc(cls, dt: datetime, target_timezone: str) -> datetime:
"""
Convert UTC datetime to target timezone
Args:
dt: UTC datetime
target_timezone: Target IANA timezone string
Returns:
Datetime in target timezone
"""
if dt.tzinfo is None:
dt = dt.replace(tzinfo=ZoneInfo("UTC"))
try:
tz = ZoneInfo(target_timezone)
return dt.astimezone(tz)
except Exception as e:
logger.warning(f"Invalid timezone {target_timezone}, using default",
error=str(e))
tz = ZoneInfo(cls.DEFAULT_TIMEZONE)
return dt.astimezone(tz)
@classmethod
def validate_timezone(cls, timezone_str: str) -> bool:
"""
Validate if timezone string is valid
Args:
timezone_str: IANA timezone string to validate
Returns:
True if valid, False otherwise
"""
try:
ZoneInfo(timezone_str)
return True
except Exception:
return False
@classmethod
def get_timezone_offset_hours(cls, timezone_str: str) -> float:
"""
Get current UTC offset for timezone in hours
Args:
timezone_str: IANA timezone string
Returns:
UTC offset in hours (e.g., +2.0 for CEST)
"""
try:
tz = ZoneInfo(timezone_str)
now = datetime.now(tz)
offset_seconds = now.utcoffset().total_seconds()
return offset_seconds / 3600
except Exception as e:
logger.warning(f"Could not get offset for {timezone_str}",
error=str(e))
return 0.0
@classmethod
def is_business_hours(
cls,
dt: Optional[datetime] = None,
timezone_str: str = DEFAULT_TIMEZONE,
start_hour: int = 8,
end_hour: int = 20
) -> bool:
"""
Check if datetime is within business hours
Args:
dt: Datetime to check (defaults to now)
timezone_str: IANA timezone string
start_hour: Business hours start (24h format)
end_hour: Business hours end (24h format)
Returns:
True if within business hours, False otherwise
"""
if dt is None:
dt = cls.get_current_datetime_in_timezone(timezone_str)
elif dt.tzinfo is None:
# Assume it's in the target timezone
tz = ZoneInfo(timezone_str)
dt = dt.replace(tzinfo=tz)
else:
# Convert to target timezone
dt = cls.convert_from_utc(dt, timezone_str)
# Check if weekday (Monday=0, Sunday=6)
if dt.weekday() >= 5: # Saturday or Sunday
return False
# Check if within business hours
return start_hour <= dt.hour < end_hour
@classmethod
def get_next_business_day_at_time(
cls,
target_time: time,
timezone_str: str = DEFAULT_TIMEZONE,
from_datetime: Optional[datetime] = None
) -> datetime:
"""
Get next business day at specific time in timezone
Args:
target_time: Time to schedule (e.g., time(6, 0) for 6 AM)
timezone_str: IANA timezone string
from_datetime: Starting datetime (defaults to now)
Returns:
Next business day at target_time in specified timezone
"""
if from_datetime is None:
current = cls.get_current_datetime_in_timezone(timezone_str)
else:
current = cls.convert_from_utc(from_datetime, timezone_str)
# Start with next day
next_day = current.date()
next_datetime = cls.combine_date_time_in_timezone(
next_day, target_time, timezone_str
)
# If we haven't passed target_time today, use today
if current.time() < target_time:
next_datetime = cls.combine_date_time_in_timezone(
current.date(), target_time, timezone_str
)
# Skip weekends
while next_datetime.weekday() >= 5: # Saturday or Sunday
next_day = next_datetime.date()
from datetime import timedelta
next_day = next_day + timedelta(days=1)
next_datetime = cls.combine_date_time_in_timezone(
next_day, target_time, timezone_str
)
return next_datetime
# Convenience functions for common operations
def get_tenant_current_date(tenant_timezone: str = "Europe/Madrid") -> date:
"""Get current date for tenant's timezone"""
return TimezoneHelper.get_current_date_in_timezone(tenant_timezone)
def get_tenant_current_datetime(tenant_timezone: str = "Europe/Madrid") -> datetime:
"""Get current datetime for tenant's timezone"""
return TimezoneHelper.get_current_datetime_in_timezone(tenant_timezone)
def is_tenant_business_hours(tenant_timezone: str = "Europe/Madrid") -> bool:
"""Check if it's currently business hours for tenant"""
return TimezoneHelper.is_business_hours(timezone_str=tenant_timezone)
def validate_timezone(timezone_str: str) -> bool:
"""Validate timezone string"""
return TimezoneHelper.validate_timezone(timezone_str)