Skip to content

Commit c86359d

Browse files
committed
Improved docs
1 parent 007fd20 commit c86359d

File tree

9 files changed

+417
-110
lines changed

9 files changed

+417
-110
lines changed

Justfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ update:
2626
{{PIP}} install -U \
2727
build \
2828
pytest \
29+
docutils \
2930
pytest-sugar \
3031
pytest-clarity \
3132
freezegun \
@@ -132,3 +133,8 @@ strftime:
132133
f"Format Specifiers:\n{pt.get_string()}\n\n"
133134
"Notes:\n* - Locale-dependent\n+ - C99 extension\n! - when extension"
134135
)
136+
137+
# Preview README.rst
138+
docs:
139+
rst2html5 --output README.html README.rst
140+
open README.html

README.rst

Lines changed: 329 additions & 51 deletions
Large diffs are not rendered by default.

src/when/cli.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
#!/usr/bin/env python
22
import argparse
3-
import logging
43
import sys
4+
import logging
55
from pathlib import Path
66

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

11-
logger = logging.getLogger(__name__)
12-
1311

1412
def get_parser(settings):
1513
class DBSizeAction(argparse.Action):
@@ -227,11 +225,14 @@ def log_config(verbosity, settings):
227225
logging.getLogger("asyncio").setLevel(logging.WARNING)
228226

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

234+
return logger
235+
235236

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

261-
log_config(args.verbosity, settings)
262+
logger = log_config(args.verbosity, settings)
262263
logger.debug(args)
263264
if args.help:
264265
parser.print_help()

src/when/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
+------+--------------------------------------------------------------------------+--------------------------+------+
4040
| %M | Minute (00-59) | 55 | |
4141
+------+--------------------------------------------------------------------------+--------------------------+------+
42-
| %n | New-line character ('\n') | | + |
42+
| %n | New-line character | '\\n' | + |
4343
+------+--------------------------------------------------------------------------+--------------------------+------+
4444
| %p | AM or PM designation | PM | |
4545
+------+--------------------------------------------------------------------------+--------------------------+------+
@@ -49,7 +49,7 @@
4949
+------+--------------------------------------------------------------------------+--------------------------+------+
5050
| %S | Second (00-61) | 02 | |
5151
+------+--------------------------------------------------------------------------+--------------------------+------+
52-
| %t | Horizontal-tab character ('\t') | | + |
52+
| %t | Horizontal-tab character | '\\t' | + |
5353
+------+--------------------------------------------------------------------------+--------------------------+------+
5454
| %T | ISO 8601 time format (HH:MM:SS), equivalent to %H:%M:%S | 14:55:02 | + |
5555
+------+--------------------------------------------------------------------------+--------------------------+------+

src/when/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from .db import client
1414
from .lunar import lunar_phase
1515

16-
logger = logging.getLogger(__name__)
16+
logger = utils.logger()
1717

1818

1919
def holidays(settings, co="US", ts=None):

src/when/db/client.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from .. import utils
99
from ..exceptions import DBError
1010

11-
logger = logging.getLogger(__name__)
11+
logger = utils.logger()
1212

1313
DB_FILENAME = Path(__file__).parent / "when.db"
1414
DB_SCHEMA = """
@@ -41,7 +41,7 @@
4141
SELECT c.id, c.name, c.ascii, c.sub, c.co, c.tz
4242
FROM city c
4343
WHERE
44-
(c.id = :value OR c.name = :value OR c.ascii = :value)
44+
(c.id = :value OR UPPER(c.name) = :value OR UPPER(c.ascii) = :value)
4545
"""
4646

