How I Color-Coded Historical Events in My Timeline Builder

Day 83 of 100 Days Coding Challenge: Python

I’ve loved history for as long as I can remember. By fourth grade, I was devouring children’s versions of The Histories by Herodotus while other kids were still stuck on Goosebumps. My parents’ library was my playground, and I spent hours flipping through volumes that smelled faintly of dust and mystery. By the time school took history seriously in high school, I was already knee-deep in maps and timelines.

Back then, I didn’t know the word “mind mapping,” but that’s essentially what I was doing. I’d draw crude maps, sketch arrows, and connect civilizations like I was building my own analog version of Wikipedia. Today’s task reminded me of that hobby: giving my timeline builder a taxonomy of event kinds—wars, dynasties, tech, culture, economy, religion—so that I can glance at a timeline and instantly know what kind of drama unfolded. Think of it as giving history not just a shape, but also a color palette.

Today’s Motivation / Challenge

Why bother with taxonomies? Because not all events are created equal. A war feels very different from an invention, and a dynasty change isn’t the same as a religious reform. By categorizing events and slapping on colors and icons, the app becomes more than a list of “things that happened.” It becomes a visual storybook where you can spot patterns at a glance—wars clustering together, bursts of culture, or dynasties toppling like dominoes.

Purpose of the Code (Object)

The code assigns each event a category (“kind”) and gives it a color and icon. When you view a civilization’s timeline, events appear as dots along its bar, each colored by type. A neat legend ties it all together, letting you see not just when things happened, but what kind of things they were. It’s like adding subtitles to history.

AI Prompt

Today is the day 8 of this project. Please add the following script:
Day 8 — Event types & icons

  • Kind taxonomy (war/dynasty/tech/culture/economy/religion).
  • Accept: legend renders; event dots color by kind.

Functions & Features

  • Define taxonomy of event types (war, dynasty, tech, culture, economy, religion, other).
  • Assign each taxonomy a color and icon for clarity.
  • Display event dots along timeline bars, grouped and color-coded by type.
  • Add a clean legend so users know exactly what each dot means.

Requirements / Setup

You’ll need:

  • Python 3.11

Installs:

pip install plotly streamlit

Minimal Code Sample

KIND_COLORS = {

    “war”: “red”, “dynasty”: “blue”, “tech”: “green”,

    “culture”: “purple”, “economy”: “orange”, “religion”: “gold”

}

for kind, group in grouped_events.items():

    fig.add_scatter(

        x=group[“year”], y=group[“civ”],

        mode=”markers”, name=kind.capitalize(),

        marker=dict(color=KIND_COLORS.get(kind, “gray”))

    )

Each event type gets its own scatter trace with a color-coded legend.

The Civilization Timeline Builder

Notes / Lessons Learned

We started by defining the taxonomy—deciding which categories mattered most. I learned from yesterday’s naming mishap that consistency is everything, so I double-checked each description to keep it tidy. Each taxonomy got a color, and if an event didn’t fit, we just shoved it into “Other.” Practical and honest.

One fun surprise: Python 3.11’s str | None is cleaner than Optional[str], though you may see both depending on your version. Reddit was full of debates about this, but personally, I like the shorthand.

The real magic came when we modified the build_time_band function to group events by kind and add scatter traces only if there were points. That way, the legend didn’t become cluttered with empty categories. Now the timelines look like candy-colored storylines, dotted with wars, dynasties, and cultural sparks. The result? A clean legend across the top and timelines that actually feel alive.

Optional Ideas for Expansion

  • Add tiny event icons (like swords for wars or scrolls for culture) alongside the dots.
  • Let users filter the timeline by event kind (e.g., “Show me only tech”).
  • Stack events into mini-bands under each civ for an even clearer breakdown.

Mapping Civilizations with Plotly in the Timeline App

Day 82 of 100 Days Coding Challenge: Python

Today’s quest was to give my civilizations a home on the globe. The idea was simple: accept the centroid latitude and longitude so I could finally see where each empire sat. Yesterday’s Poetry setup was already in place, so I dove straight into app/models/civilization.py and added two nullable float columns. Import section checked—no duplicates this time (lesson learned from yesterday’s chaos).

After migrating the database, I seeded some coordinates via a shiny new CSV file in data/seeds/civ_centroids. I even wrote a quick one-off script to update rows by slug. Feeling clever, I turned to streamlit_app.py, wiring it to fetch centroids and display them in the Civilization view. The beauty here? Both the timeline and map shared the same filters, so when I adjusted one, the other politely followed along.

