Skip to content

Commit b9de801

Browse files
authored
Merge pull request #44 from PPeitsch/fix/holiday-url-update
Fix holiday fetching URL and add ArgentinaDatos provider
2 parents afe9ce7 + e6b6f8f commit b9de801

5 files changed

Lines changed: 222 additions & 3 deletions

File tree

app/config/config.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ class Config:
1212
HOLIDAY_PROVIDER = os.getenv("HOLIDAY_PROVIDER", "ARGENTINA_WEBSITE")
1313
HOLIDAYS_BASE_URL = os.getenv(
1414
"HOLIDAYS_BASE_URL",
15-
"https://www.argentina.gob.ar/interior/feriados-nacionales-{year}",
15+
"https://www.argentina.gob.ar/jefatura/feriados-nacionales-{year}",
16+
)
17+
HOLIDAY_API_URL = os.getenv(
18+
"HOLIDAY_API_URL",
19+
"https://api.argentinadatos.com/v1/feriados/{year}",
1620
)
1721

1822
# Configuración horaria
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from datetime import datetime
2+
from typing import List
3+
4+
import requests
5+
6+
from app.models.models import Holiday
7+
8+
9+
class ArgentinaApiProvider:
10+
"""
11+
Holiday provider that fetches holidays from the ArgentinaDatos API.
12+
"""
13+
14+
def __init__(self, api_url: str):
15+
self.url_template = api_url
16+
17+
def get_holidays(self, year: int) -> List[Holiday]:
18+
"""
19+
Fetches holidays for a given year from the API and returns them as Holiday objects.
20+
"""
21+
url = self.url_template.format(year=year)
22+
try:
23+
response = requests.get(url, timeout=10)
24+
response.raise_for_status()
25+
data = response.json()
26+
except requests.RequestException as e:
27+
print(f"Error fetching holiday data from API for year {year}: {e}")
28+
return []
29+
except ValueError as e:
30+
print(f"Error decoding JSON from API for year {year}: {e}")
31+
return []
32+
33+
holidays: List[Holiday] = []
34+
for entry in data:
35+
try:
36+
date_str = entry.get("fecha")
37+
description = entry.get("nombre")
38+
type_raw = entry.get("tipo")
39+
40+
if not date_str or not description:
41+
continue
42+
43+
parsed_date = datetime.strptime(date_str, "%Y-%m-%d").date()
44+
45+
# Filter by year just in case
46+
if parsed_date.year != year:
47+
continue
48+
49+
type_map = {
50+
"inamovible": "Inamovible",
51+
"trasladable": "Trasladable",
52+
"puente": "Fines Turísticos",
53+
"nolaborable": "No Laborable",
54+
}
55+
type_ = type_map.get(type_raw, "Otro")
56+
57+
holiday = Holiday(date=parsed_date, description=description, type=type_)
58+
holidays.append(holiday)
59+
60+
except (ValueError, AttributeError) as e:
61+
print(f"Error parsing holiday entry: {entry}. Error: {e}")
62+
continue
63+
64+
return holidays

app/services/holiday_service.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
from flask import Config
22

3+
from app.services.holiday_providers.argentina_api_provider import ArgentinaApiProvider
34
from app.services.holiday_providers.argentina_website_provider import (
45
ArgentinaWebsiteProvider,
56
)
67
from app.services.holiday_providers.base import HolidayProvider
78

89
# A mapping of provider names to their corresponding classes.
910
# This makes it easy to add new providers in the future.
10-
PROVIDER_MAP = {"ARGENTINA_WEBSITE": ArgentinaWebsiteProvider}
11+
PROVIDER_MAP = {
12+
"ARGENTINA_WEBSITE": ArgentinaWebsiteProvider,
13+
"ARGENTINA_API": ArgentinaApiProvider,
14+
}
1115

1216