4747
ALIASES_LISTING_QUERY = """
@@ -165,6 +165,10 @@ def add_alias(self, name, gid):
165165
[(val.strip(), gid) for val in name.split(",")],
166166
)
167167

168+
@property
169+
def size(self):
170+
return self.filename.stat().st_size if self.filename.exists() else 0
171+
168172
@utils.timer
169173
def create_db(self, data, remove_existing=True):
170174
if self.filename.exists():
@@ -179,7 +183,7 @@ def create_db(self, data, remove_existing=True):
179183
cur.executemany("INSERT INTO city VALUES (?, ?, ?, ?, ?, ?, ?)", data)
180184
nrows = cur.rowcount
181185

182-
print(f"Inserted {nrows} rows")
186+
logger.info(f"Inserted {nrows:,} rows ({self.size:,} bytes)")
183187

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

194198
def parse_search(self, value):
195-
bits = [a.strip() for a in value.split(",")]
199+
bits = [a.strip().upper() for a in value.split(",")]
196200
nbits = len(bits)
197201
if nbits > 3:
198202
raise DBError(f"Invalid city search expression: {value}")
199203

200204
match nbits:
201205
case 1:
202-
return [value, None, None]
206+
return [bits[0], None, None]
203207
case 2:
204-
return [bits[0], bits[1], None]
208+
return [bits[0], None, bits[1]]
205209
case 3:
206210
return bits
207211

208212
def search(self, value, exact=False):
209-
value, co, sub = self.parse_search(value)
213+
value, sub, co = self.parse_search(value)
210214
if exact:
215+
data = {"value": value}
211216
sql = XSEARCH_QUERY
212217
if co:
213-
sql = f"{sql} AND c.co = :co AND c.sub = :sub" if sub else f"{sql} AND c.co = :co"
218+
data["co"] = co
219+
sql = f"{sql} AND UPPER(c.co) = :co"
220+
if sub:
221+
data["sub"] = sub
222+
sql = f"{sql} AND UPPER(c.sub) = :sub"
214223

215-
return self._search(
216-
sql, value, {"value": value, "co": co.upper() if co else co, "sub": sub}
217-
)
224+
return self._search(sql, value, data)
218225

219226
like_exprs = ["c.name LIKE :like", "c.ascii LIKE :like"]
220227
if co:
@@ -230,7 +237,7 @@ def search(self, value, exact=False):
230237
{
231238
"like": f"%{value}%",
232239
"value": value,
233-
"co": co.upper() if co else co,
234-
"sub": sub.upper() if sub else sub,
240+
"co": co,
241+
"sub": sub,
235242
},
236243
)

src/when/db/make.py

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,35 @@
11
import io
2+
import time
23
import logging
34
import zipfile
4-
from collections import defaultdict
5+
from collections import defaultdict, namedtuple
56
from pathlib import Path
67

78
from .. import utils
89

9-
logger = logging.getLogger(__name__)
10+
logger = utils.logger()
11+
GeoCity = namedtuple(
12+
"GeoCity",
13+
"gid,"
14+
"name,"
15+
"aname,"
16+
"alt,"
17+
"lat,"
18+
"lng,"
19+
"fclass,"
20+
"fcode,"
21+
"co,"
22+
"cc2,"
23+
"a1,"
24+
"a2,"
25+
"a3,"
26+
"a4,"
27+
"pop,"
28+
"el,"
29+
"dem,"
30+
"tz,"
31+
"mod"
32+
)
1033

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

30-
zip_bytes = utils.fetch(GEONAMES_CITIES_URL_FMT.format(size))
53+
url = GEONAMES_CITIES_URL_FMT.format(size)
54+
logger.info(f"Beginning download from {url}")
55+
start = time.time()
56+
zip_bytes = utils.fetch(url)
57+
end = time.time()
58+
logger.info(f"Received {len(zip_bytes):,} bytes in {end-start:,}s")
3159
zip_filename = io.BytesIO(zip_bytes)
3260
with zipfile.ZipFile(zip_filename) as z:
3361
z.extract(txt_filename.name, txt_filename.parent)
@@ -77,49 +105,28 @@ def process_geonames_txt(fobj, minimum_population=15_000, admin_1=None):
77105
skip_if = {"PPL", "PPLL", "PPLS", "PPLF", "PPLR"}
78106
admin_1 = admin_1 or {}
79107
data = []
80-
i = 0
81-
for line in fobj:
108+
for i, line in enumerate(fobj, 1):
82109
i += 1
83-
(
84-
gid,
85-
name,
86-
aname,
87-
alt,
88-
lat,
89-
lng,
90-
fclass,
91-
fcode,
92-
co,
93-
cc2,
94-
a1,
95-
a2,
96-
a3,
97-
a4,
98-
pop,
99-
el,
100-
dem,
101-
tz,
102-
mod,
103-
) = line.rstrip().split("\t")
104-
105-
pop = int(pop) if pop else 0
110+
ct = GeoCity(*line.rstrip().split("\t"))
111+
112+
pop = int(ct.pop) if ct.pop else 0
106113
if (
107-
(fcode in skip)
108-
or (fcode in skip_if and (pop < minimum_population))
109-
or (fcode == "PPLA5" and name.startswith("Marseille") and name[-1].isdigit())
114+
(ct.fcode in skip)
115+
or (ct.fcode in skip_if and (pop < minimum_population))
116+
or (ct.fcode == "PPLA5" and ct.name.startswith("Marseille") and ct.name[-1].isdigit())
110117
):
111-
skipped[fcode] += 1
118+
skipped[ct.fcode] += 1
112119
continue
113120

114-
fcodes[fcode] += 1
115-
sub = admin_1.get(f"{co}.{a1}", a1)
116-
data.append([int(gid), name, aname, co, sub, tz, int(pop)])
121+
fcodes[ct.fcode] += 1
122+
sub = admin_1.get(f"{ct.co}.{ct.a1}", ct.a1)
123+
data.append([int(ct.gid), ct.name, ct.aname, ct.co, sub, ct.tz, pop])
117124

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

122-
logger.debug(f"Processed {i} lines, kept {len(data)}")
129+
logger.info(f"Processed {i:,} lines, kept {len(data):,}")
123130
return data
124131

125132

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

143151
return load_admin1(txt)

src/when/utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import re
44
import sys
55
import time
6+
import logging
7+
from functools import cache
68
from datetime import datetime, timedelta
79
from pathlib import Path
810

@@ -15,6 +17,11 @@
1517
from .exceptions import WhenError
1618

1719

20+
@cache
21+
def logger():
22+
return logging.getLogger("when")
23+
24+
1825
def gettz(name=None):
1926
tz = _gettz(name)
2027
if name is None:

tests/test_when.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,9 @@ def test_fetch_admin_1(self, loader):
217217
expect.unlink(True)
218218

219219
def test_parse_search(self, db):
220-
assert db.parse_search("a") == ["a", None, None]
221-
assert db.parse_search("a, b") == ["a", "b", None]
222-
assert db.parse_search("a, b,c") == ["a", "b", "c"]
220+
assert db.parse_search("a") == ["A", None, None]
221+
assert db.parse_search("a, b") == ["A", None, "B"]
222+
assert db.parse_search("a, b,c") == ["A", "B", "C"]
223223
with pytest.raises(exceptions.DBError, match="Invalid city search expression: a,b,c,d"):
224224
db.parse_search("a,b,c,d")
225225

0 commit comments

Comments
 (0)