Everything looked great—until I noticed two glaring absences. Rome and Sumer were missing, as if they’d been conquered all over again. The culprit? My seed data. Apparently, I’d named one “Rome” instead of “Roman,” and completely skipped Sumer. Maps don’t forgive typos, but at least the debugging process made me laugh instead of cry.

Today’s Motivation / Challenge

Maps are where timelines come to life. You can talk all day about Rome ,stretching from 509 BCE to 476 CE, but seeing it light up on a globe makes it real. Adding centroids is like putting pins on a giant corkboard of history—suddenly you’re not just reading about civilizations, you’re traveling to them.

Purpose of the Code (Object)

The code links civilizations to a pair of coordinates and plots them on a map. Each civilization becomes a marker you can hover over to see details. The magic lies in syncing the map with the timeline filters—change the years or regions, and both the chart and the map update in unison. It’s like history’s own GPS.

AI Prompt

Today, I would like to do the following:

  • Add centroid latitude/longitude for each civilization.
  • Plot them as markers with Plotly.
  • Accept: changing filters updates both the timeline and the map consistently; hover shows useful info.

Functions & Features

  • Add latitude and longitude columns to civilization records.
  • Plot civilizations on an interactive map with Plotly.
  • Hover over markers to reveal civilization details.
  • Sync map and timeline filters for a seamless view.

Requirements / Setup

You’ll need:

  • Python 3.11

Installs:

pip install plotly streamlit

Minimal Code Sample

fig = px.scatter_geo(

    data_frame=items,

    lat=”lat”,

    lon=”lon”,

    text=”name”

)

st.plotly_chart(fig, use_container_width=True)

Each civilization gets a marker on the map; hover reveals its name.

The Civilization Timeline Builder

Notes / Lessons Learned

So, what exactly went wrong? My CSV file was missing Sumer entirely, and instead of “Roman,” I had typed “Rome.” The app wasn’t confused—it was just being literal. Civilization slugs don’t do nicknames.

The funny part is that I was meticulously following the process, confident that everything was fine. It wasn’t until I noticed a strangely empty Mediterranean that I realized something was off. Double-checking with the API showed the slug as “roman,” which didn’t match my sloppy CSV entry.

In the end, nothing was wrong with the Python script at all—the bug was in my data. A good reminder that sometimes the problem isn’t your code, it’s the breadcrumbs you fed it. All told, it took about 1.75 hours, but now every civilization is exactly where it belongs.

Optional Ideas for Expansion

  • Add pop-up info windows with key facts (years, tags, a fun trivia line).
  • Layer in event markers so battles and inventions appear on the map too.
  • Color-code civilizations by region for instant visual grouping.

Adding a Function to Visualize History with Plotly Timeline Bars

Day 81 of 100 Days Coding Challenge: Python

Today’s mission was to finally put some clothes on our bare-bones timeline—specifically, horizontal bars that show civilizations stretching across the years like colorful ribbons. Picture this: you pick a range of years in the sidebar, and voilà, a neat bar chart unfurls, each line representing a civilization strutting across history.

Ever since kicking off this Civilization Timeline project, I’ve been living in a constant state of “overwhelmed but learning.” Every day brings a new tool, a new library, or a new way to break my code. But at its core, it’s still Python—the same familiar friend in a slightly fancier suit.

The tricky part? Each new feature has to be inserted into just the right place. Add it too early, and it vanishes; too late, and it breaks everything. It’s like threading a needle in the dark: satisfying when it works, frustrating when it doesn’t.

Today’s Motivation / Challenge

Why add bars? Because walls of text are fine for a history book, but if you really want to see civilizations rise and fall, you need visuals. Bars turn abstract dates into something tangible. Instead of saying, “The Han Dynasty lasted four centuries,” you can see its line stretching across the timeline, rubbing shoulders with Rome. It’s like putting history on a conveyor belt and watching empires roll past.

Purpose of the Code (Object)

The code adds timeline bands—horizontal bars that show how long each civilization lasted within the years you’ve selected. You pick a date range, and the app maps the civilizations that overlap with it. Hover over a bar, and you’ll see its start and end dates. It’s history turned into a chart you can actually play with.

AI Prompt

Please add the following function to the existing script:
Day 6 — Timeline bands

  • Horizontal bars per civ within selected range (Plotly).
  • Accept: bars scale correctly; hover shows dates & duration.

Functions & Features

  • Draw horizontal timeline bars for civilizations in the selected year range.
  • Hover tooltips show start and end dates plus duration.
  • Single-civilization band on the detail page for close-up analysis.

Requirements / Setup

You’ll need:

  • Python 3.11

Installs:

