diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b70980f..24a8129 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,3 +5,13 @@ repos: - id: ruff args: [--fix, --exit-non-zero-on-fix] - id: ruff-format +- repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + additional_dependencies: ["tomli"] +- repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v1.8.0' + hooks: + - id: mypy + exclude: '^tests/' diff --git a/README.md b/README.md index 5fb6311..ddc1a1e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,61 @@ -### Bikram Sambat Calendar +## Bikram Sambat Calendar -#### Usage +### Installation +1. Using pip ```console pip install bscal -bscal ``` + +2. Using pipx + +```console +pipx install bscal +``` + +### Usage + + +#### Display calendar in BS format + +```console +$ bscal + Poush 2080 +Su Mo Tu We Th Fr Sa + 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 +``` + +> [!TIP] +> You can provide `year` to display a calendar for that year, and also optionally specify the `month` for a specific month's calendar. + +#### Display the current BS date: + +```console +$ bsdate +Tue Poush 17 09:14:14 +0545 2080 +``` + +#### Display a specific date (from given ISO format) + +```console +$ bsdate 2016-09-08 +Thu Bhadra 23 09:17:21 +0545 2073 +``` + +#### Display a specific date (represented as a Unix timestamp): + +```console +$ bsdate 1473305798 +Thu Bhadra 23 09:21:38 +0545 2073 +``` + +#### Convert BS date to AD format + +```console +$ bsdate -c 2073-05-23 +Thu Sep 8 09:19:00 +0545 2016 +``` \ No newline at end of file diff --git a/bscal.py b/bscal.py index 558e0aa..6ae180c 100644 --- a/bscal.py +++ b/bscal.py @@ -1,7 +1,8 @@ import calendar -from datetime import date, timedelta +import contextlib +from datetime import date, datetime, timedelta from itertools import repeat -from typing import Optional, Tuple +from typing import Iterator, Optional, Sequence, Tuple lookup = { # bs_year: [days_in_each_month, ...] @@ -316,19 +317,21 @@ def ad_to_bs(ad: date, start: Optional[int] = None) -> Tuple[int, int, int]: class BSCalendar(calendar.TextCalendar): - def __init__(self, firstweekday: int = 0, to_highlight=()) -> None: + def __init__(self, firstweekday: int = 0, to_highlight: Tuple[int, ...] = ()): super().__init__(firstweekday) self._to_highlight = to_highlight - self._formatting = () + self._formatting_ctx: Tuple[int, ...] = () - def formatmonthname(self, theyear, themonth, width, withyear=True): + def formatmonthname( + self, theyear: int, themonth: int, width: int, withyear: bool = True + ) -> str: """Return a formatted month name.""" s = months[themonth - 1] if withyear: s = f"{s} {theyear!r}" return s.center(width) - def itermonthdays(self, year, month): + def itermonthdays(self, year: int, month: int) -> Iterator[int]: """Like itermonthdates(), but will yield day numbers. For days outside the specified month the day number is 0. """ @@ -342,17 +345,63 @@ def itermonthdays(self, year, month): yield from repeat(0, days_after) def formatmonth(self, theyear: int, themonth: int, w: int = 0, l: int = 0) -> str: # noqa: E741 - self._formatting = (theyear, themonth) + self._formatting_ctx = (theyear, themonth) return super().formatmonth(theyear, themonth, w, l) def formatday(self, day: int, weekday: int, width: int) -> str: s = super().formatday(day, weekday, width) - if (*self._formatting, day) == self._to_highlight: + if (*self._formatting_ctx, day) == self._to_highlight: s = f"\033[30;47m{s}\033[0m" return s -def main(args=None): +def bsconv(bs_datestring: str) -> None: + ad = bs_to_ad(*map(int, bs_datestring.split("-")[:3])) + ad_tz = datetime.combine(ad, datetime.now().time()).astimezone() + print(ad_tz.strftime("%a %b %e %T %Z %Y")) + + +def bsdate(args: Optional[Sequence[str]] = None) -> None: + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument( + "-c", + required=False, + metavar="", + help="Convert BS date to AD format", + dest="convert", + ) + parser.add_argument( + "date", + nargs="?", + metavar="STRING", + help="Convert given datetime string (isoformat/unix-timestamp) to BS format", + ) + opt = parser.parse_args(args) + if opt.convert: + bsconv(opt.convert) + return + + dt: Optional[datetime] = None + if opt.date: + with contextlib.suppress(ValueError): + dt = datetime.fromtimestamp(int(opt.date.lstrip("@"))) + if not dt and opt.date: + try: + _date = date.fromisoformat(opt.date) + except ValueError: + dt = datetime.fromisoformat(opt.date) + else: + dt = datetime.combine(_date, datetime.now().time()) + + dt = dt or datetime.now() + dt = dt.astimezone() + year, month, day = ad_to_bs(dt.date()) + print(dt.strftime("%a"), months[month - 1], day, dt.strftime("%T %Z"), year) + + +def cal(args: Optional[Sequence[str]] = None) -> None: import argparse import sys @@ -364,16 +413,16 @@ def main(args=None): type=int, help="month number (1-12, text only)", ) - args = parser.parse_args(args) + opt = parser.parse_args(args) year, month, day = ad_to_bs(date.today()) cal = BSCalendar(6, to_highlight=(year, month, day) if sys.stdout.isatty() else ()) - if not args.year or args.month: - result = cal.formatmonth(args.year or year, args.month or month) + if not opt.year or opt.month: + result = cal.formatmonth(opt.year or year, opt.month or month) else: - result = cal.formatyear(args.year) + result = cal.formatyear(opt.year) print(result) if __name__ == "__main__": - main() + cal() diff --git a/pyproject.toml b/pyproject.toml index e4c727c..3950515 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,8 @@ classifiers = ["Programming Language :: Python :: 3"] dynamic = ["version"] [project.scripts] -bscal = "bscal:main" +bscal = "bscal:cal" +bsdate = "bscal:bsdate" [project.optional-dependencies] tests = ["pytest", "hypothesis"] @@ -22,6 +23,11 @@ license-files = ["LICENSE"] [tool.setuptools_scm] +[tool.mypy] +check_untyped_defs = true +files = ["bscal.py"] +strict = true + [tool.ruff] ignore = ["ISC001", "S101"] select = [ @@ -33,6 +39,9 @@ select = [ show-source = true show-fixes = true +[tool.ruff.format] +preview = true + [tool.ruff.flake8-type-checking] strict = true diff --git a/tests/test_cli.py b/tests/test_cli.py index 65875ff..86f35fd 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,6 +1,6 @@ import pytest -from bscal import main +from bscal import cal results_2080_poush = """\ Poush 2080 @@ -61,7 +61,7 @@ ], ) def test_cli(capsys, args, expected): - main(args) + cal(args) out, err = capsys.readouterr() assert out == expected assert not err