Nik Kantar 2021-09-27T00:00:00+00:00 Nik Kantar https://www.nkantar.com/ Bandwidth There’s a limit to how much we can handle. https://www.nkantar.com/blog//blog/2021/09/bandwidth/ 2021-09-27T00:00:00+00:00 I like to do stuff. You probably do too. As does just about everyone else. But there’s a limit to how much of it we can actually do. Our bandwith isn’t infinite.

There are only so many things we can be doing at any given time—too many and it becomes a never-ending to-do list full of anxiety. And there’s also a limit to how big those things can be—too big and just starting feels intimidating and futile.

Sadly, I don’t have any mind blowing thoughts or suggestions. I just wanted to type this out so I can maybe remember it the next time I feel bad about having reached my limit and still wanting to do more. Because I think that I’m actually doing pretty well. And you probably are too.

figurine of combination of dumpster fire and "this is fine" memes

]]>
“Growing apart and losing touch is human and healthy” DHH wrote something poignant in 2018 and here it is. https://www.nkantar.com/blog//blog/2021/09/growing-apart-losing-touch-human-healthy/ 2021-09-18T00:00:00+00:00 DHH wrote something rather poignant in 2018 and I frankly mostly just want to echo it in its entirety.

]]>
Simple, Easy, Complex, Difficult Some language nuance. https://www.nkantar.com/blog//blog/2021/09/simple-easy-complex-difficult/ 2021-09-17T00:00:00+00:00 Some things in life are simple but not easy, while others are easy but not simple. What’s the difference? Let’s look at some examples.

One very relevant to me is that I’d like to ride my bicycle more, which is primarily challenged by the comfort of my bed in the morning. Accomplishing this is simple—I just need to hop on the bicycle and go. However, actually following through is not easy—my bed is really comfortable.

On the opposite end of the spectrum is cleaning and lubricating my motorcycle chain. It’s a frustratingly complex task for an apartment dweller—I have to gather everything (light, keys, gloves, paper towels, cardboard, scrubber, cleaner, lube, etc.), bring it three floors down and halfway through the shared garage to my parking spot, move the motorcycle into a more convenient position, set things up, clean and lube the chain, clean up the inevitable mess, bring everything back upstairs, and put it where it belongs…with a near 100% chance of forgetting something and having to make an extra trip. But the actual work involved is pretty easy—gathering things, walking, cleaning and lubing the chain, and going back upstairs are all rather insignificant tasks.

Of course, there are plenty of things that are both simple and easy—e.g., brushing teeth—and neither—e.g., building distributed systems.

The antonyms for “simple” and “easy” are “complex” and “difficult”, respectively. Thus, one could say that I find riding my bicycle more often simple yet difficult, while I think of motorcycle chain maintenance as complex but easy.

I’m really glad we cleared that up.

]]>
Make the Important Things Easy An old nugget of wisdom. https://www.nkantar.com/blog//blog/2021/09/important-easy/ 2021-09-13T00:00:00+00:00 On an old episode of the rather fantastic Greater Than Code podcast, the inimitable Jessica Kerr said:

“What you make easy, people will do.”
— Jessica Kerr, Greater Than Code: Episode 017 [44:11], January 31, 2017

The line has stuck with me for these four and a half years, as it’s one of the truest things I’ve ever heard, much like the obvious conclusion:

Make the important things easy.

]]>
Hindwards Moved back. https://www.nkantar.com/blog//blog/2021/08/hindwards/ 2021-08-30T00:00:00+00:00 Six years ago I moved from Los Angeles to Santa Clara, and two years later I moved back.

With the great power of hindsight, I realized that suburban life—and especially that suburban life—wasn’t for me. I missed the walks, drives, weather, attitude, diversity, food, and cars. I missed everything else, too. I missed the good stuff. I missed the bad stuff. I missed home.

LA sky

Home truly is where the heart is.

]]>
Starminder v2 Starminder is relaunching with notable changes. https://www.nkantar.com/blog//blog/2021/08/starminder-v2/ 2021-08-22T00:00:00+00:00 TL;DR: The current version of Starminder is going away on 9/30/2021—check the repo for the new hotness.


A couple of weeks ago I wrote a bit about the future of some of my projects. Today, Starminder is ready enough for its transition to begin. ⭐

What It’s Been

Starminder has been a web app through which users could log in with their GitHub account and set a schedule on which they would receive an email containing a few of their starred projects, as a reminder.