pip install plotly

Minimal Code Sample

fig = px.timeline(

    x_start=[c[“start_year”] for c in items],

    x_end=[c[“end_year”] for c in items],

    y=[c[“name”] for c in items]

)

fig.update_yaxes(autorange=”reversed”)

st.plotly_chart(fig, use_container_width=True)

Each bar stretches across its start and end years—Plotly handles the drawing magic.

The Civilization Timeline Builder

Notes / Lessons Learned

The day began with installing Plotly, which immediately felt like unlocking a new superpower: interactive graphs! At first, I only wanted to create multi-civilization bars on the main page, but then I thought, “Why stop there?” So I snuck in a single-civ band on the detail page, too. Now, whenever you zoom in on one civilization, you get its own mini-timeline. Totally extra, but worth it.

The big lesson? Inserting new code is like assembling IKEA furniture. Put the shelf in the wrong slot and suddenly the whole bookcase wobbles. Place the band code in just the right spot, and everything stands tall.

Optional Ideas for Expansion

  • Add different colors for event types (wars, tech, culture) along the bars.
  • Let users toggle between BCE/CE and “absolute” year numbers.
  • Allow comparison mode where two civilizations’ bands overlap on the same detail page.

Muscle Pain to Strong Run from Consistency and Small Wins

Brian’s fitness journal after a brain stroke

From Muscle Pain to Strong Run

This morning began… uncomfortably early.

I woke up needing to use the bathroom, but as I walked there, I realized my abdominal muscles had filed a formal complaint. They were so sore that walking in a straight line felt like an optional feature rather than a guarantee.

Recovery is not always straightforward for me. Because of my kidney condition, I cannot rely on high protein intake to support muscle repair. So when I push myself, soreness tends to linger longer than I would prefer.

Fortunately, rest remains a reliable strategy.

I went back to bed and fell asleep easily. When I woke again—just before my alarm—my muscles had improved noticeably. Not perfect, but functional. I had worried about my planking session, but surprisingly, it felt easier than the day before. Either recovery worked overnight, or my muscles decided to cooperate out of courtesy.

A few hours later, I headed out for my run.

The results were unexpectedly good. I reached my target pace and kept my split times consistent throughout. I set a steady rhythm early and managed to hold it to the end—a small but satisfying victory.

Days like this remind me how much my running has improved over the years.

In the beginning, it was difficult. I actually run more now than I did before my brain stroke. At first, I ran because my wife encouraged me. Exercise supports both my kidney health and brain recovery, and I wanted to show her that I was trying.

Ironically, telling her that would only make her sad. She prefers that I do these things for myself.

Over time, that shift happened naturally. Running stopped being something I did “for someone else” and became part of my life. Along the way, I added other exercises almost without thinking.

Consistency quietly turned into identity.

Meanwhile, at home, our cat continues her own version of restricted training. While I can run freely outside, she remains confined to my office for recovery. My wife briefly tried moving her to the bedroom, but the moment she gained access, she immediately planned a full return to her usual routine—kitchen exploration, counter patrol, and likely a trip up to the catwalk.

Given her enthusiasm, we decided that “freedom” might come a little too soon for her incision’s comfort.

So, back to the office she went.

Now I find myself wondering: after ten days of limited movement, will she experience muscle soreness too? Or will she simply resume full-speed chaos as if nothing ever happened?

Knowing her, I suspect the latter.

Adding Streamlit Civilization Timeline Filters to the App

Day 80 of 100 Days Coding Challenge: Python

At the start of this whole adventure, I felt like a kid stuck between two arcades: one machine labeled “Poetry” and the other “PowerShell.” Both terminals were blinking at me, demanding quarters, and I wasn’t sure which joystick to grab first. Overwhelming? Absolutely. Exciting? Even more so. Curiosity is a noisy creature—it drowned out the fear.

Fast-forward to today: we added Streamlit. Or at least, we were supposed to—except I discovered I had already installed it. (Past Me occasionally leaves gifts for Future Me.) With it, I cobbled together an interactive web app that shows a skeleton of civilizations. It’s not pretty, but it works, and sometimes “working” is the most beautiful sight of all.

I’d first heard of Streamlit while dabbling in machine learning tutorials. Back then, it was this shiny, mysterious framework that promised to turn Python scripts into apps with the ease of flipping a pancake. Today, I can actually use it—and maybe one day, I’ll build dashboards that model the universe of Asimov’s Foundation or Brandon Sanderson’s Cosmere. Ambitious? Yes. Possible? With enough coffee, absolutely.

Today’s Motivation / Challenge

