blog CSV with Frontmatter to manage events and tasks

Tags:
calendar csv pim python

I tend to use my calendar and reminders to track a lot of what I want to do, but sometimes I want to manage the data externally and sync. For certain kinds of repeating tasks, I wanted to edit a list of tasks in a group, and it seemed like csv might work. While csv does not have a strong specification, many programming tools make it easy to work with. The initial format I thought about was something simple.

date,summary
2024-01-01,something
2024-01-02,something
2024-01-03,something

Using the Python DictReader class, it is quite easy to parse a csv file into something we can loop through for our entries. Calendar entries typically have a uid to ensure uniqueness, so using uuidgen|pbcopy in our terminal, gives us a quick way to add a UID to our entries.

date,uid,summary
2024-01-01,BBF7FEAF-C775-4504-8B30-F6AE86AC6F22,something
2024-01-02,CDE1EFB7-E1B3-4DB6-8ECF-3EA542406E24,something
2024-01-03,EBB76613-19C5-435A-B2C1-2BA019A132B6,something

This means when we loop through our events, we can then replace the ones we want.

Another problem is that sometimes we may want to set meta data for one of our files but csv does not support a lot of customization. Remembering the frontmatter in many blog systems, we can use something like python-frontmatter to add in our metadata.

---
calendar: my-events
type: todo # Could also use event
---
date,uid,summary
2024-01-01,BBF7FEAF-C775-4504-8B30-F6AE86AC6F22,something
2024-01-02,CDE1EFB7-E1B3-4DB6-8ECF-3EA542406E24,something
2024-01-03,EBB76613-19C5-435A-B2C1-2BA019A132B6,something

I often like having an external link for where the task is actually performed, and icalendar supports a URL field.

---
calendar: my-events
type: todo # Could also use event
---
date,uid,summary,url
2024-01-01,BBF7FEAF-C775-4504-8B30-F6AE86AC6F22,pay bills A,https://creditcard.example.com
2024-01-02,CDE1EFB7-E1B3-4DB6-8ECF-3EA542406E24,pay bills B,https://electric.example.com
2024-01-03,EBB76613-19C5-435A-B2C1-2BA019A132B6,recurring maintenance,https://manuals.example.com/exampleC

Now we can separate out our metadata from the rest of our csv file.

# Process our file through the frontmatter package to split it into metadata
# and content
import frontmatter
post = frontmatter.load(fp)
# We can then pull out any specific metadata we want to work on. Like which
# calendar we want to update and if we're working with VEVENT or VTODO
calendar = Calendar.lookup(post["calendar"])
object_type = klass(post.get("type", "todo"))
# Lastly we can use the same DictReader class to read through the content lines
# as normal.
for row in csv.DictReader(post.content.splitlines()):
    print(row)

Now that we have individual row entries, we can use something like icalendar to process that into a calendar entry.

While the rest of my code is still in flux, and not quite ready to publish, it means the rest of the processing looks fairly simple.

for row in csv.DictReader(post.content.splitlines()):
    # For each row in our file, lets figure out the URL we're fetching
    # so that we can update it. caldav doesn't really have an idea about HTTP PATCH
    # so we do a GET, replace some stuff, and PUT it back.
    url = calendar.url(row["uid"])
    obj = object_type.get_or_create(uid=row["uid"], url=url)

    # Loop through each field in our row and process it. Using a match here is
    # probably not great, but it's shown as an example where you only want to
    # process certain fields, and possibly do some kind of checks or manipulations
    for key in row:
        match key:
            case "due" | "dtstart" | "dtend":
                if isinstance(value, str):
                    value = parse_date(row[key])
                else:
                    value = row[key]
                obj.replace(key, row[key])
            case "summary":
                obj.replace(key, row[key])
            case "priority":
                obj.replace(key, row[key])
            case "description":
                obj.replace(key, row[key])
            case "url":
                obj.replace(key, row[key])
            case "rrule":
                value = icalendar.vRecur.from_ical(row[key])
                obj.replace(key, value)
    # Lastly we put our object back.
    obj.put(url)

While there is more code I want to cleanup before I publish something public, this is already making it easier to do some high level planning of my calendar/tasks in a format that is easier to see all at once.

Using the same code, I also have a version that can go through markdown+frontmatter files and use those for events/reminders.