Nathan Grigg

This looks ominous. (But it wasn’t actually.)

My family was sick all last week, and I fell behind on a tight deadline at work. So in the last two days, I’ve been working like crazy to catch up. Almost done.

I got a debit card for my oldest child so I could move to direct deposit allowance. Dealing with cash gets old fast.

I’ve often wished there were counterparts to the “for Dummies” series labeled “for mathematicians.” Replace 500 pages of babble and sidebars with five pages of formulas and analysis.

I’m trying to figure out a few U.S. tax laws that apply to me for the first time this year.

My thermostat just showed me an advertisement. I’m not happy about this.

Here is a great article on OmniFocus, its current state, and useful tips by Gabe Weatherhead. Like Gabe, I recently started using OmniFocus 2 after some years away.

Brute force

One of the main lessons that I have learned in the last three years in my job as a programmer is that if you only have to do a job once or twice, use brute force.

Today, I was setting up a Synology NAS. The instructions for getting started are extremely sparse, consisting mainly of “type in the IP address in a web browser and follow the instructions there.”

In the old days, I would have started typing IP addresses until I found one that worked. Today, I made a new directory called “ips” and ran

for i in $(seq 50); do
  echo "curl 10.0.1.$i > $i"
done | parallel -j 25

Then, since I am (obviously) impatient, I ran wc -l * every few seconds to see which files were not empty. I found it at

Later I discovered, which is the official way to find your Synology.

I got an Apple Watch for my birthday. So far, the best part is easy access to my calendar while I’m at work.

I would like to write more regularly, so I’m experimenting with a shorter post format, what Manton Reece calls a microblog post. I spent some time today tweaking my Jekyll configuration to enable them on this site.

Uploading an Image with Workflow and Flask

Workflow is an iOS app that lets you build a simple program by dragging blocks around, similar to Apple’s Automator app that ships with macOS. A recent update makes it possible to send a wider variety of HTTP requests, which allows you to interact with web APIs that aren’t otherwise supported.

Or, if you have a web server, write your own API.

Upload images workflow

Here is a workflow to take images from my phone and upload them to my server. It makes one request per image. It sets the custom header Grigg-Authentication to make sure that random people aren’t uploading images. It puts a file into the POST request with field name image. The responses will be HTML image tags, which are collected and then copied to the clipboard.


Flask is a Python web framework. It makes it very easy to map URLs to Python functions.

The first thing I wrote was a private decorator, that would check the HTTP headers for my authentication key. It doesn’t have to be a decorator, but that makes it easier to reuse in the future.

1 KEY = 'password1!'
3 def private(f):
4     @functools.wraps(f)
5     def wrapper(*args, **kwargs):
6         if flask.request.headers.get('Grigg-Authenticate') != KEY:
7             flask.abort(403)
8         return f(*args, **kwargs)
9     return wrapper

If you are not using a secure (HTTPS) connection, somebody could read your authentication key and pretend to be you. You can set this up directly with Flask, but since I’m already running nginx, I used that. (I will share the details in a future post.)

Next, there is some basic Flask setup. I changed the response MIME type to plain text and registered an error handler that will report any exceptions in the response, rather than logging an error where I won’t see it.

 1 app = flask.Flask(__name__)
 3 class TextResponse(flask.Response):
 4     default_mimetype = 'text/plain'
 6 app.response_class = TextResponse
 8 @app.errorhandler(Exception)
 9 def handle_generic_exception(e):
10     return 'Unhandled exception: {!r}\n'.format(e)

Then, there is the routing code. This function is called every time someone visits /blog/upload-image, as specified in the route decorator.

 1 @app.route('/blog/upload-image', methods=['POST'])
 2 @private
 3 def blog_upload_image():
 4     try:
 5         fh = flask.request.files['image']
 6     except KeyError:
 7         flask.abort(400, 'Expected a file with key "image", not found')
 9     _, extension = os.path.splitext(fh.filename)
10     filename = upload_image(fh, extension)
11     return '<img src="{}" class="centered">\n'.format(filename)