Why does this matter? Because squinting at raw JSON is about as fun as watching paint dry. With Streamlit, I can slide, click, and filter through civilizations like I’m browsing an ancient version of Netflix. Want only South Asian civilizations between 500 BCE and 500 CE? Done. It’s history on demand, minus the endless commercials.

Purpose of the Code (Object)

The code turns a pile of historical data into a friendly web interface. Instead of hammering at APIs or database queries, you get sliders and dropdowns. Slide to pick a year range, click to pick a region or tag, and voilà—the app shows you only what you asked for. It’s history made interactive, without the headaches.

AI Prompt

Please create an instruction to do the Day 5 tasks. Today’s goal is:

Day 5 — Streamlit list & filters

  • Sidebar: year slider (−3000 to 2000), region multiselect, tag multiselect.
  • Main: civ cards; click → civ detail.

Accept: filters change visible list without errors.

Functions & Features

  • Sidebar with interactive filters: year range, regions, and tags.
  • Main panel shows civilization “cards.”
  • Clicking a card loads civilization details.
  • Filters instantly update the list—no reloads needed.

Requirements / Setup

You’ll need:

  • Python 3.11

Installs:

pip install streamlit requests plotly

Minimal Code Sample

with st.sidebar:

    year_range = st.slider(“Year range”, -3000, 2000, (-500, 500))

    regions = st.multiselect(“Regions”, [“Europe”, “East Asia”, “South Asia”])

    tags = st.multiselect(“Tags”, [“war”, “tech”, “culture”])

resp = requests.get(f”{base}/civs”, params={

    “start”: year_range[0], “end”: year_range[1], “region”: regions, “tags”: tags

})

items = resp.json()[“items”]

Sidebar collects filters, sends them to the API, and displays matching civilizations.

The Civilization Timeline Builder

Notes / Lessons Learned

Streamlit hides a sneaky detail: caching. This program uses st.cache_data for things like JSON and dataframes, while st.cache_resource babysits the heavy stuff (like database clients). The kicker? The order matters. Place your filters and routing before the main content, or you’ll spend hours wondering why your app feels like it’s dragging a boulder uphill.

The more I poked around, the more I realized: Streamlit isn’t just picky—it’s opinionated, like a good chef. At first, it annoyed me. But then it clicked. The structure is there to keep things smooth, fast, and uncluttered. Sometimes “why” matters more than “how.” And yes, that thought will haunt me until I build that Cosmere dashboard.

Optional Ideas for Expansion

  • Add a search box so you can type “Rome” instead of scrolling endlessly.
  • Make the civ cards prettier with images and little flag icons.
  • Add a “random civilization” button for the historically indecisive.

Creating Filterable FastAPI Endpoints for Civilizations and Events

Day 79 of 100 Days Coding Challenge: Python

For weeks now, I’ve been averaging two to three hours a day on this project—me, my laptop, and an endless parade of poetry commands. Today, however, I blinked, stretched, and realized my task was finished in just forty-five minutes. A miracle! Granted, I’m still fumbling with Poetry like it’s an unfamiliar dance partner, but at least I’ve learned that typing poetry run uvicorn app.main:api –reload is far superior to the old python something.py shuffle. Progress tastes sweet.

The mission of the day was to spin up read-only endpoints: /civs and /events, complete with filters for region, time period, and tags. My only real hiccup? Yesterday I had to reinstall Python 3.11, and AI didn’t quite catch that curveball. Starting a new session gave me déjà vu instructions—like being told twice to tie my shoes. So I stuck with yesterday’s session, which kept the advice on-track. Next, I’ll try this from a clean project folder to see if consistency still holds. Spoiler: consistency matters a lot more than I thought.

Today’s Motivation / Challenge

History is a mess of overlapping civilizations, wars, and inventions—but wouldn’t it be nice if you could filter it the way you filter Netflix shows? “Only East Asia, 200 BCE to 200 CE, with extra tags for technology, please.” That’s the vibe. Today’s endpoints turn a chaotic jumble into a menu you can actually navigate.

Purpose of the Code (Object)

The code opens up two doors into our database: one for civilizations, one for events. Instead of drowning you in everything at once, it politely asks, “Want to see just one region? Or only certain centuries? How about filtering by tags?” It’s a smarter way to explore history without scrolling through endless lists.

AI Prompt

Add the following functions:
Endpoints: /civs, /events, filters: region, year_range, tags.

Accept: Swagger lists routes; basic filters return sane counts.