Here’s one such email:

Example Starminder email

Its raison d’etre is that I found myself having starred hundreds of projects—effectively turning my stars into a graveyard of thoughts like “I should look into this more”—and I wanted to prune them occasionally. A few other people found some reasons to sign up as well.

But I haven’t cared enough to maintain the web app, and I don’t like the idea of holding onto people’s API tokens through a publicly available piece of software with rapidly declining security, so it had to go.

What It’s Becoming

Starminder shall continue to live, but in a very different shape.

See, the valuable part of the project was never the web app—that was merely a mechanism for configuration. The valuable part are the emails. And email sending can be triggered in a lot of other ways. Like, for example, via cron.

So, Starminder is now becoming a script triggered via GitHub Actions. It will require forking and a bit of setup, and will allow me to avoid having to worry about any substantial infrastructure. It will also allow me to sidestep holding onto anyone else’s API tokens, which is great.

The repo has setup instructions. The web app will go away on Thursday, 30 September, 2021. Happy forking. 🍴

]]>
Project Directions Some of my projects are languishing and need rethinking. https://www.nkantar.com/blog//blog/2021/08/project-directions/ 2021-08-09T00:00:00+00:00 Hi. I’m still in the process of ingesting and digesting this hard to swallow pill, so please bear with me. :D

Here’s my GitHub activity for the past year, as of today, Monday, 9 August, 2021:

My GitHub activity

As you can see, there was a flurry of excitement in the first two months of the year, and, uhh, well not a whole lot since—or before. I’m currently in the acceptance stage about the fact that I’m just not that dedicated to maintaining a bunch of low impact projects. The reality is that I also enjoy a lot of things that aren’t software, and my day job is v. awesome and fulfilling.

Which begs the question of the futures of my various projects. Libraries and command line tools are already in a good place, effectively being in maintenance mode, so there’s really nothing to cover there. Some of the web apps I have are also fine as they are, but two in particular are in need of serious rescoping: Starminder and Microblot.

Starminder

Starminder is currently a woefully outdated Flask app, with dependencies last updated…nevermind. I recently rewrote the whole thing in Django to consolidate my projects around a unified stack for consistency, but have since realized that’s still not good enough. I’m currently working on an implementation that would deprecate the web app entirely for the price of some extra user effort.

Microblot

Microblot isn’t launched yet, and at this point I don’t think it will be in its current state. The current (tentative) implementation is designed as a multi-tenant SaaS web app for me to run for everyone, and I’ve recently realized that would be a full time job in its own right, except for the lack of a paycheck. I’m still not quite sure what I’ll do with it, but one good option is downscoping it to a single-tenant web app everyone could run for themselves. The parts relevant for that are actually mostly done, amusingly enough.

]]>
Quick and Dirty Python: HOWTO Let’s build up small Python script. https://www.nkantar.com/blog//blog/2021/07/quick-dirty-python-howto/ 2021-07-18T00:00:00+00:00 A few months ago I shared a script I wrote to automate a tedious task. While that’s all well and good, I thought it might be fun to go through the process of actually writing it, step by step. For more thorough context you can refer to that post, but we’ll go over the basics here for the sake of completeness.

The Problem

We need to figure out the best health insurance plan based on our main need: out-of-network mental health coverage.

Here is the data we have to work with:

Plan Name Deductible Percentage Reimbursed Monthly Premium
Bronze $150 20% $0
Silver $300 35% $25
Gold $500 50% $75
Platinum $1000 65% $100

And here’s the formula to actually calculate the annual cost:

  (annual cost - deductible) * percentage paid
+ deductible
+ annual premiums
= total

The target output is probably a list of totals. We’ll figure that out when we get there.

The Skeleton

I start every Python script with the following boilerplate:

#!/usr/bin/env python3


def main():
    ...  # TODO


if __name__ == "__main__":
    main()

The Skeleton, Part One

The first part is a hashbang:

#!/usr/bin/env python3

This line enables us to mark the file as executable (via chmod +x) and run it without specifying the Python interpreter (./calc.py instead of python3 calc.py). I don’t actually do this too often, but it’s not a bad habit, since it does happen sometimes.

The Skeleton, Part Two

The second part is just an empty function:

def main():
    ...  # TODO

This is where we’ll put stuff we actually want to do. Its value will become clear in the next section.

The Skeleton, Part Three

The last part is one of my favorite bits of Python magic:

