hapilibur 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hapilibur/__init__.py +25 -0
- hapilibur/__main__.py +151 -0
- hapilibur/core.py +279 -0
- hapilibur/data/cuti-bersama.json +29 -0
- hapilibur/data/jakarta-2026.json +313 -0
- hapilibur/data/libur-nasional.json +38 -0
- hapilibur/data/surabaya-2026.json +313 -0
- hapilibur/py.typed +0 -0
- hapilibur-0.2.0.dist-info/METADATA +162 -0
- hapilibur-0.2.0.dist-info/RECORD +14 -0
- hapilibur-0.2.0.dist-info/WHEEL +5 -0
- hapilibur-0.2.0.dist-info/entry_points.txt +2 -0
- hapilibur-0.2.0.dist-info/licenses/LICENSE +21 -0
- hapilibur-0.2.0.dist-info/top_level.txt +1 -0
hapilibur/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""hapilibur — dataset & bantu hitung hari libur nasional dan cuti bersama Indonesia.
|
|
2
|
+
|
|
3
|
+
Sumber: SKB 3 Menteri (Hari Libur Nasional & Cuti Bersama) dan Imsakiyah Bimas Islam Kemenag.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .core import ( # noqa: F401
|
|
7
|
+
CUTI_FILE,
|
|
8
|
+
LIBUR_FILE,
|
|
9
|
+
available_cities,
|
|
10
|
+
between,
|
|
11
|
+
check,
|
|
12
|
+
check_detail,
|
|
13
|
+
cuti,
|
|
14
|
+
holiday_range,
|
|
15
|
+
imsak,
|
|
16
|
+
imsak_cities_available,
|
|
17
|
+
is_holiday,
|
|
18
|
+
is_libur,
|
|
19
|
+
libur,
|
|
20
|
+
month,
|
|
21
|
+
to_csv,
|
|
22
|
+
upcoming,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__version__ = "0.2.0"
|
hapilibur/__main__.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""CLI hapilibur: cek hari libur, cuti bersama, dan jadwal imsakiyah dari terminal."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import datetime
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from . import __version__, core
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _emit(entries: list[dict] | dict, as_json: bool, as_csv: bool) -> None:
|
|
13
|
+
if as_json:
|
|
14
|
+
print(json.dumps(entries, ensure_ascii=False, indent=2))
|
|
15
|
+
return
|
|
16
|
+
if as_csv:
|
|
17
|
+
items = entries if isinstance(entries, list) else [entries]
|
|
18
|
+
print(core.to_csv([e for e in items if e.get("date")]), end="")
|
|
19
|
+
return
|
|
20
|
+
if isinstance(entries, dict):
|
|
21
|
+
print(f"{entries.get('date')} {entries.get('name')}")
|
|
22
|
+
return
|
|
23
|
+
for entry in entries:
|
|
24
|
+
print(f"{entry['date']} {entry['name']}")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _fmt_date(value: str, as_json: bool) -> None:
|
|
28
|
+
detail = core.check_detail(value)
|
|
29
|
+
if as_json:
|
|
30
|
+
print(json.dumps(detail or {"date": value, "is_holiday": False}, ensure_ascii=False, indent=2))
|
|
31
|
+
return
|
|
32
|
+
print(f"{value}: {detail['name'] if detail else 'Bukan hari libur nasional / cuti bersama'}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _print_holidays(entries: list[dict], year: int | None, as_json: bool, as_csv: bool) -> None:
|
|
36
|
+
if not entries:
|
|
37
|
+
label = f"tahun {year}" if year else "rentang tersebut"
|
|
38
|
+
if as_json:
|
|
39
|
+
print("[]")
|
|
40
|
+
elif not as_csv:
|
|
41
|
+
print(f"Tidak ada data untuk {label}.")
|
|
42
|
+
else:
|
|
43
|
+
print("tanggal,nama")
|
|
44
|
+
return
|
|
45
|
+
_emit(entries, as_json, as_csv)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _print_imsak(payload: dict, as_json: bool) -> None:
|
|
49
|
+
if as_json:
|
|
50
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
51
|
+
return
|
|
52
|
+
print(f"{payload['title']} ({payload['timezone']})")
|
|
53
|
+
print(f"{'Hari':<5}{'Tanggal':<12}{'Imsak':<7}{'Subuh':<7}{'Zuhur':<7}{'Ashar':<7}{'Magrib':<7}{'Isya':<7}")
|
|
54
|
+
for item in payload["schedule"]:
|
|
55
|
+
print(
|
|
56
|
+
f"{item['day']:<5}{item['date']:<12}{item['imsak']:<7}{item['subuh']:<7}"
|
|
57
|
+
f"{item['zuhur']:<7}{item['ashar']:<7}{item['magrib']:<7}{item['isya']:<7}"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _add_output_flags(parser: argparse.ArgumentParser) -> None:
|
|
62
|
+
parser.add_argument("--json", action="store_true", help="keluarkan sebagai JSON")
|
|
63
|
+
parser.add_argument("--csv", action="store_true", help="keluarkan sebagai CSV")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
67
|
+
parser = argparse.ArgumentParser(
|
|
68
|
+
prog="hapilibur",
|
|
69
|
+
description="Cek hari libur nasional, cuti bersama, dan jadwal imsakiyah Indonesia.",
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
72
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
73
|
+
|
|
74
|
+
p_check = sub.add_parser("check", help="cek apakah suatu tanggal hari libur")
|
|
75
|
+
p_check.add_argument("tanggal", help="tanggal dalam format YYYY-MM-DD, misal 2026-08-17")
|
|
76
|
+
p_check.add_argument("--json", action="store_true", help="keluarkan sebagai JSON")
|
|
77
|
+
|
|
78
|
+
p_tahun = sub.add_parser("tahun", help="daftar hari libur nasional satu tahun")
|
|
79
|
+
p_tahun.add_argument("tahun", nargs="?", type=int, help="tahun (default: tahun berjalan)")
|
|
80
|
+
_add_output_flags(p_tahun)
|
|
81
|
+
|
|
82
|
+
p_cuti = sub.add_parser("cuti", help="daftar cuti bersama satu tahun")
|
|
83
|
+
p_cuti.add_argument("tahun", nargs="?", type=int, help="tahun (default: tahun berjalan)")
|
|
84
|
+
_add_output_flags(p_cuti)
|
|
85
|
+
|
|
86
|
+
p_bulan = sub.add_parser("bulan", help="daftar tanggal libur satu bulan")
|
|
87
|
+
p_bulan.add_argument("tahun", type=int, help="tahun, misal 2026")
|
|
88
|
+
p_bulan.add_argument("bulan", type=int, help="bulan 1-12, misal 3")
|
|
89
|
+
p_bulan.add_argument("--tanpa-cuti", action="store_true", help="kecualikan cuti bersama")
|
|
90
|
+
_add_output_flags(p_bulan)
|
|
91
|
+
|
|
92
|
+
p_selang = sub.add_parser("selang", help="daftar tanggal libur dalam rentang tanggal", aliases=["range"])
|
|
93
|
+
p_selang.add_argument("dari", help="tanggal awal YYYY-MM-DD")
|
|
94
|
+
p_selang.add_argument("sampai", help="tanggal akhir YYYY-MM-DD")
|
|
95
|
+
_add_output_flags(p_selang)
|
|
96
|
+
|
|
97
|
+
p_next = sub.add_parser("upcoming", help="hari libur berikutnya", aliases=["next"])
|
|
98
|
+
p_next.add_argument("tanggal", nargs="?", help="awal pencarian (default: hari ini)")
|
|
99
|
+
p_next.add_argument("-n", "--count", type=int, default=1, help="jumlah hari libur (default: 1)")
|
|
100
|
+
p_next.add_argument("--json", action="store_true", help="keluarkan sebagai JSON")
|
|
101
|
+
|
|
102
|
+
p_imsak = sub.add_parser("imsak", help="jadwal imsakiyah sebuah kota")
|
|
103
|
+
p_imsak.add_argument("kota", help="nama kota, misal jakarta atau surabaya")
|
|
104
|
+
p_imsak.add_argument("tahun", nargs="?", type=int, help="tahun (default: tahun berjalan)")
|
|
105
|
+
p_imsak.add_argument("--json", action="store_true", help="keluarkan sebagai JSON")
|
|
106
|
+
|
|
107
|
+
sub.add_parser("kota", help="daftar kota yang tersedia")
|
|
108
|
+
|
|
109
|
+
return parser
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main(argv: list[str] | None = None) -> int:
|
|
113
|
+
args = _build_parser().parse_args(argv)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
if args.command == "check":
|
|
117
|
+
_fmt_date(args.tanggal, as_json=args.json)
|
|
118
|
+
elif args.command == "tahun":
|
|
119
|
+
year = args.tahun or datetime.date.today().year
|
|
120
|
+
_print_holidays(core.libur(year), year, args.json, args.csv)
|
|
121
|
+
elif args.command == "cuti":
|
|
122
|
+
year = args.tahun or datetime.date.today().year
|
|
123
|
+
_print_holidays(core.cuti(year), year, args.json, args.csv)
|
|
124
|
+
elif args.command == "bulan":
|
|
125
|
+
entries = core.month(args.tahun, args.bulan, include_cuti=not args.tanpa_cuti)
|
|
126
|
+
_print_holidays(entries, args.tahun, args.json, args.csv)
|
|
127
|
+
elif args.command in ("selang", "range"):
|
|
128
|
+
_print_holidays(core.between(args.dari, args.sampai), None, args.json, args.csv)
|
|
129
|
+
elif args.command in ("upcoming", "next"):
|
|
130
|
+
result = core.upcoming(args.tanggal, n=args.count)
|
|
131
|
+
if args.json:
|
|
132
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
133
|
+
elif args.count == 1:
|
|
134
|
+
assert isinstance(result, dict)
|
|
135
|
+
print(f"{result['date']} {result['name']}")
|
|
136
|
+
else:
|
|
137
|
+
assert isinstance(result, list)
|
|
138
|
+
for item in result:
|
|
139
|
+
print(f"{item['date']} {item['name']}")
|
|
140
|
+
elif args.command == "imsak":
|
|
141
|
+
_print_imsak(core.imsak(args.kota, args.tahun), as_json=args.json)
|
|
142
|
+
elif args.command == "kota":
|
|
143
|
+
print(", ".join(core.available_cities()))
|
|
144
|
+
except ValueError as exc:
|
|
145
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
146
|
+
return 2
|
|
147
|
+
return 0
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
sys.exit(main())
|
hapilibur/core.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Fungsi inti hapilibur: cek hari libur nasional, cuti bersama, dan imsakiyah.
|
|
2
|
+
|
|
3
|
+
Semua data diambil dari file JSON yang dibundel dalam package
|
|
4
|
+
(lihat folder ``hapilibur/data``) — data bisa dipakai lintas bahasa.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import datetime as _dt
|
|
9
|
+
import functools
|
|
10
|
+
import json
|
|
11
|
+
from importlib.resources import files
|
|
12
|
+
|
|
13
|
+
LIBUR_FILE = "libur-nasional.json"
|
|
14
|
+
CUTI_FILE = "cuti-bersama.json"
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"LIBUR_FILE",
|
|
18
|
+
"CUTI_FILE",
|
|
19
|
+
"available_cities",
|
|
20
|
+
"between",
|
|
21
|
+
"check",
|
|
22
|
+
"check_detail",
|
|
23
|
+
"cuti",
|
|
24
|
+
"holiday_range",
|
|
25
|
+
"imsak",
|
|
26
|
+
"imsak_cities_available",
|
|
27
|
+
"is_holiday",
|
|
28
|
+
"is_libur",
|
|
29
|
+
"libur",
|
|
30
|
+
"month",
|
|
31
|
+
"to_csv",
|
|
32
|
+
"upcoming",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
_DATE_FMT = "%Y-%m-%d"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@functools.lru_cache(maxsize=64)
|
|
39
|
+
def _load(name: str) -> dict:
|
|
40
|
+
try:
|
|
41
|
+
data = files("hapilibur").joinpath("data").joinpath(name).read_text(encoding="utf-8")
|
|
42
|
+
except FileNotFoundError as exc:
|
|
43
|
+
raise FileNotFoundError(f"File data '{name}' tidak ditemukan di package hapilibur.") from exc
|
|
44
|
+
try:
|
|
45
|
+
return json.loads(data)
|
|
46
|
+
except json.JSONDecodeError as exc:
|
|
47
|
+
raise ValueError(f"File data '{name}' rusak (JSON tidak valid): {exc}") from exc
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _parse_date(value: str | _dt.date | _dt.datetime) -> _dt.date:
|
|
51
|
+
if isinstance(value, _dt.datetime):
|
|
52
|
+
return value.date()
|
|
53
|
+
if isinstance(value, _dt.date):
|
|
54
|
+
return value
|
|
55
|
+
if not isinstance(value, str):
|
|
56
|
+
raise TypeError(f"Tanggal harus str/YYYY-MM-DD, date, atau datetime — dapat {type(value).__name__}")
|
|
57
|
+
text = value.strip()
|
|
58
|
+
try:
|
|
59
|
+
return _dt.datetime.strptime(text, _DATE_FMT).date()
|
|
60
|
+
except ValueError as exc:
|
|
61
|
+
raise ValueError(f"Tanggal '{value}' tidak valid — gunakan format YYYY-MM-DD (contoh: 2026-08-17).") from exc
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _year_entries(name: str, year: int) -> list[dict]:
|
|
65
|
+
"""Ambil daftar {date, name} untuk satu tahun dari file JSON dataset."""
|
|
66
|
+
if not isinstance(year, int) or year < 1 or year > 9999:
|
|
67
|
+
raise ValueError(f"Tahun '{year}' tidak valid — harus bilangan 1-9999.")
|
|
68
|
+
payload = _load(name)
|
|
69
|
+
return list(payload.get("years", {}).get(str(year), {}).get("holidays", []))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _dates(name: str, year: int) -> dict[_dt.date, str]:
|
|
73
|
+
return {_parse_date(entry["date"]): entry["name"] for entry in _year_entries(name, year)}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _all_dates(year: int | None = None, include_cuti: bool = True) -> dict[_dt.date, str]:
|
|
77
|
+
"""Gabungan hari libur nasional + (opsional) cuti bersama."""
|
|
78
|
+
year = year or _dt.date.today().year
|
|
79
|
+
merged: dict[_dt.date, str] = {}
|
|
80
|
+
merged.update(_dates(LIBUR_FILE, year))
|
|
81
|
+
if include_cuti:
|
|
82
|
+
for d, name in _dates(CUTI_FILE, year).items():
|
|
83
|
+
if d in merged:
|
|
84
|
+
merged[d] = f"{merged[d]} • {name}"
|
|
85
|
+
else:
|
|
86
|
+
merged[d] = name
|
|
87
|
+
return merged
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _kinds(year: int) -> dict[_dt.date, set[str]]:
|
|
91
|
+
"""Petakan tanggal -> {'libur_nasional', 'cuti_bersama'}."""
|
|
92
|
+
kinds: dict[_dt.date, set[str]] = {}
|
|
93
|
+
for entry in _year_entries(LIBUR_FILE, year):
|
|
94
|
+
kinds.setdefault(_parse_date(entry["date"]), set()).add("libur_nasional")
|
|
95
|
+
for entry in _year_entries(CUTI_FILE, year):
|
|
96
|
+
kinds.setdefault(_parse_date(entry["date"]), set()).add("cuti_bersama")
|
|
97
|
+
return kinds
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def libur(year: int | None = None) -> list[dict]:
|
|
101
|
+
"""Daftar hari libur nasional (lengkap dengan tanggal) dalam satu tahun.
|
|
102
|
+
|
|
103
|
+
>>> libur(2026)[0] # doctest: +SKIP
|
|
104
|
+
{'date': '2026-01-01', 'name': 'Tahun Baru 2026 Masehi'}
|
|
105
|
+
"""
|
|
106
|
+
year = year or _dt.date.today().year
|
|
107
|
+
return list(_year_entries(LIBUR_FILE, year))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def cuti(year: int | None = None) -> list[dict]:
|
|
111
|
+
"""Daftar cuti bersama nasional dalam satu tahun."""
|
|
112
|
+
year = year or _dt.date.today().year
|
|
113
|
+
return list(_year_entries(CUTI_FILE, year))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def month(year: int, month_number: int, include_cuti: bool = True) -> list[dict]:
|
|
117
|
+
"""Daftar tanggal libur (nasional + cuti bersama) dalam satu bulan sebuah tahun.
|
|
118
|
+
|
|
119
|
+
``month_number`` 1-12 (Januari-Desember).
|
|
120
|
+
|
|
121
|
+
>>> month(2026, 3)[:1] # doctest: +SKIP
|
|
122
|
+
[{'date': '2026-03-18', 'name': 'Cuti Bersama Hari Suci Nyepi'}]
|
|
123
|
+
"""
|
|
124
|
+
if not 1 <= month_number <= 12:
|
|
125
|
+
raise ValueError("month_number harus 1-12")
|
|
126
|
+
return [
|
|
127
|
+
{"date": date.isoformat(), "name": name}
|
|
128
|
+
for date, name in sorted(_all_dates(year, include_cuti=include_cuti).items())
|
|
129
|
+
if date.year == year and date.month == month_number
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def between(
|
|
134
|
+
start: str | _dt.date | _dt.datetime,
|
|
135
|
+
end: str | _dt.date | _dt.datetime,
|
|
136
|
+
include_cuti: bool = True,
|
|
137
|
+
) -> list[dict]:
|
|
138
|
+
"""Daftar tanggal libur (nasional + cuti bersama) dalam rentang tanggal inklusif.
|
|
139
|
+
|
|
140
|
+
>>> len(between("2026-08-01", "2026-08-31")) # doctest: +SKIP
|
|
141
|
+
1
|
|
142
|
+
"""
|
|
143
|
+
start_date = _parse_date(start)
|
|
144
|
+
end_date = _parse_date(end)
|
|
145
|
+
if end_date < start_date:
|
|
146
|
+
start_date, end_date = end_date, start_date
|
|
147
|
+
result: list[dict] = []
|
|
148
|
+
for year in range(start_date.year, end_date.year + 1):
|
|
149
|
+
for date, name in sorted(_all_dates(year, include_cuti=include_cuti).items()):
|
|
150
|
+
if start_date <= date <= end_date:
|
|
151
|
+
result.append({"date": date.isoformat(), "name": name})
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def holiday_range(
|
|
156
|
+
start: str | _dt.date | _dt.datetime, end: str | _dt.date | _dt.datetime
|
|
157
|
+
) -> list[dict]:
|
|
158
|
+
"""Alias bahasa Inggris dari :func:`between`."""
|
|
159
|
+
return between(start, end)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def is_libur(value: str | _dt.date | _dt.datetime, include_cuti: bool = True) -> bool:
|
|
163
|
+
"""True bila ``value`` adalah hari libur (nasional + opsional cuti bersama).
|
|
164
|
+
|
|
165
|
+
.. versionchanged:: 0.2.0
|
|
166
|
+
Parameter ``include_cuti`` kini benar-benar dihormati
|
|
167
|
+
(sebelumnya selalu True).
|
|
168
|
+
"""
|
|
169
|
+
tanggal = _parse_date(value)
|
|
170
|
+
if include_cuti:
|
|
171
|
+
return tanggal in _all_dates(tanggal.year, include_cuti=True)
|
|
172
|
+
return tanggal in _dates(LIBUR_FILE, tanggal.year)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def is_holiday(value: str | _dt.date | _dt.datetime) -> bool:
|
|
176
|
+
"""Alias bahasa Inggris dari :func:`is_libur` (termasuk cuti bersama)."""
|
|
177
|
+
return is_libur(value, include_cuti=True)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def check(value: str | _dt.date | _dt.datetime) -> str | None:
|
|
181
|
+
"""Nama hari libur untuk tanggal tertentu, atau ``None`` bila bukan."""
|
|
182
|
+
tanggal = _parse_date(value)
|
|
183
|
+
return _all_dates(tanggal.year).get(tanggal)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def check_detail(value: str | _dt.date | _dt.datetime) -> dict | None:
|
|
187
|
+
"""Detail hari libur: ``{date, name, jenis[]}`` atau ``None``.
|
|
188
|
+
|
|
189
|
+
``jenis`` berisi kombinasi ``libur_nasional`` / ``cuti_bersama``.
|
|
190
|
+
Baru di 0.2.0.
|
|
191
|
+
"""
|
|
192
|
+
tanggal = _parse_date(value)
|
|
193
|
+
name = _all_dates(tanggal.year).get(tanggal)
|
|
194
|
+
if name is None:
|
|
195
|
+
return None
|
|
196
|
+
kinds = sorted(_kinds(tanggal.year).get(tanggal, set()))
|
|
197
|
+
return {"date": tanggal.isoformat(), "name": name, "jenis": kinds}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def upcoming(value: str | _dt.date | _dt.datetime | None = None, n: int = 1) -> dict | list[dict]:
|
|
201
|
+
"""Satu atau beberapa hari libur berikutnya (>= tanggal yang diberikan).
|
|
202
|
+
|
|
203
|
+
``n=1`` mengembalikan dict; ``n>1`` mengembalikan list dict ber-urutan tanggal.
|
|
204
|
+
Bila tidak ada data, ``n=1`` mengembalikan ``{"date": None, "name": None}``
|
|
205
|
+
dan ``n>1`` mengembalikan list kosong.
|
|
206
|
+
"""
|
|
207
|
+
if n < 1:
|
|
208
|
+
raise ValueError("n harus >= 1")
|
|
209
|
+
start = _parse_date(value) if value else _dt.date.today()
|
|
210
|
+
found: list[dict] = []
|
|
211
|
+
for year in range(start.year, start.year + 5):
|
|
212
|
+
if len(found) >= n:
|
|
213
|
+
break
|
|
214
|
+
for date, name in sorted(_all_dates(year).items()):
|
|
215
|
+
if date < start:
|
|
216
|
+
continue
|
|
217
|
+
found.append({"date": date.isoformat(), "name": name})
|
|
218
|
+
if len(found) >= n:
|
|
219
|
+
break
|
|
220
|
+
if n == 1:
|
|
221
|
+
return found[0] if found else {"date": None, "name": None}
|
|
222
|
+
return found
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def to_csv(entries: list[dict], jenis_default: str = "libur") -> str:
|
|
226
|
+
"""Ubah daftar ``{date, name}`` menjadi string CSV ``tanggal,nama``.
|
|
227
|
+
|
|
228
|
+
Baru di 0.2.0 — untuk CLI ``--csv`` dan analisis cepat.
|
|
229
|
+
"""
|
|
230
|
+
lines = ["tanggal,nama"]
|
|
231
|
+
for entry in entries:
|
|
232
|
+
name = str(entry.get("name", "")).replace('"', '""')
|
|
233
|
+
lines.append(f"{entry.get('date', '')},\"{name}\"")
|
|
234
|
+
return "\n".join(lines) + ("\n" if entries else "")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def imsak(city: str, year: int | None = None) -> dict:
|
|
238
|
+
"""Jadwal imsakiyah (imsak, subuh, zuhur, ashar, magrib, isya) untuk sebuah kota.
|
|
239
|
+
|
|
240
|
+
``city`` tidak case-sensitive dan boleh tanpa spasi, contoh: ``"jakarta"``, ``"Surabaya"``.
|
|
241
|
+
"""
|
|
242
|
+
year = year or _dt.date.today().year
|
|
243
|
+
if not isinstance(year, int) or year < 1 or year > 9999:
|
|
244
|
+
raise ValueError(f"Tahun '{year}' tidak valid — harus bilangan 1-9999.")
|
|
245
|
+
slug = "".join(ch for ch in city.strip().lower() if ch.isalnum())
|
|
246
|
+
if not slug:
|
|
247
|
+
raise ValueError("Nama kota tidak boleh kosong.")
|
|
248
|
+
try:
|
|
249
|
+
return _load(f"{slug}-{year}.json")
|
|
250
|
+
except FileNotFoundError as exc:
|
|
251
|
+
raise ValueError(
|
|
252
|
+
f"Belum ada jadwal imsakiyah untuk kota {city!r} tahun {year}. "
|
|
253
|
+
f"Kota tersedia: {', '.join(available_cities())}."
|
|
254
|
+
) from exc
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def available_cities() -> list[str]:
|
|
258
|
+
"""Daftar kota yang tersedia pada data imsakiyah."""
|
|
259
|
+
excluded = {LIBUR_FILE[:-5], CUTI_FILE[:-5]}
|
|
260
|
+
cities = set()
|
|
261
|
+
try:
|
|
262
|
+
members = list(files("hapilibur").joinpath("data").iterdir())
|
|
263
|
+
except FileNotFoundError:
|
|
264
|
+
return []
|
|
265
|
+
for member in members:
|
|
266
|
+
if member.suffix != ".json":
|
|
267
|
+
continue
|
|
268
|
+
stem = member.stem
|
|
269
|
+
if stem in excluded:
|
|
270
|
+
continue
|
|
271
|
+
city, sep, year = stem.rpartition("-")
|
|
272
|
+
if sep and city and year.isdigit():
|
|
273
|
+
cities.add(city.title())
|
|
274
|
+
return sorted(cities)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def imsak_cities_available() -> list[str]:
|
|
278
|
+
"""Alias dari :func:`available_cities`."""
|
|
279
|
+
return available_cities()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "1.0",
|
|
3
|
+
"title": "Cuti Bersama Indonesia",
|
|
4
|
+
"country": "Indonesia",
|
|
5
|
+
"country_code": "ID",
|
|
6
|
+
"updated_at": "2026-09-12T00:00:00Z",
|
|
7
|
+
"sources": [
|
|
8
|
+
{
|
|
9
|
+
"name": "SKB 3 Menteri (Menag, Menaker, MenPAN-RB) Nomor 1497, 2, 5 Tahun 2025",
|
|
10
|
+
"title": "Hari Libur Nasional dan Cuti Bersama Tahun 2026",
|
|
11
|
+
"url": "https://www.kemenkopmk.go.id/sites/default/files/pengumuman/2025-09/SKB%20Libur%20Nasional%20dan%20Cuti%20Bersama%20Tahun%202026.pdf"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"years": {
|
|
15
|
+
"2026": {
|
|
16
|
+
"count": 8,
|
|
17
|
+
"holidays": [
|
|
18
|
+
{ "date": "2026-02-16", "name": "Cuti Bersama Tahun Baru Imlek 2577 Kongzili" },
|
|
19
|
+
{ "date": "2026-03-18", "name": "Cuti Bersama Hari Suci Nyepi" },
|
|
20
|
+
{ "date": "2026-03-20", "name": "Cuti Bersama Hari Raya Idul Fitri 1447 H" },
|
|
21
|
+
{ "date": "2026-03-23", "name": "Cuti Bersama Hari Raya Idul Fitri 1447 H" },
|
|
22
|
+
{ "date": "2026-03-24", "name": "Cuti Bersama Hari Raya Idul Fitri 1447 H" },
|
|
23
|
+
{ "date": "2026-05-15", "name": "Cuti Bersama Kenaikan Yesus Kristus" },
|
|
24
|
+
{ "date": "2026-05-28", "name": "Cuti Bersama Hari Raya Idul Adha 1447 H" },
|
|
25
|
+
{ "date": "2026-12-24", "name": "Cuti Bersama Kelahiran Yesus Kristus" }
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|