Functions & Features

  • /civs: lists civilizations, filtered by region and year range.
  • /events: lists events, filtered by year, region, or tag.
  • Case-insensitive partial matches (so “Asia” catches “East Asia” and “South Asia”).
  • Bonus: endpoints /meta/tags and /meta/regions to preview valid options.

Requirements / Setup

You’ll need:

  • Python 3.11

A few installs:

pip install fastapi uvicorn sqlmodel

Minimal Code Sample

@api.get(“/civs”)

def list_civs(region: Optional[str] = None, start: int = -3000, end: int = 2000):

    data = session.query(Civilization).filter(

        Civilization.start_year <= end,

        Civilization.end_year >= start

    )

    if region:

        data = data.filter(Civilization.region.ilike(f”%{region}%”))

    return data.all()

This is the heart of the idea: query the database, filter by dates and region, then return the results.

The Civilization Timeline Builder

Notes / Lessons Learned

My biggest revelation today: don’t hop between sessions if you value your sanity. Switching can mean duplicated instructions, skipped steps, and missing files—a recipe for confusion.

Another surprise was Swagger’s stubbornness. Typing “Asia” for the region gave me nothing. Turns out the actual values were “East Asia” and “South Asia.” The fix? Using ilike(“%Asia%”), which basically lets the database play “guess what I meant.”

Finally, adding /meta/tags and /meta/regions was a lifesaver. Think of it as a cheat sheet so I don’t embarrass myself with typos.

Optional Ideas for Expansion

  • Add fuzzy matching so “Han” still finds “Han Dynasty.”
  • Return friendly error messages if someone types “Atlantis.”

FastAPI Database Seeding: Lessons From a Very Long Morning

Day 78 of 100 Days Coding Challenge: Python

Today was the first time this project nearly made me cry on a weekend. Silver lining: I had the weekend off. I went full detective, fixed each clue, and 2.5 hours later, the case was closed.

It started with my app pretending it had never heard of my “event” and “civilization” seeds. We tried to reason with it; the app replied with more errors. So I made the dramatic choice to roll back to Python 3.11. Did my blood run cold? Absolutely. I had nightmares about dependency dominoes falling over. I’ve been burned before—change Python and some random file screams in a different room.

Did I quit? Not today. I switched to 3.11, watched a few files wobble, and then fixed them, one by one. In the end, every model, seed, and endpoint reported for duty. No tears, just a very caffeinated victory lap.

Today’s Motivation / Challenge

Seeding a timeline of civilizations isn’t just “data entry with a cape.” It’s giving your app a memory. The database goes from goldfish to historian, and your API finally has something interesting to say. It’s like stocking a library before opening day.

Purpose of the Code (Object)

This code loads a starter set of civilizations and events into a database, then exposes simple read-only API endpoints to browse them. You can filter events by year range and civilization (by slug or name) and see each event alongside the civilization it belongs to. In short: seed, serve, search.

AI Prompt: Make it cleaner.

Functions & Features

  • Seed CSV files into the database (idempotent: re-run without duplicates).
  • /events endpoint returns events with the civilization name and slug.
  • Filters: start, end, civ (slug or name, case-insensitive), kind, limit.
  • /civs lists civilizations, with an optional region filter (and an easy name contains search).
  • Friendly API docs at /docs (FastAPI’s Swagger UI).

Requirements / Setup

  • Python 3.11
  • Poetry (recommended) or pip
  • Key packages: sqlmodel, sqlalchemy, pydantic, fastapi, uvicorn

Example with pip:

pip install “sqlmodel>=0.0.21” “sqlalchemy>=2.0,<2.1” “pydantic>=2,<3” fastapi “uvicorn[standard]”

Minimal Code Sample

# Return events with attached civilization info and filters

from sqlalchemy import and_, or_

from sqlalchemy.orm import selectinload

stmt = select(Event).options(selectinload(Event.civilization))

conds = []

if start is not None: conds.append(Event.year >= start)

if end is not None:   conds.append(Event.year <= end)

    stmt = stmt.join(Civilization)

    conds.append(or_(Civilization.slug == civ, Civilization.name.ilike(f”%{civ}%”)))

if conds: stmt = stmt.where(and_(*conds))

events = session.exec(stmt.order_by(Event.year).limit(limit)).all()

One query, optional joins, and eager loading to return civilized results.

The Civilization Timeline Builder

Notes / Lessons Learned

I learned the hard way that your editor and Poetry can quietly disagree about which interpreter you’re using. Always aim them both at the same Poetry venv before chasing ghosts. Rolling back Python isn’t a moral failure; it’s maintenance. Things did break, but fixing them systematically beat panicking artistically. The relationship typing error was just Python version meets SQLAlchemy expectations—once aligned, the models behaved. And yes, I triple-checked what I pushed to GitHub. The .gitignore did the heavy lifting, and a tiny .gitkeep kept the db/ folder tidy. Complicated project, simple rule: automate the boring parts and commit the safe parts.

