Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 64 additions & 0 deletions app/services/holiday_providers/argentina_api_provider.py
Original file line number Diff line number Diff line change
@@ -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
14 changes: 12 additions & 2 deletions app/services/holiday_service.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
from flask import Config

from app.services.holiday_providers.argentina_api_provider import ArgentinaApiProvider
from app.services.holiday_providers.argentina_website_provider import (
ArgentinaWebsiteProvider,
)
from app.services.holiday_providers.base import HolidayProvider

# 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:
Expand All @@ -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
Expand Down
116 changes: 116 additions & 0 deletions tests/test_argentina_api_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
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 == []

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"
25 changes: 25 additions & 0 deletions tests/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +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_website_provider import (
ArgentinaWebsiteProvider,
)
Expand Down Expand Up @@ -131,6 +132,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):
"""
Expand Down Expand Up @@ -172,6 +174,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.
Expand Down
Loading