Skip to content

Commit

Permalink
Improved docs
Browse files Browse the repository at this point in the history
  • Loading branch information
dakrauth committed Feb 3, 2025
1 parent 007fd20 commit 20c0701
Show file tree
Hide file tree
Showing 12 changed files with 488 additions and 142 deletions.
6 changes: 6 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ update:
{{PIP}} install -U \
build \
pytest \
docutils \
pytest-sugar \
pytest-clarity \
freezegun \
Expand Down Expand Up @@ -132,3 +133,8 @@ strftime:
f"Format Specifiers:\n{pt.get_string()}\n\n"
"Notes:\n* - Locale-dependent\n+ - C99 extension\n! - when extension"
)
# Preview README.rst
docs:
rst2html5 --output README.html README.rst
open README.html
399 changes: 342 additions & 57 deletions README.rst

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/when/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Calculate and convert times across time zones and cities of significant population.
"""

__version__ = "3.2.1"
__version__ = "3.3"
VERSION = tuple(int(i) for i in __version__.split("."))


Expand Down
11 changes: 6 additions & 5 deletions src/when/cli.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
#!/usr/bin/env python
import argparse
import logging
import sys
import logging
from pathlib import Path

from . import __version__, core, db, utils, lunar, exceptions
from . import timezones
from .config import Settings, __doc__ as FORMAT_HELP

logger = logging.getLogger(__name__)


def get_parser(settings):
class DBSizeAction(argparse.Action):
Expand Down Expand Up @@ -227,11 +225,14 @@ def log_config(verbosity, settings):
logging.getLogger("asyncio").setLevel(logging.WARNING)

logging.basicConfig(level=log_level, format=log_format, force=True)
logger = utils.logger()
logger.debug(
"Configuration files read: %s",
", ".join(str(s) for s in settings.read_from) if settings.read_from else "None",
)

return logger


def main(sys_args, when=None, settings=None):
if "--pdb" in sys_args: # pragma: no cover
Expand All @@ -258,7 +259,7 @@ def main(sys_args, when=None, settings=None):
parser = get_parser(settings)
args = parser.parse_args(sys_args)

log_config(args.verbosity, settings)
logger = log_config(args.verbosity, settings)
logger.debug(args)
if args.help:
parser.print_help()
Expand Down Expand Up @@ -306,7 +307,7 @@ def main(sys_args, when=None, settings=None):
)
return 0

formatter = core.Formatter(settings, args.format)
formatter = when.formatter(args.format)
try:
results = when.results(
args.timestr,
Expand Down
4 changes: 2 additions & 2 deletions src/when/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
+------+--------------------------------------------------------------------------+--------------------------+------+
| %M | Minute (00-59) | 55 | |
+------+--------------------------------------------------------------------------+--------------------------+------+
| %n | New-line character ('\n') | | + |
| %n | New-line character | '\\n' | + |
+------+--------------------------------------------------------------------------+--------------------------+------+
| %p | AM or PM designation | PM | |
+------+--------------------------------------------------------------------------+--------------------------+------+
Expand All @@ -49,7 +49,7 @@
+------+--------------------------------------------------------------------------+--------------------------+------+
| %S | Second (00-61) | 02 | |
+------+--------------------------------------------------------------------------+--------------------------+------+
| %t | Horizontal-tab character ('\t') | | + |
| %t | Horizontal-tab character | '\\t' | + |
+------+--------------------------------------------------------------------------+--------------------------+------+
| %T | ISO 8601 time format (HH:MM:SS), equivalent to %H:%M:%S | 14:55:02 | + |
+------+--------------------------------------------------------------------------+--------------------------+------+
Expand Down
23 changes: 15 additions & 8 deletions src/when/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from .db import client
from .lunar import lunar_phase

logger = logging.getLogger(__name__)
logger = utils.logger()


def holidays(settings, co="US", ts=None):
Expand Down Expand Up @@ -59,9 +59,10 @@ def floating(m):
mx = 2 + max(len(t[0]) for t in results)
for title, dt in sorted(results, key=lambda o: o[1]):
delta = dt - date.today()
emoji, phase, age = lunar_phase(settings["lunar"], dt)
print(
"{:.<{}}{} ({} days) [{}]".format(
title, mx, dt.strftime(holiday_fmt), delta.days, lunar_phase(settings["lunar"], dt)
"{:.<{}}{} ({} days) [{} {}]".format(
title, mx, dt.strftime(holiday_fmt), delta.days, emoji, phase
)
)

Expand Down Expand Up @@ -128,7 +129,8 @@ def when_c(self, result):

def when_l(self, result):
"Lunar phase emoji: 🌖"
return lunar_phase(self.settings["lunar"], result.dt)
emoji, phase, age = lunar_phase(self.settings["lunar"], result.dt)
return f"{emoji} {phase}"

def c99_C(self, result):
"Year divided by 100 and truncated to integer (00-99): 20"
Expand Down Expand Up @@ -201,7 +203,7 @@ def to_dict(self, dt=None):
return {
"name": self.name or self.zone_name(dt),
"city": self.city.to_dict() if self.city else None,
"utcoffset": [offset // 3600, offset % 3600 // 60, offset % 60],
"utcoffset": [offset // 3600, offset % 3600 // 60],
}

def zone_name(self, dt=None):
Expand Down Expand Up @@ -236,11 +238,13 @@ def __init__(self, dt, zone, source=None, offset=None):
if offset:
self.dt += offset

def to_dict(self):
def to_dict(self, settings):
emoji, phase, age = lunar_phase(settings["lunar"], self.dt)
return {
"iso": self.dt.isoformat(),
"lunar": {"emoji": emoji, "phase": phase, "age": round(age, 5)},
"zone": self.zone.to_dict(self.dt),
"source": self.source.to_dict() if self.source else None,
"source": self.source.to_dict(settings) if self.source else None,
"offset": utils.format_timedelta(self.offset, short=True) if self.offset else None,
}

Expand All @@ -263,6 +267,9 @@ def __init__(self, settings, local_zone=None, db=None):
self.local_zone = local_zone or TimeZoneDetail()
self.errors = {}

def formatter(self, format="default", delta=None):
return Formatter(self.settings, format=format, delta=delta)

def get_tz(self, name):
value = self.tz_dict[name]
return (utils.gettz(value), name)
Expand Down Expand Up @@ -363,7 +370,7 @@ def as_json(
self, timestamp="", sources=None, targets=None, offset=None, exact=False, **json_kwargs
):
converts = self.results(timestamp, sources, targets, offset, exact)
return json.dumps([convert.to_dict() for convert in converts], **json_kwargs)
return json.dumps([convert.to_dict(self.settings) for convert in converts], **json_kwargs)

def grouped(self, results, offset=None):
groups = {}
Expand Down
33 changes: 20 additions & 13 deletions src/when/db/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from .. import utils
from ..exceptions import DBError

logger = logging.getLogger(__name__)
logger = utils.logger()

DB_FILENAME = Path(__file__).parent / "when.db"
DB_SCHEMA = """
Expand Down Expand Up @@ -41,7 +41,7 @@
SELECT c.id, c.name, c.ascii, c.sub, c.co, c.tz
FROM city c
WHERE
(c.id = :value OR c.name = :value OR c.ascii = :value)
(c.id = :value OR UPPER(c.name) = :value OR UPPER(c.ascii) = :value)
"""

ALIASES_LISTING_QUERY = """
Expand Down Expand Up @@ -165,6 +165,10 @@ def add_alias(self, name, gid):
[(val.strip(), gid) for val in name.split(",")],
)