Optional Ideas for Expansion

  • Pagination and sorting for /events (e.g., page and order params).
  • Simple front-end table at / that fetches from /events with filters.
  • Tag filtering (tags=trade,tech) and a tiny autocomplete for civilization names.

Tables, Not Tales: Building a Tiny Civilization in SQLite

Day 77 of 100 Days Coding Challenge: Python

Today I put down the party streamers and picked up a blueprint. The job: lay the foundation for my Civilization Timeline app. On the menu were four tables—Civilization, Event, Tag, and the many-to-many glue EventTag—plus Alembic migrations and a tiny pytest to make sure I didn’t accidentally build a pyramid upside down.

This project has more moving parts than my last one, so I treated each step like crossing a rope bridge: slow, steady, and testing every plank. I reminded myself that I don’t need to understand every microscopic detail today; I just need to know why each step matters. With AI as my trail guide (and occasional comic relief), I focused on purpose over perfection—and got to a green “tests passed” without falling into the canyon.

Today’s Motivation / Challenge

Databases are the memory of an app. If I want timelines, comparisons, and “who overlapped whom,” I first need a clean vocabulary of things and relationships. Today’s work turns ideas into durable structures—like labeling boxes before you stack them in the attic, so Future Me doesn’t cry later.

Purpose of the Code (Object)

The code defines four tables—civilizations, events, tags, and the event-tag join—using SQLModel. Alembic migrations snapshot this structure so I can version changes safely. A simple pytest spins up a temporary database, runs the migration, and confirms the tables exist. If the test passes, my schema is real—not just a wish.

AI Prompt

(And we also recorded the Day 2 acceptance line in the project script: “Day 2 — Data model v1: Tables: civilizations, events, tags, joins. Use SQLModel + Alembic. Accept: migration creates tables; pytest -k ‘migrations’ passes.”)

Functions & Features

  • Define core schema with SQLModel: Civilization, Event, Tag.
  • Model many-to-many relationships via EventTag.
  • Set up Alembic and generate an initial migration.
  • Run a pytest that upgrades a fresh DB and asserts the four tables exist.

Requirements / Setup

  • Python 3.11+ recommended

Install basics:

pip install sqlmodel sqlalchemy alembic pytest

Environment:

# SQLite file location

setx DATABASE_URL “sqlite:///db/civ.db”   # (PowerShell: $env:DATABASE_URL = …)

Minimal Code Sample

# app/models/core.py

from typing import Optional, List

from sqlmodel import SQLModel, Field, Relationship

class EventTag(SQLModel, table=True):        # link table first so it’s importable

    event_id: int = Field(primary_key=True, foreign_key=”event.id”)

    tag_id:   int = Field(primary_key=True, foreign_key=”tag.id”)

class Civilization(SQLModel, table=True):

    id: Optional[int] = Field(default=None, primary_key=True)

    name: str; region: str

    start_year: int; end_year: Optional[int] = None

    events: List[“Event”] = Relationship(back_populates=”civilization”)

class Event(SQLModel, table=True):

    id: Optional[int] = Field(default=None, primary_key=True)

    civilization_id: int = Field(foreign_key=”civilization.id”)

    title: str; year: int

    civilization: Optional[Civilization] = Relationship(back_populates=”events”)

    tags: List[“Tag”] = Relationship(back_populates=”events”, link_model=EventTag)

class Tag(SQLModel, table=True):

    id: Optional[int] = Field(default=None, primary_key=True)

    name: str

    events: List[Event] = Relationship(back_populates=”tags”, link_model=EventTag)

Define the link table first; then reference it with link_model=EventTag for a clean many-to-many.

The Civilization Timeline Builder

Notes / Lessons Learned

  • I first tried adding __tablename__ like in raw SQLAlchemy. SQLModel didn’t appreciate that. Dropping __tablename__ and using table=True made everything click. Foreign keys then target the default singular table names (“civilization.id”, “event.id”, “tag.id”), which is tidy and predictable.
  • AI is a fantastic copilot—as long as I stay the pilot. When in doubt, I asked for small, verifiable steps and sanity checks. Explaining my error messages clearly got me faster, better fixes.
  • Alembic initialization matters. The one-liner alembic init alembic builds the right scaffolding. File structure isn’t ceremony—it’s how tools find each other.
  • A tiny gotcha: Alembic autogenerate used SQLModel’s AutoString in the revision, but the file only imported SQLAlchemy. Adding import sqlmodel at the top (or swapping AutoString for sa.String) fixed it. Another tiny snag: from __future__ import annotations must be first or not at all in migration files—easiest solution was to remove it.
  • Pytest wasn’t installed at first; the error reminded me. After installing, pytest -k “migrations” passed. The green check felt like a gold star sticker from elementary school—strangely motivating.

