# Developer Guide Instructions for contributing to the Analytics Portfolio project. --- ## Table of Contents 1. [Development Setup](#development-setup) 2. [Adding a Blog Post](#adding-a-blog-post) 3. [Adding a New Page](#adding-a-new-page) 4. [Adding a Dashboard Tab](#adding-a-dashboard-tab) 5. [Creating Figure Factories](#creating-figure-factories) 6. [Branch Workflow](#branch-workflow) 7. [Code Standards](#code-standards) --- ## Development Setup ### Prerequisites - Python 3.11+ (via pyenv) - Docker and Docker Compose - Git ### Initial Setup ```bash # 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 ```bash make test # Run tests make lint # Check code style make format # Auto-format code make ci # Run all checks (lint + 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/`: ```bash 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: ```markdown --- 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: ```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 ```markdown --- 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()`: ```python """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: ```python # pages/blog/article.py dash.register_page( __name__, path_template="/blog/", # 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`: ```python NAV_ITEMS = [ {"label": "Home", "href": "/", "icon": "tabler:home"}, {"label": "Your Page", "href": "/your-page", "icon": "tabler:star"}, # ... ] ``` ### 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/` | | `pages/toronto/dashboard.py` | `/toronto` | --- ## Adding a Dashboard Tab Dashboard tabs are in `portfolio_app/pages/toronto/tabs/`. ### Step 1: Create Tab Layout ```python # 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: ```python 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 ```python # 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` ```python # 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 ```bash # 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 ```bash # 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: ```python 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 ```bash make format # Runs ruff formatter make lint # Checks style ``` ### Docstrings Google style, only for non-obvious functions: ```python 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.