Files
personal-portfolio/docs/CONTRIBUTING.md
lmiranda 263b52d5e4
Some checks failed
CI / lint-and-test (push) Has been cancelled
docs: sync documentation with codebase
Fixes identified by doc-guardian audit:

Critical fixes:
- DATABASE_SCHEMA.md: Fix staging model name stg_police__crimes → stg_toronto__crime
- DATABASE_SCHEMA.md: Update mart model names to match actual dbt models
- CLAUDE.md: Fix errors/ description (no handlers module exists)
- scripts/etl/toronto.sh: Fix parser module references to actual modules

Stale fixes:
- CONTRIBUTING.md: Add make typecheck, test-cov; fix make ci description
- PROJECT_REFERENCE.md: Document services/, callback modules, all Makefile targets
- CLAUDE.md: Expand Makefile commands, add plugin documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 16:25:29 -05:00

9.7 KiB

Developer Guide

Instructions for contributing to the Analytics Portfolio project.


Table of Contents

  1. Development Setup
  2. Adding a Blog Post
  3. Adding a New Page
  4. Adding a Dashboard Tab
  5. Creating Figure Factories
  6. Branch Workflow
  7. Code Standards

Development Setup

Prerequisites

  • Python 3.11+ (via pyenv)
  • Docker and Docker Compose
  • Git

Initial Setup

# Clone repository
git clone https://gitea.hotserv.cloud/lmiranda/personal-portfolio.git
cd personal-portfolio

# Run setup (creates venv, installs deps, copies .env.example)
make setup

# Start PostgreSQL + PostGIS
make docker-up

# Initialize database
make db-init

# Start development server
make run

The app runs at http://localhost:8050.

Useful Commands

make test       # Run tests
make test-cov   # Run tests with coverage
make lint       # Check code style
make format     # Auto-format code
make typecheck  # Run mypy type checker
make ci         # Run all checks (lint, typecheck, test)
make dbt-run    # Run dbt transformations
make dbt-test   # Run dbt tests

Adding a Blog Post

Blog posts are Markdown files with YAML frontmatter, stored in portfolio_app/content/blog/.

Step 1: Create the Markdown File

Create a new file in portfolio_app/content/blog/:

touch portfolio_app/content/blog/your-article-slug.md

The filename becomes the URL slug: /blog/your-article-slug

Step 2: Add Frontmatter

Every blog post requires YAML frontmatter at the top:

---
title: "Your Article Title"
date: "2026-01-17"
description: "A brief description for the article card (1-2 sentences)"
tags:
  - data-engineering
  - python
  - lessons-learned
status: published
---

Your article content starts here...

Required fields:

Field Description
title Article title (displayed on cards and page)
date Publication date in YYYY-MM-DD format
description Short summary for article listing cards
tags List of tags (displayed as badges)
status published or draft (drafts are hidden from listing)

Step 3: Write Content

Use standard Markdown:

## Section Heading

Regular paragraph text.

### Subsection

- Bullet points
- Another point

```python
# Code blocks with syntax highlighting
def example():
    return "Hello"

Bold text and italic text.

Blockquotes for callouts


### Step 4: Test Locally

```bash
make run

Visit http://localhost:8050/blog to see the article listing. Visit http://localhost:8050/blog/your-article-slug for the full article.

Example: Complete Blog Post

---
title: "Building ETL Pipelines with Python"
date: "2026-01-17"
description: "Lessons from building production data pipelines at scale"
tags:
  - python
  - etl
  - data-engineering
status: published
---

When I started building data pipelines, I made every mistake possible...

## The Problem

Most tutorials show toy examples. Real pipelines are different.

### Error Handling

```python
def safe_transform(df: pd.DataFrame) -> pd.DataFrame:
    try:
        return df.apply(transform_row, axis=1)
    except ValueError as e:
        logger.error(f"Transform failed: {e}")
        raise

Conclusion

Ship something that works, then iterate.


---

## Adding a New Page

Pages use Dash's automatic routing based on file location in `portfolio_app/pages/`.

### Step 1: Create the Page File

```bash
touch portfolio_app/pages/your_page.py

Step 2: Register the Page

Every page must call dash.register_page():

"""Your page description."""

import dash
import dash_mantine_components as dmc

dash.register_page(
    __name__,
    path="/your-page",      # URL path
    name="Your Page",       # Display name (for nav)
    title="Your Page Title" # Browser tab title
)


def layout() -> dmc.Container:
    """Page layout function."""
    return dmc.Container(
        dmc.Stack(
            [
                dmc.Title("Your Page", order=1),
                dmc.Text("Page content here."),
            ],
            gap="lg",
        ),
        size="md",
        py="xl",
    )

Step 3: Page with Dynamic Content

For pages with URL parameters:

# pages/blog/article.py
dash.register_page(
    __name__,
    path_template="/blog/<slug>",  # Dynamic parameter
    name="Article",
)