Optional Ideas for Expansion

  • Add seed scripts and CSVs (e.g., 10–12 major civilizations with a few events each) so the UI can show real bands tomorrow.
  • Introduce an enum (or controlled vocabulary) for event kinds and a couple of helpful indexes for faster filtering.
  • Write a second test that inserts a sample event with two tags and asserts the join table behaves as expected.

A New Project: Starting a Civilization Timeline Builder

Day 76 of 100 Days Coding Challenge: Python

Civilization Time Builder (Day 76 – Day 96)

When AI first suggested this project, I hesitated. The generated project plan was full of terms I barely understood. Take “Poetry,” for example. At first, it sounded like something Shakespeare might dabble in—but in reality, it’s a tool for managing Python dependencies. Once I looked it up and understood what it did, I decided to give it a try.

Environment management had been a thorn in my side during the Stock Tracker App Project. A single update to Python or one of its libraries could break everything I had built. I had already watched my programs collapse because of an accidental update. It was clear: if I wanted to build something more complex, I needed to learn a tool like Poetry. 

The theme of this project also pulled me in. It was all about history—a subject I’ve loved since childhood. I can still remember sitting in my parents’ library, leafing through their history books for hours. In that sense, this project felt tailor-made for me.

What excites me most is how expandable this program can be. I could easily adapt it into something larger, like mapping the timelines of Asimov’s Foundation series, Sanderson’s Stormlight Archive, or other Cosmere-related books.

That said, this was one of the toughest challenges I’ve faced so far. Some days I spent five or six hours just trying to fix problems. At one point, I almost quit altogether. But I kept going. Looking back, I’m glad I didn’t give up. Yes, I made plenty of mistakes—but each one taught me something valuable. If nothing else, I learned the importance of patience—with my code and with myself.

Experience

Today marked Day 1 of my shiny new project: the Civilization Timeline Builder. Finally, a project that ties together two of my favorite things—coding and history. The idea is to build an app that makes exploring history feel a little less like flipping through a dusty textbook and a little more like discovering buried treasure.

This project feels bigger than anything I’ve tackled before. Bigger data, bigger scope, bigger headaches. To avoid drowning in dependency drama, I brought in Poetry, my new virtual assistant. Poetry takes charge of libraries, versions, and packaging so I don’t end up in dependency jail. Think of it as a backstage manager who makes sure the actors (libraries) show up on time and don’t fight with each other.

The process, however, wasn’t like my earlier, simpler projects where I just spun up a virtual environment and carried on. Right away, I could sense something different. I usually code at an unholy hour of the morning, fueled by post-exercise clarity, but even then I had to stop and double-check every step. I even started a day early just to reread the instructions, as if I were reviewing the rules of an unfamiliar board game before play.

Today’s Motivation / Challenge

Why bother with all this fuss? Because Poetry is the difference between a messy desk covered in loose papers and a neat binder where everything is tabbed, labeled, and ready to go. Dependency management sounds boring—until it breaks. Then it’s all you think about. This project matters because it forces me to upgrade my tools and habits, making me a more organized (and hopefully less panicked) developer.

Purpose of the Code (Object)

The code for today doesn’t build empires or conquer continents just yet. Instead, it sets up a tiny FastAPI server with a health check and a stub of civilization data. It’s the scaffolding—the skeleton—upon which everything else will hang. Think of it as the tent poles before the circus begins.

Functions & Features

  • Start a FastAPI server with a health check endpoint.
  • Return a couple of sample civilizations from in-memory data.
  • Filter civilizations by region and year range.

Requirements / Setup

  • Python 3.13
  • Poetry (dependency management)
  • FastAPI, Streamlit, Uvicorn

Install Poetry’s deps via:

poetry install

Run the API:

poetry run uvicorn app.main:api –reload

Minimal Code Sample

from fastapi import FastAPI, Query

from typing import List, Optional

api = FastAPI(title=”CivTimeline API”)

@api.get(“/health”)

def health():

    return {“ok”: True}

CIVS = [

    {“id”: 1, “name”: “Roman Republic/Empire”, “region”: “Europe”, “start_year”: -509, “end_year”: 476},

    {“id”: 2, “name”: “Han Dynasty”, “region”: “East Asia”, “start_year”: -206, “end_year”: 220},

]