1317
def get_holiday_provider(config: Config) -> HolidayProvider:
@@ -27,7 +31,13 @@ def get_holiday_provider(config: Config) -> HolidayProvider:
2731
base_url = getattr(config, "HOLIDAYS_BASE_URL", None)
2832
if not base_url:
2933
raise ValueError("HOLIDAYS_BASE_URL is not configured.")
30-
return provider_class(base_url=base_url)
34+
return ArgentinaWebsiteProvider(base_url=base_url)
35+
36+
if provider_name.upper() == "ARGENTINA_API":
37+
api_url = getattr(config, "HOLIDAY_API_URL", None)
38+
if not api_url:
39+
raise ValueError("HOLIDAY_API_URL is not configured.")
40+
return ArgentinaApiProvider(api_url=api_url)
3141

3242
# This part would be extended for other providers
3343
# For now, we raise an error if the provider is in the map but has no
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
from unittest.mock import MagicMock, patch
2+
3+
import pytest
4+
import requests
5+
6+
from app.models.models import Holiday
7+
from app.services.holiday_providers.argentina_api_provider import ArgentinaApiProvider
8+
9+
10+
class TestArgentinaApiProvider:
11+
"""
12+
Tests for the ArgentinaApiProvider.
13+
"""
14+
15+
@pytest.fixture
16+
def mock_response(self):
17+
"""Creates a mock response object."""
18+
mock = MagicMock(spec=requests.Response)
19+
mock.raise_for_status.return_value = None
20+
return mock
21+
22+
def test_get_holidays_success(self, mock_response):
23+
"""
24+
Test successful holiday parsing from API JSON response.
25+
"""
26+
year = 2025
27+
mock_response.json.return_value = [
28+
{"fecha": "2025-01-01", "nombre": "Año Nuevo", "tipo": "inamovible"},
29+
{
30+
"fecha": "2025-05-01",
31+
"nombre": "Día del Trabajador",
32+
"tipo": "inamovible",
33+
},
34+
]
35+
provider = ArgentinaApiProvider(api_url="http://fake-api.com/{year}")
36+
with patch("requests.get", return_value=mock_response):
37+
holidays = provider.get_holidays(year)
38+
assert len(holidays) == 2
39+
assert holidays[0].description == "Año Nuevo"
40+
assert holidays[0].date.year == 2025
41+
42+
def test_get_holidays_filter_by_year(self, mock_response):
43+
"""
44+
Test that holidays from other years are filtered out.
45+
"""
46+
year = 2025
47+
mock_response.json.return_value = [
48+
{"fecha": "2025-01-01", "nombre": "Correct Year", "tipo": "inamovible"},
49+
{"fecha": "2024-12-31", "nombre": "Wrong Year", "tipo": "inamovible"},
50+
]
51+
provider = ArgentinaApiProvider(api_url="http://fake-api.com/{year}")
52+
with patch("requests.get", return_value=mock_response):
53+
holidays = provider.get_holidays(year)
54+
assert len(holidays) == 1
55+
assert holidays[0].description == "Correct Year"
56+
57+
def test_get_holidays_malformed_entry(self, mock_response):
58+
"""
59+
Test that malformed entries are skipped.
60+
"""
61+
year = 2025
62+
mock_response.json.return_value = [
63+
{"fecha": "2025-01-01", "nombre": "Good", "tipo": "inamovible"},
64+
{"fecha": "", "nombre": "Bad Date", "tipo": "inamovible"},
65+
{"fecha": "2025-01-02", "nombre": "", "tipo": "inamovible"},
66+
]
67+
provider = ArgentinaApiProvider(api_url="http://fake-api.com/{year}")
68+
with patch("requests.get", return_value=mock_response):
69+
holidays = provider.get_holidays(year)
70+
assert len(holidays) == 1
71+
assert holidays[0].description == "Good"
72+
73+
def test_get_holidays_network_error(self):
74+
"""
75+
Test that network errors are handled gracefully.
76+
"""
77+
provider = ArgentinaApiProvider(api_url="http://fake-api.com/{year}")
78+
with patch(
79+
"requests.get", side_effect=requests.RequestException("Network Error")
80+
):
81+
holidays = provider.get_holidays(2025)
82+
assert holidays == []
83+
84+
def test_get_holidays_json_error(self, mock_response):
85+
"""
86+
Test that JSON decoding errors are handled gracefully.
87+
"""
88+
mock_response.json = MagicMock(side_effect=ValueError("Invalid JSON"))
89+
provider = ArgentinaApiProvider(api_url="http://fake-api.com/{year}")
90+
with patch("requests.get", return_value=mock_response):
91+
holidays = provider.get_holidays(2025)
92+
assert holidays == []
93+
94+
def test_get_holidays_parsing_exceptions(self, mock_response):
95+
"""
96+
Test that parsing exceptions (ValueError, AttributeError) are caught.
97+
"""
98+
year = 2025
99+
mock_response.json.return_value = [
100+
# Good entry
101+
{"fecha": "2025-01-01", "nombre": "Good", "tipo": "inamovible"},
102+
# ValueError: Invalid date format (strptime fails)
103+
{
104+
"fecha": "invalid-01-01",
105+
"nombre": "Bad Date Format",
106+
"tipo": "inamovible",
107+
},
108+
# AttributeError: Entry is not a dict (no .get method)
109+
"invalid_string_entry",
110+
]
111+
provider = ArgentinaApiProvider(api_url="http://fake-api.com/{year}")
112+
with patch("requests.get", return_value=mock_response):
113+
holidays = provider.get_holidays(year)
114+
# Should only contain the valid holiday
115+
assert len(holidays) == 1
116+
assert holidays[0].description == "Good"