if __name__ == "__main__":
    main()

In short, these two lines ensure that the main function is executed when the file is run. Merely importing something from it in another file won’t trigger the execution, meaning our script is immediately also a reusable module. How’s that for convenience? You can read heaps more about this on Real Python.

The Data

Since we already know what the source data looks like, we can add it now, starting with the plans:

PLANS = [
    {"name": "Bronze", "deductible": 150, "percentage": 0.2, "monthly": 0},
    {"name": "Silver", "deductible": 300, "percentage": 0.35, "monthly": 25},
    {"name": "Gold", "deductible": 500, "percentage": 0.5, "monthly": 75},
    {"name": "Platinum", "deductible": 1000, "percentage": 0.65, "monthly": 100},
]

Looking at our formula, another fixed data point is the annual cost of therapy:

SESSION = 100  # single session cost
GROSS = 52 * SESSION

Protip: since utmost performance isn’t a consideration in this case, there’s nothing wrong with calculating the actual cost by multiplying the cost of a single session by the number of weeks in a year. This is a lot more legible and less error-prone than hardcoding GROSS = 5200, and really shows its benefits for more complex use cases, like for example the number of seconds in a year: YEAR_IN_SECONDS = 365 * 24 * 60 * 60 vs. YEAR_IN_SECONDS = 31_536_000.

I like to keep immutable data in constants at the top of the file, as it’s thus separate from the mutable world of business logic and visually out of the way, so we get this:

#!/usr/bin/env python3


SESSION = 100  # single session cost
GROSS = 52 * SESSION
PLANS = [
    {"name": "Bronze", "deductible": 150, "percentage": 0.2, "monthly": 0},
    {"name": "Silver", "deductible": 300, "percentage": 0.35, "monthly": 25},
    {"name": "Gold", "deductible": 500, "percentage": 0.5, "monthly": 75},
    {"name": "Platinum", "deductible": 1000, "percentage": 0.65, "monthly": 100},
]


def main():
    ...  # TODO


if __name__ == "__main__":
    main()

The Business Logic

The Test

Our script doesn’t actually do anything useful yet, so let’s change that by testing out what we’ve set up so far:

def main():
    for plan in PLANS:
        print(plan["name"])

Running this produces the following output:

Bronze
Silver
Gold
Platinum

Great—we’re in business!

The Math

Now that we have iteration and output, we can start implementing the math:

def plan_total(plan):
    post_deductible = GROSS - plan["deductible"]
    percentage = 1 - plan["percentage"]  # we want percentage of cost, not reimbursement
    premiums = 12 * plan["monthly"]
    total = (post_deductible * percentage) + plan["deductible"] + premiums
    return total

def calculate_plan_totals():
    for plan in PLANS:
        text = f"{plan['name']}: {plan_total(plan)}"
        print(text)

if __name__ == "__main__":
    calculate_plan_totals()

At this point we can already separate plan total calculation into its own function for legibility. I’m a big fan of small functions like this because of how neatly they package discrete functionality.

We can also rename main into calculate_plan_totals, as that’s what it actually does.

Running this produces effectively what we’re looking for:

Bronze 150: 4190.0
Silver 300: 3785.0
Gold 500: 3750.0
Platinum 1000: 3670.0

What’s not so ideal is that my actual list was much longer than this, and it wasn’t exactly a given that I’d spot the lowest number myself. Wouldn’t it be great if the script could tell us that? Of course it would!

The Lowest

For the sake of satisfyingly pretty UI, let’s add an arrow pointing at the lowest plan. For that we need to calculate all the totals and find that lowest value before outputting anything:

def calculate_plan_totals():
    plans = [{"name": plan["name"], "total": plan_total(plan)} for plan in PLANS]
    lowest = min([plan["total"] for plan in plans])
    for plan in plans:
        text = f"{plan['name']}: {plan['total']}"
        if plan["total"] == lowest:
            text = f"{text} <-"
        print(text)

There are now two iterations, which is sure to incur a performance hit, but we can probably spare a few extra milliseconds, especially considering how cool our output is now:

Bronze 150: 4190.0
Silver 300: 3785.0
Gold 500: 3750.0
Platinum 1000: 3670.0 <-

The Script

In its final form, the script looks like this:

#!/usr/bin/env python3


