Spaces:
Running
Running
File size: 5,556 Bytes
b7c46f4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | """
Nigeria's national calendar as events: federal public holidays, the Islamic
holidays and Ramadan (computed, so marked is_estimate), and the December festive
trading season. Generated rather than scraped, so history back to 2000 costs
nothing — which matters, because the correlation step learns from history.
Known limits, kept honest by is_estimate and the design doc:
- Islamic dates use the Umm al-Qura calendar; Nigeria's dates follow the
Sultan of Sokoto's moon-sighting announcement and can differ by a day.
- The Federal Government sometimes declares extra or substitute days (e.g. a
Monday when a holiday falls on Sunday, or a one-off holiday). Those aren't
predictable; the news pipeline is where declarations will be caught.
- School terms vary by state and aren't included yet.
"""
import sqlite3
from datetime import date, timedelta
from hijridate import Gregorian, Hijri
from intel.db import now_iso
from intel.freshness import record_attempt
from intel.handoff import DATA_STREAM, INTERFACE, produce
SOURCE_ID = "calendar"
COUNTRY = "ng"
FIRST_YEAR = 2000
YEARS_AHEAD = 10
def easter(year: int) -> date:
"""Western Easter Sunday (anonymous Gregorian algorithm)."""
a, b, c = year % 19, year // 100, year % 100
d, e = b // 4, b % 4
f = (b + 8) // 25
g = (b - f + 1) // 3
h = (19 * a + b - d - g + 15) % 30
i, k = c // 4, c % 4
l = (32 + 2 * e + 2 * i - h - k) % 7
m = (a + 11 * h + 22 * l) // 451
month = (h + l - 7 * m + 114) // 31
day = (h + l - 7 * m + 114) % 31 + 1
return date(year, month, day)
def _hijri_to_greg(hy: int, hm: int, hd: int) -> date:
return Hijri(hy, hm, hd).to_gregorian()
def _hijri_years_overlapping(year: int) -> range:
"""The Hijri years whose dates can fall in this Gregorian year (a Hijri year
is ~11 days shorter, so it's always the one in progress on 1 January and the
next)."""
hy = Gregorian(year, 1, 1).to_hijri().year
return range(hy, hy + 2)
def events_for_year(year: int) -> list[dict]:
"""[{key, type, title, starts_on, ends_on, is_estimate}] for one Gregorian year."""
out = []
def add(key, type_, title, start, end=None, estimate=False):
if start.year == year or (end and end.year == year):
out.append({"key": key, "type": type_, "title": title, "starts_on": start, "ends_on": end or start,
"is_estimate": estimate})
add("new_year", "public_holiday", "New Year's Day", date(year, 1, 1))
gf = easter(year) - timedelta(days=2)
add("good_friday", "public_holiday", "Good Friday", gf)
add("easter_monday", "public_holiday", "Easter Monday", gf + timedelta(days=3))
add("workers_day", "public_holiday", "Workers' Day", date(year, 5, 1))
# Democracy Day moved from 29 May to 12 June from 2019.
add("democracy_day", "public_holiday", "Democracy Day", date(year, 6, 12) if year >= 2019 else date(year, 5, 29))
add("independence_day", "public_holiday", "Independence Day", date(year, 10, 1))
add("christmas", "public_holiday", "Christmas Day", date(year, 12, 25))
add("boxing_day", "public_holiday", "Boxing Day", date(year, 12, 26))
# Festive trading season: VenThrive's own window for the December spending peak.
add("festive_season", "season", "Festive trading season", date(year, 12, 15), date(year + 1, 1, 5), estimate=True)
for hy in _hijri_years_overlapping(year):
ramadan_start = _hijri_to_greg(hy, 9, 1)
eid_fitr = _hijri_to_greg(hy, 10, 1)
add("ramadan", "season", "Ramadan", ramadan_start, eid_fitr - timedelta(days=1), estimate=True)
add("eid_al_fitr", "religious_holiday", "Eid al-Fitr", eid_fitr, eid_fitr + timedelta(days=1), estimate=True)
eid_adha = _hijri_to_greg(hy, 12, 10)
add("eid_al_adha", "religious_holiday", "Eid al-Adha", eid_adha, eid_adha + timedelta(days=1), estimate=True)
add("mawlid", "religious_holiday", "Eid-el-Maulud (Mawlid)", _hijri_to_greg(hy, 3, 12), estimate=True)
return out
def ingest(con: sqlite3.Connection, first_year: int = FIRST_YEAR, last_year: int | None = None,
run_id: str = "manual") -> dict:
last_year = last_year or date.today().year + YEARS_AHEAD
now = now_iso()
n = 0
try:
for year in range(first_year, last_year + 1):
for e in events_for_year(year):
ref = f"{e['key']}:{e['starts_on'].isoformat()}"
con.execute(
"""insert into events (id, family, type, location_id, starts_on, ends_on, title, detail,
is_estimate, source_id, source_ref, ingested_at)
values (?, 'calendar', ?, ?, ?, ?, ?, null, ?, ?, ?, ?)
on conflict (source_id, source_ref) do update set title = excluded.title,
ends_on = excluded.ends_on, is_estimate = excluded.is_estimate""",
(f"{SOURCE_ID}:{ref}", e["type"], COUNTRY, e["starts_on"].isoformat(), e["ends_on"].isoformat(),
e["title"], int(e["is_estimate"]), SOURCE_ID, ref, now),
)
n += 1
con.commit()
except Exception as e:
record_attempt(con, SOURCE_ID, ok=False, error=str(e))
raise
record_attempt(con, SOURCE_ID, ok=True, latest_data_period=f"{last_year}-12-31")
summary = {"years": f"{first_year}-{last_year}", "events": n}
produce(con, run_id, DATA_STREAM, INTERFACE, {"kind": "calendar", **summary})
return summary
|