def layout(slug: str = "") -> dmc.Container:
    """Layout receives URL parameters as arguments."""
    article = get_article(slug)
    if not article:
        return dmc.Text("Article not found")

    return dmc.Container(
        dmc.Title(article["meta"]["title"]),
        # ...
    )

Step 4: Add Navigation (Optional)

To add the page to the sidebar, edit portfolio_app/components/sidebar.py:

# For main pages (Home, About, Blog, etc.)
NAV_ITEMS_MAIN = [
    {"path": "/", "icon": "tabler:home", "label": "Home"},
    {"path": "/your-page", "icon": "tabler:star", "label": "Your Page"},
    # ...
]

# For project/dashboard pages
NAV_ITEMS_PROJECTS = [
    {"path": "/projects", "icon": "tabler:folder", "label": "Projects"},
    {"path": "/your-dashboard", "icon": "tabler:chart-bar", "label": "Your Dashboard"},
    # ...
]

The sidebar uses icon buttons with tooltips. Each item needs path, icon (Tabler icon name), and label (tooltip text).

URL Routing Summary

File Location URL
pages/home.py / (if path="/")
pages/about.py /about
pages/blog/index.py /blog
pages/blog/article.py /blog/<slug>
pages/toronto/dashboard.py /toronto

Adding a Dashboard Tab

Dashboard tabs are in portfolio_app/pages/toronto/tabs/.

Step 1: Create Tab Layout

# pages/toronto/tabs/your_tab.py
"""Your tab description."""

import dash_mantine_components as dmc

from portfolio_app.figures.choropleth import create_choropleth
from portfolio_app.toronto.demo_data import get_demo_data


def create_your_tab_layout() -> dmc.Stack:
    """Create the tab layout."""
    data = get_demo_data()

    return dmc.Stack(
        [
            dmc.Grid(
                [
                    dmc.GridCol(
                        # Map on left
                        create_choropleth(data, "your_metric"),
                        span=8,
                    ),
                    dmc.GridCol(
                        # KPI cards on right
                        create_kpi_cards(data),
                        span=4,
                    ),
                ],
            ),
            # Charts below
            create_supporting_charts(data),
        ],
        gap="lg",
    )

Step 2: Register in Dashboard

Edit pages/toronto/dashboard.py to add the tab:

from portfolio_app.pages.toronto.tabs.your_tab import create_your_tab_layout

# In the tabs list:
dmc.TabsTab("Your Tab", value="your-tab"),

# In the panels:
dmc.TabsPanel(create_your_tab_layout(), value="your-tab"),

Creating Figure Factories

Figure factories are in portfolio_app/figures/. They create reusable Plotly figures.

Pattern

# figures/your_chart.py
"""Your chart type factory."""

import plotly.express as px
import plotly.graph_objects as go
import pandas as pd


def create_your_chart(
    df: pd.DataFrame,
    x_col: str,
    y_col: str,
    title: str = "",
) -> go.Figure:
    """Create a your_chart figure.

    Args:
        df: DataFrame with data.
        x_col: Column for x-axis.
        y_col: Column for y-axis.
        title: Optional chart title.

    Returns:
        Configured Plotly figure.
    """
    fig = px.bar(df, x=x_col, y=y_col, title=title)

    fig.update_layout(
        template="plotly_white",
        margin=dict(l=40, r=40, t=40, b=40),
    )

    return fig

Export from __init__.py

# figures/__init__.py
from .your_chart import create_your_chart

__all__ = [
    "create_your_chart",
    # ...
]

Branch Workflow

main (production)
  ↑
staging (pre-production)
  ↑
development (integration)
  ↑
feature/XX-description (your work)

Creating a Feature Branch

# Start from development
git checkout development
git pull origin development

# Create feature branch
git checkout -b feature/10-add-new-page

# Work, commit, push
git add .
git commit -m "feat: Add new page"
git push -u origin feature/10-add-new-page

Merging

# Merge into development
git checkout development
git merge feature/10-add-new-page
git push origin development

# Delete feature branch
git branch -d feature/10-add-new-page
git push origin --delete feature/10-add-new-page

Rules:

  • Never commit directly to main or staging
  • Never delete development
  • Feature branches are temporary

Code Standards

Type Hints

Use Python 3.10+ style:

def process(items: list[str], config: dict[str, int] | None = None) -> bool:
    ...

Imports

Context Style
Same directory from .module import X
Sibling directory from ..schemas.model import Y
External packages import pandas as pd

Formatting

make format  # Runs ruff formatter
make lint    # Checks style

Docstrings

Google style, only for non-obvious functions:

def calculate_score(values: list[float], weights: list[float]) -> float:
    """Calculate weighted score.

    Args:
        values: Raw metric values.
        weights: Weight for each metric.

    Returns:
        Weighted average score.
    """
    ...

Questions?

Check CLAUDE.md for AI assistant context and architectural decisions.