SESSION = 100  # single session cost
GROSS = 52 * SESSION
PLANS = [
    {"name": "Bronze", "deductible": 150, "percentage": 0.2, "monthly": 0},
    {"name": "Silver", "deductible": 300, "percentage": 0.35, "monthly": 25},
    {"name": "Gold", "deductible": 500, "percentage": 0.5, "monthly": 75},
    {"name": "Platinum", "deductible": 1000, "percentage": 0.65, "monthly": 100},
]


def plan_total(plan):
    post_deductible = GROSS - plan["deductible"]
    percentage = 1 - plan["percentage"]  # we want percentage of cost, not reimbursement
    premiums = 12 * plan["monthly"]
    total = (post_deductible * percentage) + plan["deductible"] + premiums
    return total


def calculate_plan_totals():
    plans = [{"name": plan["name"], "total": plan_total(plan)} for plan in PLANS]
    lowest = min([plan["total"] for plan in plans])
    for plan in plans:
        text = f"{plan['name']}: {plan['total']}"
        if plan["total"] == lowest:
            text = f"{text} <-"
        print(text)


if __name__ == "__main__":
    calculate_plan_totals()

Pretty cool, right?

The Bonus

There are always things we can do to refactor a given piece of code. Always.

We could use sorted and define a custom sorting function to use the calculated totals and not iterate through the list twice ourselves. We could probably clean up some variable names—I’m mildly annoyed at the lack of clarity of percentage, for example. We could add function parametrization to make everything even more reusable. We could add types, tests, and docs.

But this is probably enough. It’s a simple script that does what it needs to do. Just some quick and dirty Python. :)

]]>
Quick and Dirty Python Something I absolutely love about Python is just how well it scales from tiny scripts to complex systems. Here’s an example of the former. https://www.nkantar.com/blog//blog/2021/03/quick-dirty-python/ 2021-03-31T00:00:00+00:00 I’ve spent a number of years now building monolithic applications and distributed systems with Python, but a big part of why I love the language so much is how well it serves simple scripting needs as well. That’s not as often talked about, and I suppose that makes sense, but I think it’s worth being reminded of occasionally.

The Problem

I had to choose a health insurance plan for work recently, which is always a pain in the butt—all those numbers, all those caveats, all that legalese. In the end, the thing that matters most to me is reimbursement for out-of-network mental health services, since therapy is easily my largest healthcare cost.

Here’s what the (made up) choices look like:

Plan Name Deductible Percentage Reimbursed Monthly Premium
Bronze $150 20% $0
Silver $300 35% $25
Gold $500 50% $75
Platinum $1000 65% $100

A quick glance provides no easy answers: the Bronze plan is appealing with its low deductible and premium, but the Platinum plan has such a high reimbursement percentage, and over the course of a full year it’s not so clear which one is more cost effective—or whether it’s even one of the others.

The formula to calculate the full annual cost looks something like this:

  (annual cost - deductible) * percentage paid
+ deductible
+ annual premiums
= total

OK, that’s not too bad, but it’s tedious to do for 15 plans. However, computers are really good at math, and I’ve got one right here in front of me.

The Python

I turned the plan data and formula into some Python, and had it do the math for me. The code is pretty straightforward:

#!/usr/bin/env python3


SESSION = 100  # single session cost
GROSS = 52 * SESSION
PLANS = [
    {"name": "Bronze", "deductible": 150, "percentage": 0.2, "monthly": 0},
    {"name": "Silver", "deductible": 300, "percentage": 0.35, "monthly": 25},
    {"name": "Gold", "deductible": 500, "percentage": 0.5, "monthly": 75},
    {"name": "Platinum", "deductible": 1000, "percentage": 0.65, "monthly": 100},
]


def plan_total(plan):
    post_deductible = GROSS - plan["deductible"]
    percentage = 1 - plan["percentage"]  # we want percentage of cost, not reimbursement
    premiums = 12 * plan["monthly"]
    total = (post_deductible * percentage) + plan["deductible"] + premiums
    return total


def calculate_plan_totals():
    plans = [{"name": plan["name"], "total": plan_total(plan)} for plan in PLANS]
    lowest = min([plan["total"] for plan in plans])
    for plan in plans:
        text = f"{plan['name']}: {plan['total']}"
        if plan["total"] == lowest:
            text = f"{text} <-"
        print(text)


if __name__ == "__main__":
    calculate_plan_totals()

I saved this to a file called calc.py—no virtual environments, no repository, no nothin’, just a simple script to do one thing with minimal fuss—and ran it.

The Result