@api.get(“/civs”)

def list_civs(region: Optional[List[str]] = Query(None), start: int = -3000, end: int = 2000):

    # Filter civs by year and region

    return {“items”: [c for c in CIVS if c[“start_year”] <= end and c[“end_year”] >= start 

                      and (not region or c[“region”] in region)]}

A small API stub with civilizations you can query.

The Civilization Timeline Builder

Notes / Lessons Learned

This project began with a repo scaffold: Poetry + FastAPI + Streamlit. It quickly proved to be a different animal than the smaller projects I’d done before. Installing Poetry felt like watching paint dry—it took so long I thought I had failed. But once I checked, it was there, quietly waiting for me to notice.

Next came the .env and .env.example files, along with .gitignore. These guardrails saved me from committing private config files to git. I learned this lesson the hard way in a previous project, where I nearly exposed secrets before catching myself. A README was also added, though I’ll admit I skipped it at first and paid the price in confusion later.

Booting up the FastAPI and Streamlit stubs worked, but the process felt chaotic. Nothing was “normal” compared to my earlier projects. To cope, I broke the steps into tiny pieces and forced myself to ask “why” at each step. The trick is not to stay in “why mode” too long—you’ll skip something important. Next time, I’m making a checklist before indulging my curiosity.

Optional Ideas for Expansion

  • Add a proper database instead of temporary in-memory data.
  • Build a simple UI filter in Streamlit for regions and years.
  • Visualize civilizations on a timeline chart.

Build a Portfolio vs. Benchmark Chart in Python

Day 75 of 100 Days Coding Challenge: Python

Today marked the final lap of my stock tracker project. To finish strong, I added one last function: checking my portfolio against the S&P 500. Think of it like racing your homemade go-kart against a Formula 1 car—suddenly, you know where you actually stand.

Most people use the ticker ^GSPC as their yardstick for the S&P 500. It’s a tidy snapshot of large-cap U.S. stocks. But here’s the trick: it only makes sense to compare apples to apples. If your portfolio is tilted toward bonds, emerging markets, or your cousin’s hot crypto tip, the S&P might not be your best match. Personally, I balance stocks and bonds between 70/30 and 50/50, depending on the season (and, frankly, my mood).

If your portfolio consists solely of ETFs, you could use SPY (an ETF that tracks the S&P 500). Want to check your bonds? Try a bond index ticker like AGG. The idea is simple: know what game you’re playing before you brag about winning.

Today’s Motivation / Challenge

Why bother with this comparison? Flying blind in investing is like bragging about your marathon time without realizing you ran half the distance. Benchmarking grounds you. It tells you whether your “brilliant strategy” is actually brilliant—or if you’d have been better off just buying the index and going fishing.

Purpose of the Code (Object)

This function aligns your portfolio’s performance with a benchmark (such as the S&P 500) from a date you choose. It then tells you how much you gained, what your growth rate looks like over time, and how painful your worst dip was. Finally, it plots a simple graph so you can see who’s winning: you or the index.

AI Prompt

Add to show the portfolio’s performance with a benchmark.

Functions & Features

  • Compare your portfolio against a benchmark (default: S&P 500).
  • Print stats: total return, growth rate, and max drawdown.
  • Plot both curves on a chart starting at the same baseline.
  • Flexible benchmark (SPY, AGG, or any valid ticker).

Requirements / Setup

  • Python 3.10+

Install dependencies:

pip install yfinance matplotlib pandas

Minimal Code Sample

# Compare portfolio vs benchmark

port_ret = portfolio / portfolio.iloc[0] – 1

bench_ret = benchmark / benchmark.iloc[0] – 1

plt.plot(port_ret.index, (1 + port_ret) * 100, label=”Portfolio”)

plt.plot(bench_ret.index, (1 + bench_ret) * 100, label=”Benchmark”)

plt.legend()

plt.show()

Both lines start at 100, so you can see the race clearly.

Stock Tracker

Notes / Lessons Learned

This project was an absolute joy. Finance has fascinated me ever since my university days, when I was the nerd reading investment books for fun. Writing this code felt like merging that old passion with my new one: programming. There’s a strange comfort in watching your ideas become actual functions—little digital tools that reflect your understanding of the markets. I’m not a professional investor, but I can proudly say this project carries bits of my life’s learning stitched into its code.

Optional Ideas for Expansion

  • Add dividends into the calculation for a more realistic comparison.
  • Try benchmarking against multiple indexes at once (stocks, bonds, international).