@property
def size(self):
return self.filename.stat().st_size if self.filename.exists() else 0

@utils.timer
def create_db(self, data, remove_existing=True):
if self.filename.exists():
Expand All @@ -179,7 +183,7 @@ def create_db(self, data, remove_existing=True):
cur.executemany("INSERT INTO city VALUES (?, ?, ?, ?, ?, ?, ?)", data)
nrows = cur.rowcount

print(f"Inserted {nrows} rows")
logger.info(f"Inserted {nrows:,} rows ({self.size:,} bytes)")

def _execute(self, con, sql, params):
return con.execute(sql, params).fetchall()
Expand All @@ -192,29 +196,32 @@ def _search(self, sql, value, params):
return City.from_results(results)

def parse_search(self, value):
bits = [a.strip() for a in value.split(",")]
bits = [a.strip().upper() for a in value.split(",")]
nbits = len(bits)
if nbits > 3:
raise DBError(f"Invalid city search expression: {value}")

match nbits:
case 1:
return [value, None, None]
return [bits[0], None, None]
case 2:
return [bits[0], bits[1], None]
return [bits[0], None, bits[1]]
case 3:
return bits

def search(self, value, exact=False):
value, co, sub = self.parse_search(value)
value, sub, co = self.parse_search(value)
if exact:
data = {"value": value}
sql = XSEARCH_QUERY
if co:
sql = f"{sql} AND c.co = :co AND c.sub = :sub" if sub else f"{sql} AND c.co = :co"
data["co"] = co
sql = f"{sql} AND UPPER(c.co) = :co"
if sub:
data["sub"] = sub
sql = f"{sql} AND UPPER(c.sub) = :sub"