$ python3 calc.py
Bronze 150: 4190.0
Silver 300: 3785.0
Gold 500: 3750.0
Platinum 1000: 3670.0 <-

As I suspected, with the above numbers it’s actually the Platinum plan that ends up being the most cost effective. With my actual numbers it was also one of non-obvious choices that won out.

The Conclusion

Python is great. Sure, you can use it to build really complex stuff to solve large problems, and you absolutely should, but you can also use it to build really simple stuff to solve small problems, and you should do that too.

The Bonus

Larry Hastings has a great talk about just this sort of thing. It was one of my PyCon 2018 favorites.


Update: I wrote a follow-up: Quick and Dirty Python: HOWTO.

]]>
HEY? Nay. I got super excited for a new awesome looking email service, but it’s got a fatal flaw that has me looking elsewhere. https://www.nkantar.com/blog//blog/2021/03/hey-nay/ 2021-03-01T00:00:00+00:00 Last year the folks at Basecamp launched a really intriguing email service called HEY. While I’ve never really gotten into Basecamp itself, I have a fair bit of respect for that team, and the HEY pitch is appealing, so I’ve been keeping an eye on it.

I’ve actually been wanting to move my nkantar.com email off of Gmail for years now, and Fastmail has been the main contender for most of that time. Laziness has prevented me from following through so far, but I’ve gotten more and more interested in doing so in the last year. The timing of HEY’s launch couldn’t have been better.

Except there’s a deal breaker: no import.

Lack of Import = Sadness :(

I have two main email accounts I use: one is on this very domain, and the other on gmail.com. I figured no service would let me import the latter, so I’ve been slowly transitioning away from it. However, I’d like the history from the former to come with me wherever I go.

At this very moment I have 5,817 emails on this domain and 19,805 on gmail.com. This includes everything in the inbox, sent, and archive. The addresses date back to 2009 and 2007, respectively. There’s a lot of stuff in there that I honestly don’t need, but there’s also quite a bit that I do.

One of the things I remember Gmail introducing into my email use—in addition to the vast storage—was incredible search. I almost never use anything more clever than just some relevant search terms, but that’s enough. I’d say I search for something in at least one of the two accounts at least once per day.

The HEY team suggests using the outgoing email account and apps to search for these things from the past. In theory this could actually work if my searching needs moved along with time. I could double up for some number of months and slowly do it less and less. But my searching needs are much too arbitrary for that—I find myself looking for things from years back often enough that I’d expect to never shake the old setup. Maybe I’m just being difficult here, but that sounds like a pretty serious downgrade, and I’m not at all convinced the upsides of HEY’s novel approach would outweigh it.

Is There a Fix Coming?

In short: no.

I haven’t found anything other than “nope” from anyone at Basecamp/HEY regarding this subject. I’ve heard the reasoning that it would be hilariously resource intensive, and I get that. I’ve also deduced that it breaks the concept a bit, as it would be this pile of untriaged stuff from the past, and sort of get that as well. But I’ve also read the rather glib “HEY is a fresh start moving forward” from the FAQ, and find it offensive and condescending. Either way, given the amount of thought obviously put into the whole thing, I can only conclude I’m simply not in their target market, which seems to be primarily comprised by people willing and able to declare email bankruptcy. I wish.

It’s a real bummer, too, since I like so much about the service. Novel approach to handling incoming mail? Looks appealing. Focus on privacy? Wonderful. Business offering supporting custom domains? Love it. Great looking web, macOS, and iOS apps? Yes, please. Reasonable pricing? Awesome.

What About a Compromise?

What I really want is to not have to keep the Gmail app on my phone and log into Gmail in the browser to search through old mail. That means that just about any way I could do that from HEY should work:

  • Shove everything into the Screener, and force me to triage 25,000 emails. After all, it is my fault.
  • Leave everything in the Imbox, and make me mark things as less important from that point on. Again, my problem.
  • Put everything into the Feed or even Paper Trail.
  • Add a new Archive section with nothing but basic search.
  • Some completely different approach that still accomplishes this.

As you can see, I’m rather flexible on the implementation.

Now What?

Well, I guess Fastmail is still a solid choice. I’d be willing to wait for months—even a year—if I had any hope this was going to be addressed in a way that works for me (and surely many, many others), but the team has pretty unequivocaly denied it. Still a bummer.

I hope I’m proven wrong before I do anything about this, but I’m not holding my breath. Oh well.

]]>