From c2e85c6a13415a8bf81d18ab1cb01da2fa497b6d Mon Sep 17 00:00:00 2001 From: PPeitsch Date: Wed, 14 Jan 2026 01:14:27 -0300 Subject: [PATCH 1/4] Fix holiday calculation URL and add ArgentinaDatos provider --- app/config/config.py | 6 +- .../argentina_api_provider.py | 64 +++++++++++++ app/services/holiday_service.py | 14 ++- tests/test_argentina_api_provider.py | 92 +++++++++++++++++++ 4 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 app/services/holiday_providers/argentina_api_provider.py create mode 100644 tests/test_argentina_api_provider.py diff --git a/app/config/config.py b/app/config/config.py index ad8fbcb..9e484ab 100644 --- a/app/config/config.py +++ b/app/config/config.py @@ -12,7 +12,11 @@ class Config: HOLIDAY_PROVIDER = os.getenv("HOLIDAY_PROVIDER", "ARGENTINA_WEBSITE") HOLIDAYS_BASE_URL = os.getenv( "HOLIDAYS_BASE_URL", - "https://www.argentina.gob.ar/interior/feriados-nacionales-{year}", + "https://www.argentina.gob.ar/jefatura/feriados-nacionales-{year}", + ) + HOLIDAY_API_URL = os.getenv( + "HOLIDAY_API_URL", + "https://api.argentinadatos.com/v1/feriados/{year}", ) # Configuración horaria diff --git a/app/services/holiday_providers/argentina_api_provider.py b/app/services/holiday_providers/argentina_api_provider.py new file mode 100644 index 0000000..2bea0a4 --- /dev/null +++ b/app/services/holiday_providers/argentina_api_provider.py @@ -0,0 +1,64 @@ +from datetime import datetime +from typing import List + +import requests + +from app.models.models import Holiday + + +class ArgentinaApiProvider: + """ + Holiday provider that fetches holidays from the ArgentinaDatos API. + """ + + def __init__(self, api_url: str): + self.url_template = api_url + + def get_holidays(self, year: int) -> List[Holiday]: + """ + Fetches holidays for a given year from the API and returns them as Holiday objects. + """ + url = self.url_template.format(year=year) + try: + response = requests.get(url, timeout=10) + response.raise_for_status() + data = response.json() + except requests.RequestException as e: + print(f"Error fetching holiday data from API for year {year}: {e}") + return [] + except ValueError as e: + print(f"Error decoding JSON from API for year {year}: {e}") + return [] + + holidays: List[Holiday] = [] + for entry in data: + try: + date_str = entry.get("fecha") + description = entry.get("nombre") + type_raw = entry.get("tipo") + + if not date_str or not description: + continue + + parsed_date = datetime.strptime(date_str, "%Y-%m-%d").date() + + # Filter by year just in case + if parsed_date.year != year: + continue + + type_map = { + "inamovible": "Inamovible", + "trasladable": "Trasladable", + "puente": "Fines Turísticos", + "nolaborable": "No Laborable", + } + type_ = type_map.get(type_raw, "Otro") + + holiday = Holiday(date=parsed_date, description=description, type=type_) + holidays.append(holiday) + + except (ValueError, AttributeError) as e: + print(f"Error parsing holiday entry: {entry}. Error: {e}") + continue + + return holidays diff --git a/app/services/holiday_service.py b/app/services/holiday_service.py index 04fb6fd..121be0f 100644 --- a/app/services/holiday_service.py +++ b/app/services/holiday_service.py @@ -1,5 +1,6 @@ from flask import Config +from app.services.holiday_providers.argentina_api_provider import ArgentinaApiProvider from app.services.holiday_providers.argentina_website_provider import ( ArgentinaWebsiteProvider, ) @@ -7,7 +8,10 @@ # A mapping of provider names to their corresponding classes. # This makes it easy to add new providers in the future. -PROVIDER_MAP = {"ARGENTINA_WEBSITE": ArgentinaWebsiteProvider} +PROVIDER_MAP = { + "ARGENTINA_WEBSITE": ArgentinaWebsiteProvider, + "ARGENTINA_API": ArgentinaApiProvider, +} def get_holiday_provider(config: Config) -> HolidayProvider: @@ -27,7 +31,13 @@ def get_holiday_provider(config: Config) -> HolidayProvider: base_url = getattr(config, "HOLIDAYS_BASE_URL", None) if not base_url: raise ValueError("HOLIDAYS_BASE_URL is not configured.") - return provider_class(base_url=base_url) + return ArgentinaWebsiteProvider(base_url=base_url) + + if provider_name.upper() == "ARGENTINA_API": + api_url = getattr(config, "HOLIDAY_API_URL", None) + if not api_url: + raise ValueError("HOLIDAY_API_URL is not configured.") + return ArgentinaApiProvider(api_url=api_url) # This part would be extended for other providers # For now, we raise an error if the provider is in the map but has no diff --git a/tests/test_argentina_api_provider.py b/tests/test_argentina_api_provider.py new file mode 100644 index 0000000..138a217 --- /dev/null +++ b/tests/test_argentina_api_provider.py @@ -0,0 +1,92 @@ +from unittest.mock import MagicMock, patch + +import pytest +import requests + +from app.models.models import Holiday +from app.services.holiday_providers.argentina_api_provider import ArgentinaApiProvider + + +class TestArgentinaApiProvider: + """ + Tests for the ArgentinaApiProvider. + """ + + @pytest.fixture + def mock_response(self): + """Creates a mock response object.""" + mock = MagicMock(spec=requests.Response) + mock.raise_for_status.return_value = None + return mock + + def test_get_holidays_success(self, mock_response): + """ + Test successful holiday parsing from API JSON response. + """ + year = 2025 + mock_response.json.return_value = [ + {"fecha": "2025-01-01", "nombre": "Año Nuevo", "tipo": "inamovible"}, + { + "fecha": "2025-05-01", + "nombre": "Día del Trabajador", + "tipo": "inamovible", + }, + ] + provider = ArgentinaApiProvider(api_url="http://fake-api.com/{year}") + with patch("requests.get", return_value=mock_response): + holidays = provider.get_holidays(year) + assert len(holidays) == 2 + assert holidays[0].description == "Año Nuevo" + assert holidays[0].date.year == 2025 + + def test_get_holidays_filter_by_year(self, mock_response): + """ + Test that holidays from other years are filtered out. + """ + year = 2025 + mock_response.json.return_value = [ + {"fecha": "2025-01-01", "nombre": "Correct Year", "tipo": "inamovible"}, + {"fecha": "2024-12-31", "nombre": "Wrong Year", "tipo": "inamovible"}, + ] + provider = ArgentinaApiProvider(api_url="http://fake-api.com/{year}") + with patch("requests.get", return_value=mock_response): + holidays = provider.get_holidays(year) + assert len(holidays) == 1 + assert holidays[0].description == "Correct Year" + + def test_get_holidays_malformed_entry(self, mock_response): + """ + Test that malformed entries are skipped. + """ + year = 2025 + mock_response.json.return_value = [ + {"fecha": "2025-01-01", "nombre": "Good", "tipo": "inamovible"}, + {"fecha": "", "nombre": "Bad Date", "tipo": "inamovible"}, + {"fecha": "2025-01-02", "nombre": "", "tipo": "inamovible"}, + ] + provider = ArgentinaApiProvider(api_url="http://fake-api.com/{year}") + with patch("requests.get", return_value=mock_response): + holidays = provider.get_holidays(year) + assert len(holidays) == 1 + assert holidays[0].description == "Good" + + def test_get_holidays_network_error(self): + """ + Test that network errors are handled gracefully. + """ + provider = ArgentinaApiProvider(api_url="http://fake-api.com/{year}") + with patch( + "requests.get", side_effect=requests.RequestException("Network Error") + ): + holidays = provider.get_holidays(2025) + assert holidays == [] + + def test_get_holidays_json_error(self, mock_response): + """ + Test that JSON decoding errors are handled gracefully. + """ + mock_response.json = MagicMock(side_effect=ValueError("Invalid JSON")) + provider = ArgentinaApiProvider(api_url="http://fake-api.com/{year}") + with patch("requests.get", return_value=mock_response): + holidays = provider.get_holidays(2025) + assert holidays == [] From c3c8792e89f1069341df0189d89b77468156fb61 Mon Sep 17 00:00:00 2001 From: PPeitsch Date: Wed, 14 Jan 2026 01:29:09 -0300 Subject: [PATCH 2/4] Add test coverage for ArgentinaApiProvider initialization --- tests/test_services.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_services.py b/tests/test_services.py index 493c368..aae1cd3 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -8,6 +8,9 @@ from app.config.config import Config from app.models.models import Holiday +from app.services.holiday_providers.argentina_api_provider import ( + ArgentinaApiProvider, +) from app.services.holiday_providers.argentina_website_provider import ( ArgentinaWebsiteProvider, ) @@ -131,6 +134,7 @@ class TestHolidayService: class MockConfig(Config): HOLIDAY_PROVIDER = "ARGENTINA_WEBSITE" HOLIDAYS_BASE_URL = "http://fake-url.com/{year}" + HOLIDAY_API_URL = "http://fake-api.com/{year}" def test_get_holiday_provider_success(self): """ @@ -172,6 +176,29 @@ class MissingUrlConfig(TestHolidayService.MockConfig): with pytest.raises(ValueError, match="HOLIDAYS_BASE_URL is not configured"): get_holiday_provider(MissingUrlConfig) + def test_get_holiday_provider_api_success(self): + """ + Test that the factory returns the correct API provider instance. + """ + + class ApiConfig(TestHolidayService.MockConfig): + HOLIDAY_PROVIDER = "ARGENTINA_API" + + provider = get_holiday_provider(ApiConfig) + assert isinstance(provider, ArgentinaApiProvider) + + def test_get_holiday_provider_api_missing_url(self): + """ + Test that a ValueError is raised if the API URL is missing. + """ + + class MissingApiUrlConfig(TestHolidayService.MockConfig): + HOLIDAY_PROVIDER = "ARGENTINA_API" + HOLIDAY_API_URL = None # type: ignore[assignment] + + with pytest.raises(ValueError, match="HOLIDAY_API_URL is not configured"): + get_holiday_provider(MissingApiUrlConfig) + def test_get_holiday_provider_not_implemented(self): """ Test that a NotImplementedError is raised for a provider without init logic. From f3530333cee07a4cc50c5eecf32001e765f9fca1 Mon Sep 17 00:00:00 2001 From: PPeitsch Date: Wed, 14 Jan 2026 01:30:52 -0300 Subject: [PATCH 3/4] Improve test coverage for ArgentinaApiProvider parsing exceptions --- tests/test_argentina_api_provider.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_argentina_api_provider.py b/tests/test_argentina_api_provider.py index 138a217..a5b165d 100644 --- a/tests/test_argentina_api_provider.py +++ b/tests/test_argentina_api_provider.py @@ -90,3 +90,23 @@ def test_get_holidays_json_error(self, mock_response): with patch("requests.get", return_value=mock_response): holidays = provider.get_holidays(2025) assert holidays == [] + + def test_get_holidays_parsing_exceptions(self, mock_response): + """ + Test that parsing exceptions (ValueError, AttributeError) are caught. + """ + year = 2025 + mock_response.json.return_value = [ + # Good entry + {"fecha": "2025-01-01", "nombre": "Good", "tipo": "inamovible"}, + # ValueError: Invalid date format (strptime fails) + {"fecha": "invalid-01-01", "nombre": "Bad Date Format", "tipo": "inamovible"}, + # AttributeError: Entry is not a dict (no .get method) + "invalid_string_entry", + ] + provider = ArgentinaApiProvider(api_url="http://fake-api.com/{year}") + with patch("requests.get", return_value=mock_response): + holidays = provider.get_holidays(year) + # Should only contain the valid holiday + assert len(holidays) == 1 + assert holidays[0].description == "Good" From e6b6f8f7830abf4a1b014f2734ba4e936dffb35e Mon Sep 17 00:00:00 2001 From: PPeitsch Date: Wed, 14 Jan 2026 01:40:45 -0300 Subject: [PATCH 4/4] Fix formatting checks for tests --- tests/test_argentina_api_provider.py | 6 +++++- tests/test_services.py | 4 +--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_argentina_api_provider.py b/tests/test_argentina_api_provider.py index a5b165d..927db53 100644 --- a/tests/test_argentina_api_provider.py +++ b/tests/test_argentina_api_provider.py @@ -100,7 +100,11 @@ def test_get_holidays_parsing_exceptions(self, mock_response): # Good entry {"fecha": "2025-01-01", "nombre": "Good", "tipo": "inamovible"}, # ValueError: Invalid date format (strptime fails) - {"fecha": "invalid-01-01", "nombre": "Bad Date Format", "tipo": "inamovible"}, + { + "fecha": "invalid-01-01", + "nombre": "Bad Date Format", + "tipo": "inamovible", + }, # AttributeError: Entry is not a dict (no .get method) "invalid_string_entry", ] diff --git a/tests/test_services.py b/tests/test_services.py index aae1cd3..7a9174d 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -8,9 +8,7 @@ from app.config.config import Config from app.models.models import Holiday -from app.services.holiday_providers.argentina_api_provider import ( - ArgentinaApiProvider, -) +from app.services.holiday_providers.argentina_api_provider import ArgentinaApiProvider from app.services.holiday_providers.argentina_website_provider import ( ArgentinaWebsiteProvider, )