return self._search(
sql, value, {"value": value, "co": co.upper() if co else co, "sub": sub}
)
return self._search(sql, value, data)

like_exprs = ["c.name LIKE :like", "c.ascii LIKE :like"]
if co:
Expand All @@ -230,7 +237,7 @@ def search(self, value, exact=False):
{
"like": f"%{value}%",
"value": value,
"co": co.upper() if co else co,
"sub": sub.upper() if sub else sub,
"co": co,
"sub": sub,
},
)
80 changes: 44 additions & 36 deletions src/when/db/make.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,35 @@
import io
import time
import logging
import zipfile
from collections import defaultdict
from collections import defaultdict, namedtuple
from pathlib import Path

from .. import utils

logger = logging.getLogger(__name__)
logger = utils.logger()
GeoCity = namedtuple(
"GeoCity",
"gid,"
"name,"
"aname,"
"alt,"
"lat,"
"lng,"
"fclass,"
"fcode,"
"co,"
"cc2,"
"a1,"
"a2,"
"a3,"
"a4,"
"pop,"
"el,"
"dem,"
"tz,"
"mod"
)

DB_DIR = Path(__file__).parent
GEONAMES_CITIES_URL_FMT = "https://download.geonames.org/export/dump/cities{}.zip"
Expand All @@ -27,7 +50,12 @@ def fetch_cities(size, dirname=DB_DIR):
if txt_filename.exists():
return txt_filename

zip_bytes = utils.fetch(GEONAMES_CITIES_URL_FMT.format(size))
url = GEONAMES_CITIES_URL_FMT.format(size)
logger.info(f"Beginning download from {url}")
start = time.time()
zip_bytes = utils.fetch(url)
end = time.time()
logger.info(f"Received {len(zip_bytes):,} bytes in {end-start:,}s")
zip_filename = io.BytesIO(zip_bytes)
with zipfile.ZipFile(zip_filename) as z:
z.extract(txt_filename.name, txt_filename.parent)
Expand Down Expand Up @@ -77,49 +105,28 @@ def process_geonames_txt(fobj, minimum_population=15_000, admin_1=None):
skip_if = {"PPL", "PPLL", "PPLS", "PPLF", "PPLR"}
admin_1 = admin_1 or {}
data = []
i = 0
for line in fobj:
for i, line in enumerate(fobj, 1):
i += 1
(
gid,
name,
aname,
alt,
lat,
lng,
fclass,
fcode,
co,
cc2,
a1,
a2,
a3,
a4,
pop,
el,
dem,
tz,
mod,
) = line.rstrip().split("\t")

pop = int(pop) if pop else 0
ct = GeoCity(*line.rstrip().split("\t"))

pop = int(ct.pop) if ct.pop else 0
if (
(fcode in skip)
or (fcode in skip_if and (pop < minimum_population))
or (fcode == "PPLA5" and name.startswith("Marseille") and name[-1].isdigit())
(ct.fcode in skip)
or (ct.fcode in skip_if and (pop < minimum_population))
or (ct.fcode == "PPLA5" and ct.name.startswith("Marseille") and ct.name[-1].isdigit())
):
skipped[fcode] += 1
skipped[ct.fcode] += 1
continue

fcodes[fcode] += 1
sub = admin_1.get(f"{co}.{a1}", a1)
data.append([int(gid), name, aname, co, sub, tz, int(pop)])
fcodes[ct.fcode] += 1
sub = admin_1.get(f"{ct.co}.{ct.a1}", ct.a1)
data.append([int(ct.gid), ct.name, ct.aname, ct.co, sub, ct.tz, pop])

for title, dct in [["KEPT", fcodes], ["SKIP", skipped]]:
for k, v in sorted(dct.items(), key=lambda kv: kv[1], reverse=True):
logger.debug(f"{title} {k:5}: {v}")

logger.debug(f"Processed {i} lines, kept {len(data)}")
logger.info(f"Processed {i:,} lines, kept {len(data):,}")
return data


Expand All @@ -139,5 +146,6 @@ def fetch_admin_1(dirname=DB_DIR):
else:
txt = utils.fetch(GEONAMES_ADMIN1_URL).decode()
filename.write_text(txt)
logger.info(f"Downloaded {len(txt):,} bytes from {GEONAMES_ADMIN1_URL}")

return load_admin1(txt)
2 changes: 1 addition & 1 deletion src/when/lunar.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def lunar_phase(settings, dt=None, dt_fmt=None):

emoji = settings["emojis"][index]
name = settings["phases"][index]
return f"{emoji} {name}"
return emoji, name, age


def full_moon_iterator(dt=None):
Expand Down
Loading

0 comments on commit 20c0701

Please sign in to comment.