Finally, the actual work is done by the upload_image function. I save the image into a dated directory with a random filename, then run a bunch of git commands.

 1 class Error(Exception):
 2     pass
 4 def random_chars(size):
 5     return base64.b32encode(
 6             uuid.uuid4().bytes).decode('ascii').lower().rstrip('=')[:size]
 8 def upload_image(fh, extension):
 9     """Upload image to blog and return filename, relative to site root."""
10     subdir = 'images/{:%Y}'.format(
11     try:
12         os.mkdir(os.path.join(PATH, subdir))
13     except FileExistsError:
14         pass
16     basename = ''.join((random_chars(8), extension))
17     filename = os.path.join(subdir, basename)
18, filename))
20     output = []
21     def run(args):
22         output.append(' '.join(args))
23         output.append(subprocess.check_output(
24             args, cwd=PATH, stderr=subprocess.STDOUT))
25     try:
26         run(['git', 'pull', '--ff-only'])
27         run(['git', 'add', filename])
28         run(['git', 'commit', '-m', 'Add image'])
29         run(['git', 'push'])
30     except subprocess.CalledProcessError:
31         raise Error('Git operation failed. Output:\n{}'.format(
32             '\n'.join(output)))
34     return filename

Time Zone News

If you like time zones—who doesn’t?—you should check out Time Zone News. Once a month or so, I get gems like this in my news feed:

Haiti cancels daylight saving time with two days notice

The planned change to daylight saving time in Haiti at 2 am local time on 13 March 2016 has been cancelled.

Or this one:

Chile reintroduces DST

Chile’s Ministry of Energy announced today that Chile will be observing daylight saving time again. Chile Standard Time will be changed back to UTC -4 at 00:00 on 15 May, and DST will be observed from 00:00 on 14 August 2016, changing time in Chile to UTC -3.

Chile used to observe DST every year until a permanent UTC offset of -3 was introduced in 2015.

It is unclear whether the time change also applies to Easter Island.

Filter RSS

I was looking to make more room on my phone’s home screen, and I realized that my use of had dwindled more than enough to remove it. I never post any more, but there are a couple of people I would still like to follow that don’t cross post to Twitter. has RSS feeds for every user, but they include both posts and replies. I only want to see posts. So I brushed off my primitive XSLT skills.

I wrote an XSLT program to delete RSS items that begin with @. While I was at it, I replaced each title with the user’s name, since the text of the post is also available in the description tag.

Here is the transformation that would filter my posts, if I had any:

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <xsl:stylesheet version="1.0"
 3     xmlns:xsl="">
 5 <!-- Default identity transformation -->
 6 <xsl:template match="@*|node()">
 7     <xsl:copy>
 8         <xsl:apply-templates select="@*|node()"/>
 9     </xsl:copy>
10 </xsl:template>
12 <!-- Replace title with my username -->
13 <xsl:template match="item/title/text()">nathangrigg</xsl:template>
15 <!-- Remove completely items which are directed at other users.
16      The RSS feed has titles of the form @username: text of post. -->
17 <xsl:template match="item[contains(title, '@nathangrigg: @')]" />
18 </xsl:stylesheet>

Now I can use xsltproc to filter the RSS. In order to fill in the username automatically, I wrapped the XSLT program in a shell script that also invokes curl.

 1 #!/bin/bash
 2 set -o errexit
 3 set -o pipefail
 4 set -o nounset
 6 if (( $# != 1 )); then
 7     >&2 echo "USAGE: $0 username"
 8     exit 1
 9 fi
11 username=$1
13 xslt() {
14 cat << EOM
15 <?xml version="1.0" encoding="UTF-8"?>
16 <xsl:stylesheet version="1.0"
17     xmlns:xsl="">
19 <!-- Default identity transformation -->
20 <xsl:template match="@*|node()">
21     <xsl:copy>
22         <xsl:apply-templates select="@*|node()"/>
23     </xsl:copy>
24 </xsl:template>
25 <!-- Replace title with just the username -->
26 <xsl:template match="item/title/text()">$username</xsl:template>
27 <!-- Remove completely items which are directed at other users.
28         The RSS feed has titles of the form @username: text of post. -->
29 <xsl:template match="item[contains(title, '@$username: @')]" />
30 </xsl:stylesheet>
31 EOM
32 }
34 rss() {
35     curl --silent --fail$username/posts
36 }
38 xsltproc <(xslt) <(rss)