11import time
22from collections .abc import Generator
33from datetime import UTC , date , datetime , timedelta
4- from typing import Any , ClassVar
4+ from typing import Any
55
66import jdatetime
77import requests
88from requests .adapters import HTTPAdapter
99from urllib3 .util .retry import Retry
1010
1111from archipy .configs .base_config import BaseConfig
12+ from archipy .helpers .decorators .cache import ttl_cache_decorator
1213from archipy .models .errors import UnknownError
1314
15+ _HOLIDAY_CACHE_MAXSIZE = 2048
16+
17+
18+ def _iran_holiday_historical_cache_ttl_seconds () -> int :
19+ """Return DATETIME.HISTORICAL_CACHE_TTL from the global config (evaluated at decorate time)."""
20+ return BaseConfig .global_config ().DATETIME .HISTORICAL_CACHE_TTL
21+
22+
23+ def _iran_holiday_standard_cache_ttl_seconds () -> int :
24+ """Return DATETIME.CACHE_TTL from the global config (evaluated at decorate time)."""
25+ return BaseConfig .global_config ().DATETIME .CACHE_TTL
26+
1427
1528class DatetimeUtils :
1629 """A utility class for handling date and time operations, including conversions, caching, and API integrations.
@@ -19,9 +32,6 @@ class DatetimeUtils:
1932 utility functions for timezone-aware datetime objects, date ranges, and string formatting.
2033 """
2134
22- """A class-level cache for storing holiday statuses to avoid redundant API calls."""
23- _holiday_cache : ClassVar [dict [str , tuple [bool , datetime ]]] = {}
24-
2535 @staticmethod
2636 def convert_to_jalali (target_date : date ) -> jdatetime .date :
2737 """Converts a Gregorian date to a Jalali (Persian) date.
@@ -46,83 +56,42 @@ def is_holiday_in_iran(cls, target_date: date) -> bool:
4656 Returns:
4757 bool: True if the date is a holiday, False otherwise.
4858 """
49- # Convert to Jalali date first
50- jalali_date = cls .convert_to_jalali (target_date )
5159 date_str = target_date .strftime ("%Y-%m-%d" )
52- current_time = cls .get_datetime_utc_now ()
53-
54- # Check cache first
55- is_cached , is_holiday = cls ._check_cache (date_str , current_time )
56- if is_cached :
57- return is_holiday
58-
59- # Fetch holiday status and cache it
60- return cls ._fetch_and_cache_holiday_status (jalali_date , date_str , current_time )
61-
62- @classmethod
63- def _check_cache (cls , date_str : str , current_time : datetime ) -> tuple [bool , bool ]:
64- """Checks the cache for holiday status to avoid redundant API calls.
65-
66- Args:
67- date_str (str): The date string to check in the cache.
68- current_time (datetime): The current time to compare against cache expiration.
69-
70- Returns:
71- tuple[bool, bool]: A tuple where the first element indicates if the cache was hit,
72- and the second element is the cached holiday status.
73- """
74- cached_data = cls ._holiday_cache .get (date_str )
75- if cached_data :
76- is_holiday , expiry_time = cached_data
77- if current_time < expiry_time :
78- return True , is_holiday
79-
80- # Remove expired cache entry
81- del cls ._holiday_cache [date_str ]
82-
83- return False , False
60+ utc_today = cls .get_datetime_utc_now ().date ()
61+ is_historical = target_date <= utc_today
62+ if is_historical :
63+ return cls ._fetch_holiday_in_iran_historical (date_str )
64+ return cls ._fetch_holiday_in_iran_standard (date_str )
8465
85- @classmethod
86- def _fetch_and_cache_holiday_status (
87- cls ,
88- jalali_date : jdatetime .date ,
89- date_str : str ,
90- current_time : datetime ,
91- ) -> bool :
92- """Fetches holiday status from the API and caches the result.
93-
94- This method calls an external API to determine if the given Jalali date is a holiday.
95- If the API call is successful, the result is cached with an expiration time to avoid
96- redundant API calls. If the API call fails, an `UnknownError` is raised.
97-
98- Args:
99- jalali_date (jdatetime.date): The Jalali date to check for holiday status.
100- date_str (str): The date string to use as a cache key.
101- current_time (datetime): The current time to set cache expiration.
102-
103- Returns:
104- bool: True if the date is a holiday, False otherwise.
105-
106- Raises:
107- UnknownError: If the API request fails due to a network issue or other request-related errors.
108- """
66+ @staticmethod
67+ @ttl_cache_decorator (
68+ ttl_seconds = _iran_holiday_historical_cache_ttl_seconds ,
69+ maxsize = _HOLIDAY_CACHE_MAXSIZE ,
70+ )
71+ def _fetch_holiday_in_iran_historical (date_str : str ) -> bool :
72+ """Resolve holiday flag for a date on or before UTC today; cached with historical TTL."""
73+ target_date = datetime .strptime (date_str , "%Y-%m-%d" ).date ()
74+ jalali_date = DatetimeUtils .convert_to_jalali (target_date )
10975 try :
110- config : Any = BaseConfig .global_config ()
111- response = cls ._call_holiday_api (jalali_date )
112- is_holiday = cls ._parse_holiday_response (response , jalali_date )
113-
114- # Determine cache TTL based on whether the date is historical
115- target_date = datetime .strptime (date_str , "%Y-%m-%d" ).replace (tzinfo = UTC ).date ()
116- is_historical = target_date <= current_time .date ()
117- cache_ttl = config .DATETIME .HISTORICAL_CACHE_TTL if is_historical else config .DATETIME .CACHE_TTL
118-
119- # Cache the result with appropriate expiration
120- expiry_time = current_time + timedelta (seconds = cache_ttl )
121- cls ._holiday_cache [date_str ] = (is_holiday , expiry_time )
76+ response = DatetimeUtils ._call_holiday_api (jalali_date )
12277 except requests .RequestException as exception :
12378 raise UnknownError from exception
79+ return DatetimeUtils ._parse_holiday_response (response , jalali_date )
12480
125- return is_holiday
81+ @staticmethod
82+ @ttl_cache_decorator (
83+ ttl_seconds = _iran_holiday_standard_cache_ttl_seconds ,
84+ maxsize = _HOLIDAY_CACHE_MAXSIZE ,
85+ )
86+ def _fetch_holiday_in_iran_standard (date_str : str ) -> bool :
87+ """Resolve holiday flag for a strictly future calendar date; cached with standard TTL."""
88+ target_date = datetime .strptime (date_str , "%Y-%m-%d" ).date ()
89+ jalali_date = DatetimeUtils .convert_to_jalali (target_date )
90+ try :
91+ response = DatetimeUtils ._call_holiday_api (jalali_date )
92+ except requests .RequestException as exception :
93+ raise UnknownError from exception
94+ return DatetimeUtils ._parse_holiday_response (response , jalali_date )
12695
12796 @staticmethod
12897 def _call_holiday_api (jalali_date : jdatetime .date ) -> dict [str , Any ]:
0 commit comments