Skip to content

Add Tumblr error handling, response validation, and typed SocialPostingVertical#83

Draft
aaronjae22 wants to merge 1 commit intomainfrom
tumblr-response-hardening
Draft

Add Tumblr error handling, response validation, and typed SocialPostingVertical#83
aaronjae22 wants to merge 1 commit intomainfrom
tumblr-response-hardening

Conversation

@aaronjae22
Copy link
Collaborator

Closes #22
Closes #45
Closes #62

The Tumblr integration returned raw JSON, and we were not validating what Tumblr sent back, and any API failure would surface as a cryptic Python error instead of something useful. This brings it up to the same standard as Strava.

  • Added TumblrAPIError so Tumblr failures are identifiable and have useful messages. Before, a 401 from Tumblr would just be a generic HTTPError, and a malformed response would crash with something like AttributeError: 'NoneType' object has no attribute 'get'. Now we get TumblrAPIError: Tumblr API error: ... with the status code and a description of what went wrong.

  • Both user/info and user/dashboard responses are now checked for the expected structure before anything tries to read from them. If Tumblr returns 200 but the payload is weird (missing keys, wrong types), it raises TumblrAPIError right away instead of silently producing garbage data.

  • Added parse_social_posting_vertical that turns a raw Tumblr post into a SocialPostingVertical model — mapping things like post ID, URL, timestamp, tags, note count, text blocks, and media URLs into the standard schema. Same approach Strava already uses.

  • fetch_social_posting_vertical now returns (parsed_verticals, raw_posts) instead of just a list of dicts. Matches Strava's pattern so consumers get both structured data and raw JSON.

  • The updated testsuite covers error wrapping, validation, and parsing.

Pagination #8 is out of scope here.


These new features don't change anything in the demoing approach.

from pardner.services.tumblr import TumblrTransferService
from core.models import DonatedPost, ServiceAccount
import json

# Get the most recent donation
sa = ServiceAccount.objects.order_by('-completed_donation_at').first()
post = DonatedPost.objects.filter(service_account=sa).first()

# Parse through the new parser
svc = TumblrTransferService('x', 'x', 'http://localhost')
vertical = svc.parse_social_posting_vertical(post.raw_data)

# See the structured output
print(json.dumps(vertical.model_dump(), indent=2, default=str))
Structured output example
{
  "pardner_object_id": "e6971577c78b49c6963cabdf6cebf529",
  "service_object_id": "810233790391877632",
  "creator_user_id": "t:iE-yd_-VKiQ4tRh4kkzLeg",
  "data_owner_id": "",
  "service": "Tumblr",
  "vertical_name": "social_posting",
  "created_at": "2026-03-05 08:25:56",
  "url": "https://angelswouldnthelpyou.tumblr.com/post/810233790391877632/david-lynch-and-sheryl-lee-twin-peaks-fire-walk",
  "abstract": "David Lynch and Sheryl Lee \nTwin Peaks Fire Walk With Me 1992",
  "associated_media": [
    {
      "media_type": "image",
      "url": "https://64.media.tumblr.com/d020bdb6ba1834edc05117b2f7ef3f84/37d16fd99f65962b-60/s1280x1920/ae567fc16c7d32e5359142ead8c14c8c43225c70.jpg"
    },
    {
      "media_type": "image",
      "url": "https://64.media.tumblr.com/d020bdb6ba1834edc05117b2f7ef3f84/37d16fd99f65962b-60/s640x960/82e3c61df944a7acdbee8da566ad5001411912b8.jpg"
    },
    {
      "media_type": "image",
      "url": "https://64.media.tumblr.com/d020bdb6ba1834edc05117b2f7ef3f84/37d16fd99f65962b-60/s540x810/f1a2d05bade2e5d0a9e6cc17050fac2475214b49.jpg"
    },
    {
      "media_type": "image",
      "url": "https://64.media.tumblr.com/d020bdb6ba1834edc05117b2f7ef3f84/37d16fd99f65962b-60/s500x750/d6fbe2d7c7d4746aab542717af43e8aeaabd84dc.jpg"
    },
    {
      "media_type": "image",
      "url": "https://64.media.tumblr.com/d020bdb6ba1834edc05117b2f7ef3f84/37d16fd99f65962b-60/s400x600/9528b99630774c979fd9afe77f41a09f27eef657.jpg"
    },
    {
      "media_type": "image",
      "url": "https://64.media.tumblr.com/d020bdb6ba1834edc05117b2f7ef3f84/37d16fd99f65962b-60/s250x400/c8b9576de6f9f79b52c5fcf5952a42115b8d2031.jpg"
    },
    {
      "media_type": "image",
      "url": "https://64.media.tumblr.com/d020bdb6ba1834edc05117b2f7ef3f84/37d16fd99f65962b-60/s100x200/62762543123ff8cad1d4467159d8d73ad827ed05.jpg"
    },
    {
      "media_type": "image",
      "url": "https://64.media.tumblr.com/d020bdb6ba1834edc05117b2f7ef3f84/37d16fd99f65962b-60/s75x75_c1/1e79db2eb2321ea7f1bdcdd309634206d38819b4.jpg"
    }
  ],
  "interaction_count": 16,
  "keywords": [
    "twin peaks fire walk with me",
    "david lynch",
    "sheryl lee",
    "laura palmer",
    "twin peaks",
    "cinema"
  ],
  "shared_content": [],
  "status": "public",
  "text": "David Lynch and Sheryl Lee \n\nTwin Peaks Fire Walk With Me 1992",
  "title": null
}

The parsed SocialPostingVertical models are not being saved to the database right now. This is intentional.

  1. The DonatedPost model only has a raw_data JSONField. There's no column or table for structured vertical data
  2. I wanted to preserve the site's access to raw Tumblr JSON and follow the Strava pattern of returning both
  3. The site currently does _, raw_posts = ... — the _ discards the parsed verticals and only raw_posts gets stored

@aaronjae22 aaronjae22 self-assigned this Mar 19, 2026
@aaronjae22 aaronjae22 requested a review from lisad March 19, 2026 00:28
) -> dict[str, Any]:
return super().fetch_token(code, authorization_response, include_client_id)

def _validate_user_info_response(self, data: Any) -> dict[str, Any]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is exactly what JSON Schema is for ! probably not worth it for this project, but validating a bunch of JSON in python code is a pain isn't it

super().__init__(f'Cannot fetch data from {service_name}: {message}')


class TumblrAPIError(Exception):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems too specific if we were going to invest in many more connectors for this library. How would we handle TumblrAPIError differently from StravaAPIError? It makes it harder to write code that uses this library - new exceptions get defined when new connectors get added, and then code using this library has to get written to handle new exceptions.

Choosing how to categorize exceptions and how to group them is an interface design question which depends on how a library is going to be used, of course - might be fun to discuss tomorrow or sometime.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants