GitHub Activity Receipt Generator

Turn your commits into a thermal-paper style receipt

Welcome to another official UVF IT build guide. Today we will construct a minimal clone of ReceiptHub, transforming raw GitHub API data into a nostalgic, thermal-paper style receipt.

This project demonstrates how to fetch public user data, aggregate statistics, and render them in a constrained, retro UI using a simple Python stack. No complex databases or paid APIs are required.

Prerequisites & Limitations

System Architecture
LOCAL RUNTIME EXTERNAL SERVICES GET /users/{username}/events Store/Retrieve Cache Flask App Python/HTML SQLite User Cache GitHub API REST Endpoint
1

Phase 1: Scaffold & Dependencies

Create a project directory and initialize a virtual environment to isolate dependencies. This ensures your system Python remains clean while you experiment with Flask.

Install Flask for the web server and Requests for API communication. These are the only two external libraries we need for this minimal clone.

mkdir github-receipt-clone
cd github-receipt-clone
python3 -m venv venv
source venv/bin/activate
pip install flask requests
2

Phase 2: Core Data Model & API Fetcher

Create a Python script that connects to the GitHub API to fetch public events for a given username. We will use the requests library to handle the HTTP GET request securely.

Parse the JSON response to extract commits, PRs, issues, and stars. We'll store this in a simple dictionary structure that our template can easily iterate over.

app.py

import os
import requests
from flask import Flask

app = Flask(__name__)

# Helper function to fetch and parse GitHub data
def fetch_github_stats(username):
    token = os.environ.get('GITHUB_TOKEN')
    if not token:
        return {'error': 'GITHUB_TOKEN environment variable not set'}
    
    url = f'https://api.github.com/users/{username}/events/public'
    headers = {'Authorization': f'token {token}'}
    
    try:
        response = requests.get(url, headers=headers)
        response.raise_for_status()
        events = response.json()
        
        # Initialize counters
        stats = {
            'commits': 0,
            'prs': 0,
            'issues': 0,
            'stars': 0
        }
        
        # Process last 30 days of events (approx 300 events max from API)
        for event in events:
            type_ = event['type']
            if type_ == 'PushEvent':
                stats['commits'] += len(event['payload']['commits'])
            elif type_ == 'PullRequestEvent':
                stats['prs'] += 1
            elif type_ == 'IssuesEvent':
                stats['issues'] += 1
            elif type_ == 'WatchEvent':
                stats['stars'] += 1
                
        return {'username': username, 'stats': stats}
    except requests.exceptions.HTTPError as e:
        if response.status_code == 404:
            return {'error': 'User not found'}
        return {'error': str(e)}
    except Exception as e:
        return {'error': str(e)}
3

Phase 3: API/UI Integration

Build the Flask routes: one for the input form and one to display the receipt. The root route will handle GET requests to show the form and POST requests to process the username.

Create a simple HTML template that styles the output to look like a thermal receipt. We'll use monospace fonts, narrow widths, and dashed lines to achieve the retro aesthetic.

app.py

from flask import render_template, request, redirect, url_for

# ... [Previous code from Phase 2] ...

@app.route('/', methods=['GET', 'POST'])
def index():
    if request.method == 'POST':
        username = request.form.get('username')
        if not username:
            return redirect(url_for('index'))
        
        data = fetch_github_stats(username)
        return render_template('receipt.html', data=data)
    
    return render_template('index.html')

if __name__ == '__main__':
    app.run(debug=True)