tests/test_services.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from app.config.config import Config
1010
from app.models.models import Holiday
11+
from app.services.holiday_providers.argentina_api_provider import ArgentinaApiProvider
1112
from app.services.holiday_providers.argentina_website_provider import (
1213
ArgentinaWebsiteProvider,
1314
)
@@ -131,6 +132,7 @@ class TestHolidayService:
131132
class MockConfig(Config):
132133
HOLIDAY_PROVIDER = "ARGENTINA_WEBSITE"
133134
HOLIDAYS_BASE_URL = "http://fake-url.com/{year}"
135+
HOLIDAY_API_URL = "http://fake-api.com/{year}"
134136

135137
def test_get_holiday_provider_success(self):
136138
"""
@@ -172,6 +174,29 @@ class MissingUrlConfig(TestHolidayService.MockConfig):
172174
with pytest.raises(ValueError, match="HOLIDAYS_BASE_URL is not configured"):
173175
get_holiday_provider(MissingUrlConfig)
174176

177+
def test_get_holiday_provider_api_success(self):
178+
"""
179+
Test that the factory returns the correct API provider instance.
180+
"""
181+
182+
class ApiConfig(TestHolidayService.MockConfig):
183+
HOLIDAY_PROVIDER = "ARGENTINA_API"
184+
185+
provider = get_holiday_provider(ApiConfig)
186+
assert isinstance(provider, ArgentinaApiProvider)
187+
188+
def test_get_holiday_provider_api_missing_url(self):
189+
"""
190+
Test that a ValueError is raised if the API URL is missing.
191+
"""
192+
193+
class MissingApiUrlConfig(TestHolidayService.MockConfig):
194+
HOLIDAY_PROVIDER = "ARGENTINA_API"
195+
HOLIDAY_API_URL = None # type: ignore[assignment]
196+
197+
with pytest.raises(ValueError, match="HOLIDAY_API_URL is not configured"):
198+
get_holiday_provider(MissingApiUrlConfig)
199+
175200
def test_get_holiday_provider_not_implemented(self):
176201
"""
177202
Test that a NotImplementedError is raised for a provider without init logic.

0 commit comments

Comments
 (0)