Full Width [alt+shift+f] Shortcuts [alt+shift+k]
Sign Up [alt+shift+s] Log In [alt+shift+l]
14
After many wonderful years of working out of my home office (see Workspaces), I've now "expanded" [1] into an office of my own. 406 W Franklin St #201 is now the Richmond-area headquarters of Buttondown. Send me gifts! The move is a bittersweet one; it was a great joy to be so close to Haley and Lucy (and, of course, Telly), and the flexibility of being able to hop off a call and then take the dog for a walk or hold Lucy for a while was very, very nice. At the same time, for the first time in my life that flexibility has become a little bit of a burden! It turns out it is very hard to concentrate on responding to emails when your alternative is to play with your daughter giggling in the adjoining room; similarly, as Buttondown grows and as more and more of my time is spent on calls, it turns out long-winded demos and onboarding calls are logistically trickier when it is Nap Time a scant six feet away. And, beyond that, it's felt harder and harder to turn my brain off for the day: when...
a month ago

Improve your reading experience

Logged in users get linked directly to articles resulting in a better reading experience. Please login for free, it takes less than 1 minute.

More from Applied Cartography

Performance improvements can be obvious and silly in retrospect

One of the most useful and janky internal tools we have in Buttondown’s codebase is a codegen pipeline called “autogen”. There is nothing “auto” about autogen: it is a series of scripts that munges a bunch of data into a bunch of different formats, to generate things like our API clients and code snippets and storybooks. Some of this data is stateful, and therefore requires a database, and therefore requires migrations — you see how this kind of thing can grow somewhat labrynthine. Each individual script is pretty simple, but as we’ve found more and more things to glom onto autogen. This, to be clear, is a good thing. It’s really nice to have automatic, consistent data and types everywhere, so that we literally cannot change the API without also pushing a concomitant change to the API docs. With each glom, though, the wall-clock time of running autogen increases — and so I found myself staring down the barrel at a 50second script running whenever we wanted to make any sort of non-trivial change to our schema. Fifty seconds was too many seconds. I set a budget of ten seconds — still a long time, but significantly less onerous — and began digging in at low-hanging fruit. There was a lot. A few that come to mind: We split up our vite config so we could only run the portion that we needed (cross-piling and minifying our CSS bundles; We disabled all Sentry and perf-tracing stuff that was getting enabled as part of the standard build; We no-oped all of the Python-land data generation if it was already there, since that stateful data didn’t change very often. This was all great, but we were still left with 15 seconds of wall clock time. Profiling each individual cog in the script revealed that the problem was essentially “it’s Python”: four items in the script ran Django commands, and just spinning up the Django process and running autodiscovery took around two seconds. Ouch! The impulse was to cut down that runtime. A great post by Adam led us to discover the biggest culprit was our Stripe imports, and we timeboxed a bit of time to try and get rid of them, either by deferring the imports or excising the library; neither seemed particularly feasible. Then, suddenly, the answer seemed obvious. If we have four scripts where the fixed cost of invoking Django is the long pole, why not simply combine the scripts? And that’s exactly what we did: if len(sys.argv) > 1 and "," in sys.argv[1]: commands = sys.argv[1].split(",") original_argv = sys.argv.copy() for command in commands: sys.argv[1] = command execute_from_command_line(sys.argv) sys.argv = original_argv else: execute_from_command_line(sys.argv)

4 days ago 2 votes
Smoke test your Django admin site

Here is a confession: I am a very strong proponent of a robust test suite being perhaps the single most important asset of a codebase, but when it comes to auxiliary services like admin sites or CLIs when it comes to testing I tend to ask for forgiveness more than I ask for permission. Django's admin site is no different: and, because Django's admin DSL is very magic-string-y, there's a lot of stuff that never gets caught by CI or mypy until a lovely CS agent informs me that something is blowing up in their face. Take this example, which bites me more often than I care to admit: from django.contrib import admin from stripe.models import StripeCustomer class StripeCustomer(models.Model): id = models.CharField(max_length=100, unique=True) username = models.CharField(max_length=100, unique=True) email_address = models.EmailField() creation_date = models.DateTimeField(auto_now=True) @admin.register(StripeCustomer) class StripeCustomerAdmin(admin.ModelAdmin): list_display = ( "id", "username", "email", "creation_date", ) search_fields = ( "username", "email", ) One thing that has made my life slightly easier in this respect is a parametric test that just makes sure we can render the empty state for every single admin view. Code snippet first, explanation after: from django.urls import get_resolver, reverse def extract_routes(resolver: URLResolver) -> iter[str]: keys = [key for key in resolver.reverse_dict.keys() if isinstance(key, str)] key_to_route = {key: resolver.reverse_dict.getlist(key)[0][0][0] for key in keys} for key in keys: yield key for key, (prefix, subresolver) in resolver.namespace_dict.items(): for route in extract_routes(subresolver): yield f"{key}:{route.name}" def is_django_admin_route(route_name: str): # Matches, e.g., `admin:emails_event_changelist`. return route_name.split(":").endswith("changelist") ADMIN_URL_ROUTE = "buttondown.urls.admin" DJANGO_ADMIN_CHANGELIST_ROUTES = [ route.name for route in extract_routes(get_resolver(ADMIN_URL_ROUTE)) if is_django_admin_route(route.name) ] # The fixture is overkill for this example, but I'm copying this from the actual codebase. @pytest.fixture def superuser_client(superuser: User, client: Client) -> Client: client.force_login(superuser) return client @pytest.mark.parametrize( "url", DJANGO_ADMIN_CHANGELIST_ROUTES ) def test_can_render_route(superuser_client: Any, url: str) -> None: url = reverse(url, args=[]) response = superuser_client.get(url) assert response.status_code == 200 Okay, a bit of a mouthful, but the final test itself is very clean and tidy and catches a lot of stuff. That extract_routes implementation looks scary and magical, and it is — I use a more robust implementation in django-typescript-routes, which itself we gratefully purloined from django-js-reverse. Lots of scary indexing, but its held up well for a while. The fixture and parametrize assumes usage of pytest (you should use pytest!) but it's trivially rewritable to use subTest instead.

2 weeks ago 11 votes
Recursive filter schema

When we added support for complex filtering in Buttondown, I spent a long time trying to come up with a schema for filters that felt sufficiently ergonomic and future-proof. I had a few constraints, all of which were reasonable: It needed to be JSON-serializable, and trivially parsable by both the front-end and back-end. It needed to be arbitrarily extendible across a number of domains (you could filter subscribers, but also you might want to filter emails or other models.) It needed to be able to handle both and and or logic (folks tagged foo and bar as well as folded tagged foo or bar). It needed to handle nested logic (folks tagged foo and folks tagged bar or baz.) The solution I landed upon is not, I’m sure, a novel one, but googling “recursive filter schema” was unsuccessful and I am really happy with the result so here it is in case you need something like this: @dataclass class FilterGroup: filters: list[Filter] groups: list[FilterGroup] predicate: "and" | "or" @dataclass class Filter: field: str operator: "less_than" | "greater_than" | "equals" | "not_equals" | "contains" | "not_contains" value: str And there you have it. Simple, easily serializable/type-safe, can handle everything you throw at it. For example, a filter for all folks younger than 18 or older than 60 and retired: FilterGroup( predicate="or", filters=[ Field( field="age", operator="less_than", value="18" ) ], groups=[ FilterGroup( predicate="and", filters=[ Field( field="age", operator="greater_than", value="60" ), Field( field="status", operator="equals", value="retired" ) ] groups=[], ) ] )

3 weeks ago 13 votes
Does that dependency spark joy?

If there's been one through line in changes to Buttondown's architecture over the past six months or so, it's been the removal and consolidation of dependencies: on the front-end, back-end, and in paid services. I built our own very spartan version of Metabase, Notion, and Storybook; we vended a half-dozen or so Django packages that were not worth the overhead of pulling from PyPI (and rewrote another half-dozen or so, which we will open-source in due time); we ripped out c3, our visualization library, and built our own; we ripped out vuedraggable and a headlessui and a slew more of otherwise-underwhelming frontend packages in favor of purpose-built (faster, smaller, less-flexible) versions. [1] There are a few reasons for this: Both Buttondown as an application and I as a developer have now been around long enough to be scarred by big ecosystem changes. Python has gone through both the 2.x to 3.x transition and, more recently, the untyped to typed transition; Vue has gone from 2.x to 3.x. The academic problem of "what happens if this language completely changes?" is no longer academic, and packages that we installed back in 2018 slowly succumbed to bitrot. It's more obvious to me now than a few years ago that pulling in dependencies incurs a non-trivial learning cost for folks paratrooping into the codebase. A wrapper library around fetch might be marginally easier to invoke once you get used to it, but it's a meaningful bump in the learning curve to adapt to it for the first time. It is easier than ever to build 60% of a tool, which is problematic in many respects but useful if you know exactly which 60% you care about. (Internal tools like Storybook or Metabase are great examples of this. It was a fun and trivial exercise to get Claude to build a tool that did everything I wanted Metabase to do, and save me $120/mo in the process.) We still use a lot of very heavy, very complex stuff that we're very happy with. Our editor sits on top of tiptap (and therefore ProseMirror); we use marked and turndown liberally, because they're fast and robust. On the Python side, our number of non-infrastructural packages is smaller but still meaningful (beautifulsoup, for instance, and django-allauth / django-anymail which are both worth their weight in gold). But the bar for pulling in a small dependency is much higher than it was, say, twelve months ago. My current white whale is to finally get rid of axios. 39 call sites to go! ↩︎

a month ago 13 votes

More in technology

How the government can raise taxes – and get away with it

It's time to treat Trump like the crisis he is

18 hours ago 2 votes
I've moved to Ghost Pro

Here's hoping this post is short and sweet… Back in 2019 I moved my blog off of WordPress and over to Ghost. In short, I wasn't happy with WordPress and wanted a blogging engine that felt more like it was made for blogging than a

11 hours ago 1 votes
Humanities Crash Course Week 13: The Qur’an

This week, I sampled one of the world’s great spiritual texts: the Qur’an. I also read poems by Rumi and saw a movie that led me to question my understanding of both. Readings As with many other people in the West, my understanding of Islam mostly comes from the news — often, in negative light. Also, I was raised Catholic and can’t help but parse these teachings through that tradition. Which is to say, I tried to read objectively, but aware that I’m likely biased. That said, I relished the opportunity to experience this book firsthand. The Qur’an has an interesting structure. Like many other books, it’s divided into chapters (surahs.) But instead of a chronological or topical organization, chapters are roughly sequenced from longest to shortest. (Except the first, which is a short prayer.) Gioia recommended fifteen surahs. As I often do after reading classic texts, I asked ChatGPT for a summary. Here it is verbatim: 1. Al-Fatiha (The Opening): A short prayer for guidance, mercy, and the straight path. Central to every unit of Muslim prayer. 2. Al-Baqara (The Cow): Longest surah; covers law, ethics, worship, community life, and stories of earlier prophets. Establishes Islam’s moral and legal foundations. 3. Al-Imran (The Family of Imran): Addresses Christians and Jews, emphasizes the unity of divine messages, and recounts the stories of Mary and Jesus with warnings against disbelief. 4. An-Nisa (The Women): Focuses on justice, women’s rights, inheritance laws, and proper conduct in war and peace; warns against hypocrisy and betrayal. 5. Al-Ma’ida (The Table Spread): Deals with dietary laws, contracts, and relations with Jews and Christians. Emphasizes loyalty to God’s covenant and the finality of Muhammad’s message. 12. Yusuf (Joseph): A narrative surah telling the story of Joseph in a literary and emotional style. A tale of patience, betrayal, forgiveness, and divine providence. 17. Al-Isra (The Night Journey): Takes its name from Muhammad’s mystical journey to Jerusalem and heaven. Emphasizes the Qur’an’s authority and the consequences of moral choices. 18. Al-Kahf (The Cave): Contains allegorical stories (Sleepers of the Cave, Moses and the Wise Man, Dhul-Qarnayn) about faith, knowledge, and the trials of life. 32. As-Sajda (The Prostration): Stresses the importance of divine revelation and resurrection. Calls for humility and recognition of God’s signs in creation. 36. Ya-Sin: Considered the “heart of the Qur’an”; focuses on God’s power, the resurrection, and human accountability, often recited for the dying. 55. Ar-Rahman (The Most Merciful): A poetic celebration of God’s mercy, creation, and judgment—famous for the refrain: “Which of your Lord’s favors will you deny?” 67. Al-Mulk (The Sovereignty): Reflects on God’s dominion over life and death. Encourages awe and contemplation of the natural world as a sign of divine power. 103. Al-Asr (Time): A short, stark reminder that time is running out—success lies in faith, good works, truth, and patience. 112. Al-Ikhlas (Sincerity): A concise declaration of pure monotheism: God is one, eternal, without equal or offspring. As may be evident here, the Qur’an blends spiritual teachings with social instruction, proposing norms, values, and institutions for a society centered on God. If Christianity’s core message is love, the core here is belief — specifically, belief in one supreme God. All else falls in line behind that idea. Reading Rumi deepened my understanding and appreciation. Through his poems, I perceived signals of what it might be like to live in a world saturated with God: ecstasy at the sight of a flower or the moon’s reflection in a still pond. Still, he reminds us these are only words: This is how it always is A great silence overcomes me, Gioia recommended a selection of Rumi’s poems, but I’ve enjoyed them enough that I’ve decided to read a whole collection. Rather than speedruning through it, I’ll read a bit at a time during my morning journaling sessions throughout the rest of the year. Audiovisual Music: Nusrat Fateh Ali Khan plus music from Tunisia. I was familiar with Ali Khan; his music was everywhere in the 1990s. He’s also one of the performers in the LAST TEMPTATION OF CHRIST soundtracks I highlighted earlier this year. Revisiting his music now gave me a new appreciation; like Rumi, he seems to express the ecstasy of living in a world soaked in divinity. Art: Gioia recommended looking at Islamic architecture. I’ve had the privilege of visiting two of the world’s great Islamic buildings, Hagia Sophia in Istanbul and the Alhambra in Granada. So that’s where my mind went. Hagia Sophia is one of the most impressive spaces I’ve ever been in; it must be experienced firsthand. That said, this NOVA program explains the building’s historical, cultural, and structural significance: This short presentation helped me understand the spiritual significance of sequencing and structure in Islamic religious buildings: The Alhambra is impressive for different reasons. For one thing, it’s not a religious structure. It’s arguably most famous for its beautiful mosaics. This video focuses on the palace’s hydraulic network, one of the world’s great engineering feats: This TEDEd presentation helped me understand the significance of the astonishing geometric decorations in these buildings: Both of these buildings also highlight Islam’s complex relationship with Christianity. Hagia Sophia started as a Christian church before being converted into a mosque, and the Alhambra stands as a reminder that part of Europe was under Islamic rule for a time. Cinema: rather than selecting a movie related to this week’s readings, I chose one I’d wanted to watch for a long time: Hal Ashby’s 1979 satire BEING THERE, starring the great Peter Sellers. By chance, a sheltered, single-minded, TV-obsessed, illiterate gardener named Chance is unwittingly thrust into the highest echelons of economic and political power. Due to his genteel demeanor, his naive utterances are continually misinterpreted as profundities. He remains aloof and uninvolved even as he becomes incredibly powerful. Rather, he becomes powerful because he is aloof and uninvolved. He has no awareness of (or concern for) how his words are interpreted, so he remains cool and neutral. This amplifies his image as a sage, feeding his growing myth. He and his interlocutors live in diverging worlds, reading into each others’ utterances meanings that aren’t there. Reflections Or are they? The movie suggests words don’t have inherent meaning; the meaning is in how we interpret them. Context informs understanding. Lacking an official backstory, people parse Chance through their preconceptions. In the first act, he introduces himself as “Chance the gardener.” But the idea that this seemingly refined gentleman is a mere gardener seems so unlikely that his interlocutor misinterprets his name as the more upper class Chancey Gardiner. And so it remains. When Chance(y) says “I like to watch,” he means something very different than what others hear, leading to particular and often unexpected actions. And it’s actions that ultimately matter. The Talmud puts it succinctly: “We do not see things as they are, we see them as we are.” I think about this whenever I engage with ancient scriptures. These texts were produced by and for very different cultures. The words mean different things to me than they likely meant to their original audiences. It’s not just that they were written in a language I don’t understand: they were written in a world I don’t understand. And yet, they endure. Why? We make the meaning. More precisely: meaning emerges as we parse the text through our particular understandings. And meaning adds value to our lives. What I took from my reading is that Islam positions itself as the culmination of monotheism. Characters, stories, and themes from the Jewish and Christian scriptures are present here too, but with a different focus. Somewhere along the line, Judaism and Christianity softened on the idea that there’s only one God. Islam aims to correct course. The idea of single-mindedly centering your life on something other than yourself is appealing. (Post-Freud, we’d say ego is the enemy.) The concept isn’t unique to Islam, of course. (And again, I’m likely reading into it.) What seems different is the Qur’an’s confident assertion of its message’s universal relevance. I can see how this all-encompassing vision can be seen as incompatible with secular post-modern societies. The Abrahamic traditions are also closely related, which can foster mutual understanding but also intensify disagreement. But I also sense the conflict emerges from literal readings, which we shouldn’t do. Again, these words emerge from — and speak to — different contexts. And I see the transcendent beauty in Rumi’s poems, the patterns of the Alhambra, and Nusrat Fateh Ali Khan’s singing, much as I do in Chartres Cathedral, the Mass in B Minor, and the Divine Comedy. All remind me that words can connect people to higher orders of being and to each other, elevating us all. Does God come in through the words or through what we bring to the words? Does it matter? It does. As Nietzsche warned, once we lose sense of the words’ specialness, we approach them differently — much like Chancey’s friends would hear him differently if they knew he was mere Chance. Some might say truth is what ultimately matters: that Chance is just a gardener who is, in the words of a character who knows, “stuffed with rice pudding between th’ ears. Shortchanged by the Lord, and dumb as a jackass.” But another character who also knows opts to not reveal the “truth” to avoid destroying his dying friend’s comforting illusion. An act of mercy? Delusion? Both? And then there’s the film’s last shot, which I won’t spoil. Let’s just say it casts doubts on whether any of these characters — or us voyeurs in this world of Chance — really knows what is in front of our eyes. Notes on Note-taking I used ChatGPT more extensively this week than at any previous point in the course. I started a ChatGPT project with the following system prompt: You are an expert tutor in literature, history, music, cinema, and more – in short, the humanities. I am in a year-long program to read through (and listen to and watch) the most important humanities works from across the world. You’ll provide summaries, pointers, and advice. Respond concisely but helpfully. Whenever possible, contextualize content by pointing out possible parallels with the modern day. Within this context, I asked ChatGPT to bring perspective to what I was reading — especially some of the most difficult passages and parallels between the other two Abrahamic traditions. I found parts of surah 4 challenging — especially those that address warfare and justice. I asked ChatGPT for a charitable interpretation of the scripture, and the LLM helpfully responded with explanations that helped me understand the text in its broader context. ChatGPT didn’t recommend it, but this video from Bishop Robert Barron also reminded me that the Judeo-Christian scriptures also have similarly difficult passages and that we must interpret them in the context for which they were written: After watching the TEDEd video about geometry in Islamic architecture, I asked ChatGPT, “How does the repetition through tessellation evident in Islamic decoration reflect the religion’s spiritual values?” I’ll quote its answer verbatim, since I found it both insightful and exemplary of the value I’m getting from using AI in this way: Beautiful question—and one that gets at the visual theology of Islam. Islamic decoration, especially through geometric tessellation, is not just ornamental—it’s deeply expressive of spiritual truths. Here’s how repetition, pattern, and abstraction in Islamic art reflect the core values of the faith: ⸻ 1. Unity in Multiplicity (Tawhid) The oneness of God (tawhid) is Islam’s central belief. Repeating geometric patterns express how divine unity underlies all diversity—a single pattern unfolds into infinite forms, just as all creation reflects the One. Spiritual message: Beneath complexity lies a single divine source. ⸻ 2. Infinity and the Transcendent Tessellations can theoretically extend forever—they have no beginning or end. This evokes infinity, a symbol of God’s eternal, boundless nature. The viewer is drawn into a contemplative state by the endless repetition. Spiritual message: God is beyond time, form, and limit. ⸻ 3. Non-figurative, Non-idolatrous Representation Islamic tradition discourages figural imagery in sacred contexts to avoid idolatry. Abstract, geometric art avoids representing God and instead points toward Him by evoking beauty, harmony, and transcendence. Spiritual message: God cannot be pictured—but can be reflected in patterns of perfection. ⸻ 4. Contemplation and Remembrance (Dhikr) Like repeated prayer or names of God, visual repetition becomes a form of remembrance. Islamic art invites stillness, reflection, and mental surrender—a visual dhikr. Spiritual message: Still the ego, contemplate the divine, dissolve into the pattern. ⸻ 5. Mathematics as Sacred Language Islamic artists used advanced geometry and symmetry, often guided by spiritual cosmology. The precision of the art mirrors the divine order in the cosmos, hinting that the universe is intelligible, ordered, and meaningful. Spiritual message: Creation is a sign (ayah) pointing to its Creator. The LLM included a helpful table: Artistic Feature Spiritual Reflection Repetition / Tessellation Unity, infinity, divine order No images of God or people Transcendence, anti-idolatry Symmetry and precision Balance, harmony, divine perfection Infinite extension God’s boundlessness and eternal presence Meditative patterning Encourages contemplation and surrender It concluded: Islamic art, in this sense, functions like visual theology: it doesn’t depict the divine, but invites you to perceive it—not through representation, but through pattern, balance, and beauty. Would you like to see some examples or diagrams that illustrate how these patterns are constructed mathematically or spiritually? I was intrigued by this last question, especially given that OpenAI just released improved ChatGPT image rendering capabilities. So I responded, “Yes, please show me an example diagram that illustrates these principles.” At this point, ChatGPT generated this image: Up Next We’re moving into the second quarter of the course. Gioia recommends books 1 and 2 of the Aeneid, book 1 of Ovid’s Metamorphoses, and a few other Roman poems, aphorisms, and proverbs. So we’re on our way back to Rome! Again, there’s a YouTube playlist for the videos I’m sharing here. I’m also sharing these posts via Substack if you’d like to subscribe and comment. See you next week!

17 hours ago 1 votes
Yesterday's 'golden age' turns into today's mess

Nick Timiraos for the Wall Street Journal: Corporate America’s Euphoria Over Trump’s ‘Golden Age’ Is Giving Way to Distress Investors were caught flat-footed by Trump’s animus toward Canada, which wasn’t a part of last year’s election campaign. Global

2 days ago 2 votes
Reading List 03/29/25

US bridges at risk of ship collisions, Airbus testing an unducted fan, an earthquake in Myanmar, China’s undersea cable cutter, and more.

2 days ago 2 votes