templates/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>GitHub Receipt Generator</title>
    <style>
        body {
            font-family: 'Courier New', Courier, monospace;
            background-color: #f4f4f4;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
        }
        .container {
            background: white;
            padding: 2rem;
            border: 1px solid #ddd;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        form {
            display: flex;
            flex-direction: column;
            gap: 1rem;
        }
        input {
            padding: 0.5rem;
            font-family: inherit;
            border: 1px solid #ccc;
        }
        button {
            padding: 0.5rem;
            background: #333;
            color: white;
            border: none;
            cursor: pointer;
            font-family: inherit;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>GitHub Receipt</h1>
        <form action="/" method="POST">
            <input type="text" name="username" placeholder="Enter GitHub username" required>
            <button type="submit">Generate Receipt</button>
        </form>
    </div>
</body>
</html>

templates/receipt.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>GitHub Receipt</title>
    <style>
        body {
            font-family: 'Courier New', Courier, monospace;
            background-color: #f4f4f4;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
        }
        .receipt {
            background: white;
            padding: 2rem;
            width: 300px;
            border: 1px solid #ddd;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            position: relative;
        }
        .receipt::after {
            content: '';
            position: absolute;
            bottom: -10px;
            left: 0;
            width: 100%;
            height: 10px;
            background: linear-gradient(45deg, transparent 33.333%, white 33.333%, white 66.667%, transparent 66.667%),
                        linear-gradient(-45deg, transparent 33.333%, white 33.333%, white 66.667%, transparent 66.667%);
            background-size: 10px 20px;
        }
        h1 {
            text-align: center;
            font-size: 1.2rem;
            margin-bottom: 1rem;
            border-bottom: 1px dashed #000;
            padding-bottom: 0.5rem;
        }
        .item {
            display: flex;
            justify-content: space-between;
            margin-bottom: 0.5rem;
        }
        .total {
            margin-top: 1rem;
            border-top: 1px dashed #000;
            padding-top: 0.5rem;
            font-weight: bold;
        }
        .error {
            color: red;
            text-align: center;
        }
        .back-link {
            display: block;
            text-align: center;
            margin-top: 1rem;
            text-decoration: none;
            color: #333;
        }
    </style>
</head>
<body>
    <div class="receipt">
        <h1>GitHub Activity</h1>
        {% if data.error %}
            <p class="error">{{ data.error }}</p>
        {% else %}
            <p class="item"><span>User:</span> <span>{{ data.username }}</span></p>
            <div class="item"><span>Commits:</span> <span>{{ data.stats.commits }}</span></div>
            <div class="item"><span>PRs:</span> <span>{{ data.stats.prs }}</span></div>
            <div class="item"><span>Issues:</span> <span>{{ data.stats.issues }}</span></div>
            <div class="item"><span>Stars:</span> <span>{{ data.stats.stars }}</span></div>
            <div class="total">
                <div class="item"><span>Total Events:</span> <span>{{ data.stats.commits + data.stats.prs + data.stats.issues + data.stats.stars }}</span></div>
            </div>
        {% endif %}
        <a href="/" class="back-link">Back to Home</a>
    </div>
</body>
</html>
4

Phase 4: Run Locally

Before starting the server, you must secure your GitHub token. Never hardcode secrets in your source files; instead, export them as environment variables so the application can access them safely at runtime.

Start the Flask development server using the standard command. This will launch a local web server on port 5000, allowing you to interact with your receipt generator in the browser.

export GITHUB_TOKEN="your_personal_access_token_here"
python app.py
5

Phase 5: Verify and Extend

Open your web browser and navigate to http://localhost:5000. You should see the input form where you can enter any public GitHub username to generate their activity receipt.

Submit a well-known username like 'torvalds' to verify that the API fetches data correctly and the thermal receipt renders with accurate commit and PR counts.

To extend the project, you could add a calculation for 'Contribution Streaks' by analyzing the dates of the fetched events, or add a 'Print' button that triggers the browser's print dialog with CSS media queries optimized for thermal paper width.

curl http://localhost:5000

Verifying the Stack

CheckCommand / Why it matters
Flask server starts without errorspython app.py
Input form renders at localhost:5000Open browser to http://localhost:5000
Receipt displays non-zero stats for active usersSubmit 'torvalds' and check HTML output
Invalid username shows error messageSubmit 'nonexistent_user_123' and verify error handling

Next Steps

To extend this clone, consider adding a SQLite database to cache results and reduce API calls, or implement a 'Print' button using window.print() with specific CSS media queries for paper size.