July 30, 2019, 1 a.m.
I built what I like to call a dessert feature on Friday — Instagram embedding.
This is strictly not mission critical; I think I have had a grand total of one person request this feature, but it’s been kicking around my backlog for ages (Buttondown is now at the point where backlog staleness can be measured in years rather than months, which is fun!)
But this is a good feature to build and deliver in two hours:
It’s well-scoped, using primitives that I know my way around.
Buttondown’s markdown rendering platform is one of the more clever pieces of over engineering that I’ve committed myself to, and it means that all I need to do to write up a new embed is this (and this is actual production code):
@generate_block_processor_extension class InstagramExtension(EmbeddedLinkBlockProcessor): prefix = "https://www.instagram.com" def render(self, url): photo = InstagramDataFetcher(url).execute() return render_to_string("instagram.html", {"instagram": photo})
Literally that’s it; the block processor
takes any paragraph that just contains an Instagram link, fetches the data for it, and spits out a template. This means I don’t have to do anything gross with regular expressions or syntax trees or fallback logic; Past Me has already taken care of that.
Fetching the data, though, was a little trickier. Instagram’s API is notoriously volatile and closed-off. I tried a couple ways:
__a=1
parameter, which of course is probably the sketchiest solution of all three but by far the easiest. It has all the data I need to fetch — and more.Once I have JSON data, piping it into a template and writing some basic harness tests is easy:
@dataclass class InstagramDataFetcher: instagram_url: str def execute(self) -> Photo: cached_photo = INSTAGRAM_CACHE.get(self.instagram_url) if cached_photo: return cached_photo response = requests.get(self.instagram_url + "?__a=1").json() media = response["graphql"]["shortcode_media"] photo = Photo( media["edge_media_to_caption"]["edges"][0]["node"]["text"], media["owner"]["username"], f"https://instagram.com/{response['graphql']['shortcode_media']['owner']}", media["owner"]["profile_pic_url"], media["display_url"], self.instagram_url, datetime.fromtimestamp(media["taken_at_timestamp"]), media["edge_media_preview_like"]["count"], media["edge_media_to_parent_comment"]["count"] if "edge_media_to_parent_comment" in media else 0, ) INSTAGRAM_CACHE.set(self.instagram_url, photo) return photo
(I cleaned up the JSON parsing here, but not as much as you’d expect; again, the ‘block processor’ construct gracefully handles errors.)
Because, after all, it was a Friday, I went the “smoke test” route (meaning I am confident in the data being piped in correctly, and I just want to make sure nothing explodes):
class InstagramDataFetcherTestCase(TestCase): @backed_by_network_fixture def test_basic(self): cases = [ "https://www.instagram.com/p/B0W8bYBJN0D/", "https://www.instagram.com/p/B0XaesrBhJl/", "https://www.instagram.com/p/B0WNCUdCtMp/", "https://www.instagram.com/p/B0YVdvYAlMg/", "https://www.instagram.com/p/B0XP7QSnVAm/", "https://www.instagram.com/p/B0Z0MXtnw0H/", "https://www.instagram.com/p/B0YqyoopB9f/", ] for case in cases: InstagramDataFetcher(case).execute()
And voilá. A tiny feature with a tiny test harness. This test harness will grow over time; my Twitter embedding test harness started out the same size, and has grown more voluminous — but in my experience the most interesting edge cases end up revealing themselves in time.
I’m currently auditing a couple different customer service CRM tool things. I have officially reached the point where “answer emails and hope I don’t forget anyone” is an unsustainable strategy for incoming requests and bug reports, which is a nice problem to have but also terrible!
My dream is something along these lines:
I am confident this toolchain and workflow exists — this is how pretty much everyone does it, right? — but I still haven’t figured out the right combination of things to search for.