-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathREADME.md
407 lines (359 loc) · 21.8 KB
/
README.md
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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
<!--
You can manually process this file with cog:
$ python -m pip install -r requirements.pip
$ python -m cogapp -rP README.md
On GitHub, it's generated by an action:
https://github.com/nedbat/nedbat/blob/main/.github/workflows/build.yml
-->
<!-- [[[cog
import base64
import datetime
import os
import sys
import time
from urllib.parse import quote, urlencode
import requests
def requests_get_json(url):
"""Get JSON data from a URL, with retries."""
headers = {}
token = None
if "github.com" in url:
token = os.environ.get("GITHUB_TOKEN", "")
if token:
headers["Authorization"] = f"Bearer {token}"
for _ in range(3):
sys.stderr.write(f"Fetching {url}\n")
resp = requests.get(url, headers=headers)
if resp.status_code == 200:
break
print(f"{resp.status_code} from {url}:", file=sys.stderr)
print(resp.text, file=sys.stderr)
time.sleep(1)
else:
raise Exception(f"Couldn't get data from {url}")
return resp.json()
def rounded_nice(n):
"""Make a good human-readable summary of a number: 1734 -> "1.7k"."""
n = int(n)
ndigits = len(str(n))
if ndigits <= 3:
return str(n)
elif 3 < ndigits <= 4:
return f"{round(n/1000, 1):.1f}k"
elif 4 < ndigits <= 6:
return f"{round(n/1000):d}k"
elif 6 < ndigits <= 7:
return f"{round(n/1_000_000, 1):.1f}M"
elif 7 < ndigits <= 9:
return f"{round(n/1_000_000):d}M"
def shields_url(
url=None,
label=None,
message=None,
color=None,
label_color=None,
logo=None,
logo_color=None,
):
"""Flexible building of a shields.io URL with optional components."""
params = {"style": "flat"}
if url is None:
url = "".join([
"/badge/",
quote(label or ""),
"-",
quote(message),
"-",
color,
])
else:
if label:
params["label"] = label
url = "https://img.shields.io" + url
if label_color:
params["labelColor"] = label_color
if logo:
params["logo"] = logo
if logo_color:
params["logoColor"] = logo_color
return url + "?" + urlencode(params)
def md_image(image_url, text, link, title=None, attrs=None):
"""Build the Markdown for an image.
image_url: the URL for the image.
text: used for the alt text and the title if title is missing.
link: the URL destination when clicking on the image.
title: the title text to use.
attrs: HTML attributes (switches to HTML syntax)
"""
if title is None:
title = text
assert "]" not in text
assert '"' not in title
if attrs:
img_attrs = " ".join(f'{k}="{v}"' for k, v in attrs.items())
return f'[<img src="{image_url}" title="{title}" {img_attrs}/>]({link})'
else:
return f'[]({link})'
def badge(text=None, link=None, title=None, **kwargs):
"""Build the Markdown for a shields.io badge."""
return md_image(image_url=shields_url(**kwargs), text=text, link=link, title=title)
def badge_mastodon(server, handle):
"""A badge for a Mastodon account."""
# https://github.com/badges/shields/issues/4492
# https://docs.joinmastodon.org/methods/accounts/#lookup
url = f"https://{server}/api/v1/accounts/lookup?acct={handle}"
followers = requests_get_json(url)["followers_count"]
return badge(
label=f"@{handle}", message=rounded_nice(followers),
logo="mastodon", color="96a3b0", label_color="450657", logo_color="white",
text=f"Follow @{handle} on Mastodon", link=f"https://{server}/@{handle}",
)
def badge_bluesky(handle):
"""A badge for a Bluesky account."""
url = f"https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor={handle}"
followers = requests_get_json(url)["followersCount"]
return badge(
label=f"Bluesky", message=rounded_nice(followers),
logo="icloud", label_color="3686f7", color="96a3b0", logo_color="white",
text=f"Follow {handle} on Bluesky", link=f"https://bsky.app/profile/{handle}",
)
def badge_stackoverflow(userid):
"""A badge for a Stackoverflow account."""
data = requests_get_json(f"https://api.stackexchange.com/2.3/users/{userid}?order=desc&sort=reputation&site=stackoverflow")["items"][0]
rep_points = rounded_nice(data["reputation"])
gold = rounded_nice(data["badge_counts"]["gold"])
silver = rounded_nice(data["badge_counts"]["silver"])
bronze = rounded_nice(data["badge_counts"]["bronze"])
sp = "\N{THIN SPACE}"
return badge(
logo="stackoverflow", logo_color=None, label_color="333333", color="e6873e",
message=(
f"{rep_points} "
+ f"\N{LARGE YELLOW CIRCLE}{sp}{gold} "
+ f"\N{MEDIUM WHITE CIRCLE}{sp}{silver} "
+ f"\N{LARGE BROWN CIRCLE}{sp}{bronze}"
),
text="Stack Overflow reputation", link=data["link"],
)
def data_url(image_file):
"""Read an image file and return a self-contained data URL."""
assert image_file.endswith((".png", ".jpg"))
with open(image_file, "rb") as imgf:
b64 = base64.b64encode(imgf.read()).decode("ascii")
return f"data:image/png;base64,{b64}"
]]] -->
<!-- [[[end]]] -->
<!--
##
## BADGES
##
-->
<!-- [[[cog
print(badge(
logo=data_url("pencil.png"), logo_color="white", label_color="eeeeee", message="Blog etc", color="888888",
text="Read my blog", link="https://nedbatchelder.com",
))
print(badge_bluesky("nedbat.com"))
print(badge_mastodon("hachyderm.io", "nedbat"))
print(badge(
logo="meetup", logo_color="red", label_color="eeeeee", message="Boston Python", color="4d7954",
text="Join us at Boston Python", link="https://about.bostonpython.com",
))
print(badge(
logo="discord", logo_color="white", label_color="7289da", message="Discord", color="ffe97c",
text="Python Discord", link="https://discord.gg/python",
))
print(badge(
logo="GitHub", label="\N{HEAVY BLACK HEART}", message="Sponsor me", color="brightgreen",
text="Sponsor me on GitHub", link="https://github.com/sponsors/nedbat",
))
print(badge_stackoverflow(userid=14343))
print(badge(
logo="python", logo_color="FFE873", label_color="306998", message="PyPI", color="4B8BBE",
text="My PyPI packages", link="https://pypi.org/user/nedbatchelder",
))
]]] -->
[](https://nedbatchelder.com)
[](https://bsky.app/profile/nedbat.com)
[](https://hachyderm.io/@nedbat)
[](https://about.bostonpython.com)
[](https://discord.gg/python)
[](https://github.com/sponsors/nedbat)
[](https://stackoverflow.com/users/14343/ned-batchelder)
[](https://pypi.org/user/nedbatchelder)
<!-- [[[end]]] -->
<!--
##
## CAUSES
##
-->
<!-- [[[cog
attrs = {"height": 75, "style": "border: 1px solid #888"}
print(md_image("https://nedbatchelder.com/pix/blm.jpg", "Black lives matter", "https://nedbatchelder.com/blog/202006/black_lives_matter.html", attrs=attrs))
print(" " * 4)
print(md_image("https://nedbatchelder.com/pix/ukraine.png", "Support Ukraine", "https://stand-with-ukraine.pp.ua/#support-ukraine", attrs=attrs))
print(" " * 4)
print(md_image("https://nedbatchelder.com/pix/progressprideflag.png", "Pride", "https://nedbatchelder.com/blog/201207/my_mom_got_married.html", attrs=attrs))
print(" " * 4)
print(md_image("https://nedbatchelder.com/pix/us-flag.png", "Optimistic despite current events", "https://nedbatchelder.com/blog/202411/my_politics.html", attrs=attrs))
]]] -->
[<img src="https://nedbatchelder.com/pix/blm.jpg" title="Black lives matter" height="75" style="border: 1px solid #888"/>](https://nedbatchelder.com/blog/202006/black_lives_matter.html)
    
[<img src="https://nedbatchelder.com/pix/ukraine.png" title="Support Ukraine" height="75" style="border: 1px solid #888"/>](https://stand-with-ukraine.pp.ua/#support-ukraine)
    
[<img src="https://nedbatchelder.com/pix/progressprideflag.png" title="Pride" height="75" style="border: 1px solid #888"/>](https://nedbatchelder.com/blog/201207/my_mom_got_married.html)
    
[<img src="https://nedbatchelder.com/pix/us-flag.png" title="Optimistic despite current events" height="75" style="border: 1px solid #888"/>](https://nedbatchelder.com/blog/202411/my_politics.html)
<!-- [[[end]]] -->
<!--
##
## ME
##
-->
I'm **Ned Batchelder**, a Python software developer and community organizer.
- My personal site is [nedbatchelder.com][nedbat].
- I'm an organizer of [Boston Python][bp].
- I'm a member of the [Python Docs Editorial Board][pdeb].
- I work for an AI company, but [have concerns about AI][antblog].
You can **find me** at:
- Bluesky: [nedbat.com](https://bsky.app/profile/nedbat.com).
- Mastodon: [@nedbat@nedbat.com][mastodon].
- Libera IRC: nedbat in [#python][libera].
- Discord: nedbat in the [Python Discord][discord].
<!--
##
## BLOG POSTS
##
-->
<!-- [[[cog
blogdata = requests_get_json("https://nedbatchelder.com/summary.json")
def write_blog_post(entry, twoline=False):
when = datetime.datetime.strptime(entry['when_iso'], "%Y%m%d")
print(f"- **[{entry['title']}]({entry['url']})**, {when:%-d %b}", end="")
if twoline:
print(f"<br/>\n{entry['description_text']} *([read..]({entry['url']}))*")
else:
print()
]]] -->
<!-- [[[end]]] -->
My latest **[blog][blog]** posts:
<!-- [[[cog
N_ENTRIES = 4
entries = blogdata["entries"][:N_ENTRIES]
for entry in entries:
write_blog_post(entry, twoline=True)
print("- and [many more][blog]..")
]]] -->
- **[Intricate interleaved iteration](https://nedbatchelder.com/blog/202501/intricate_interleaved_iteration.html)**, 30 Jan<br/>
Someone asked recently, “is there any reason to use a generator if I need to store all the values anyway?” As it happens, I did just that in the code for this blog’s sidebar because I found it the most readable way to do it. Maybe it was a good idea, maybe not. *([read..](https://nedbatchelder.com/blog/202501/intricate_interleaved_iteration.html))*
- **[Nat running](https://nedbatchelder.com/blog/202501/nat_running.html)**, 14 Jan<br/>
I took this picture nine years ago, but it’s still one of my favorites *([read..](https://nedbatchelder.com/blog/202501/nat_running.html))*
- **[Testing some tidbits](https://nedbatchelder.com/blog/202412/testing_some_tidbits.html)**, 4 Dec<br/>
A custom test harness for some esoteric Python expressions *([read..](https://nedbatchelder.com/blog/202412/testing_some_tidbits.html))*
- **[Dinner](https://nedbatchelder.com/blog/202412/dinner.html)**, 1 Dec<br/>
My son Nat has autism, and one way it affects him is he can be very quiet and passive, even when he wants something very much. This played out on our drive home from Thanksgiving this week. *([read..](https://nedbatchelder.com/blog/202412/dinner.html))*
- and [many more][blog]..
<!-- [[[end]]] -->
<!--
##
## PYPI PACKAGES
##
-->
<!-- [[[cog
pkgs = [
# (pypi name, human name, github repo, (mastserver, masthandle)),
("coverage", "Coverage.py", "nedbat/coveragepy", ("hachyderm.io", "coveragepy")),
("cogapp", "Cog", "nedbat/cog"),
("scriv", "Scriv", "nedbat/scriv"),
("dinghy", "Dinghy", "nedbat/dinghy"),
("watchgha", "WatchGHA", "nedbat/watchgha"),
("aptus", "Aptus", "nedbat/aptus"),
]
def write_package(pkg, human, repo, mastinfo=None):
description = requests_get_json(f"https://api.github.com/repos/{repo}")["description"]
main_line = f"[**{human}**](https://github.com/{repo}): {description}"
pypi_badge = badge(
url=f"/pypi/v/{pkg}?style=flat",
text="PyPI",
link=f"https://pypi.org/project/{pkg}",
title=f"The {pkg} PyPI page",
)
github_badge = badge(
url=f"/github/last-commit/{repo}?logo=github&style=flat",
text="GitHub last commit",
link=f"https://github.com/{repo}/commits",
title=f"Recent {human.lower()} commits",
)
pypi_downloads_badge = badge(
url=f"/pypi/dm/{pkg}?style=flat",
text="PyPI - Downloads",
link=f"https://pypistats.org/packages/{pkg}",
title=f"Download stats for {pkg}",
)
print(f"- {main_line}<br/>")
print(f" {pypi_badge} {github_badge} {pypi_downloads_badge}")
if mastinfo is not None:
print(f" {badge_mastodon(*mastinfo)}")
]]] -->
<!-- [[[end]]] -->
I maintain a few [**Python packages**][ned_pypi], including:
<!-- [[[cog
for args in pkgs:
write_package(*args)
]]] -->
- [**Coverage.py**](https://github.com/nedbat/coveragepy): The code coverage tool for Python<br/>
[](https://pypi.org/project/coverage) [](https://github.com/nedbat/coveragepy/commits) [](https://pypistats.org/packages/coverage)
[](https://hachyderm.io/@coveragepy)
- [**Cog**](https://github.com/nedbat/cog): Small bits of Python computation for static files<br/>
[](https://pypi.org/project/cogapp) [](https://github.com/nedbat/cog/commits) [](https://pypistats.org/packages/cogapp)
- [**Scriv**](https://github.com/nedbat/scriv): Changelog management tool<br/>
[](https://pypi.org/project/scriv) [](https://github.com/nedbat/scriv/commits) [](https://pypistats.org/packages/scriv)
- [**Dinghy**](https://github.com/nedbat/dinghy): A GitHub activity digest tool<br/>
[](https://pypi.org/project/dinghy) [](https://github.com/nedbat/dinghy/commits) [](https://pypistats.org/packages/dinghy)
- [**WatchGHA**](https://github.com/nedbat/watchgha): Live display of current GitHub action runs<br/>
[](https://pypi.org/project/watchgha) [](https://github.com/nedbat/watchgha/commits) [](https://pypistats.org/packages/watchgha)
- [**Aptus**](https://github.com/nedbat/aptus): Mandelbrot fractal viewer<br/>
[](https://pypi.org/project/aptus) [](https://github.com/nedbat/aptus/commits) [](https://pypistats.org/packages/aptus)
<!-- [[[end]]] -->
<!--
##
## OTHER PROJECTS
##
-->
I've also made a few informal projects, some mathy art, and some small utilities:
- [pkgsample](https://github.com/nedbat/pkgsample), an simple example of how to package a Python project.
- [Truchet images](https://github.com/nedbat/truchet) explores Truchet tiles, and rendering images with them.
[Blog post](https://nedbatchelder.com/blog/202208/truchet_images.html).
- [Flourish](https://github.com/nedbat/flourish) is a harmonograph explorer.
[Blog post](https://nedbatchelder.com/blog/202101/flourish.html) and [live site](https://flourish.nedbat.com/).
- [Stilted](https://github.com/nedbat/stilted) is a toy PostScript implementation.
[Blog post](https://nedbatchelder.com/blog/202208/stilted.html).
- [Gefilte Fish](https://github.com/nedbat/gefilte) is a Python-based DSL for writing Gmail filters.
[Blog post](https://nedbatchelder.com/blog/202103/gefilte_fish_gmail_filter_creation.html).
- [Pydoctor](https://github.com/nedbat/pydoctor) shows details of your Python environment, for troubleshooting.
<!--
##
## FOOTER
##
-->
<br/>
<br/>
This is a [Markdown page with embedded Python code][readme.md] rendered with [cog][cog].
See my blog post **[Cogged GitHub profile][blog_post]** for details.
<!-- [[[cog
print(f"*Updated at {datetime.datetime.now():%Y-%m-%d %H:%M} UTC*")
]]] -->
*Updated at 2025-02-12 02:45 UTC*
<!-- [[[end]]] -->
[nedbat]: https://nedbatchelder.com "My site with blog, talks, etc"
[blog]: https://nedbatchelder.com/blog "My blog"
[mastodon]: https://hachyderm.io/@nedbat
[discord]: https://pythondiscord.com
[libera]: https://libera.chat
[bp]: https://bostonpython.com "The Boston Python home page"
[antblog]: https://nedbatchelder.com/blog/202407/anthropic.html "My blog post about working at Anthropic"
[pdeb]: https://python.github.io/editorial-board/
[ned_pypi]: https://pypi.org/user/nedbatchelder "The list of all my packages on PyPI"
[cog]: https://github.com/nedbat/cog "The cog repo on GitHub"
[readme.md]: https://github.com/nedbat/nedbat/blob/main/README.md?plain=1 "The raw source for this GitHub profile"
[blog_post]: https://nedbatchelder.com/blog/202409/cogged_github_profile.html "Discussion of how this page is constructed"