Skip to content

Commit a4fed7d

Browse files
committed
demo: more demos
1 parent 561dbd0 commit a4fed7d

File tree

16 files changed

+1274
-0
lines changed

16 files changed

+1274
-0
lines changed

demos/demo29/app/app.py

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# Copyright (c) 2025, Abilian SAS
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Demo 29: Native Python app with MySQL addon - Page Counter.
4+
5+
A simple Flask application deployed natively (without Docker) that connects
6+
to a MySQL database provisioned via Hop3 addons. Implements a basic page view counter.
7+
"""
8+
from __future__ import annotations
9+
10+
import os
11+
from datetime import datetime
12+
13+
from flask import Flask, jsonify
14+
15+
app = Flask(__name__)
16+
17+
# Get DATABASE_URL from environment (set by Hop3 when addon is attached)
18+
# MySQL addon provides: DATABASE_URL, MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE
19+
DATABASE_URL = os.environ.get("DATABASE_URL")
20+
21+
22+
def get_db_connection():
23+
"""Get a database connection if DATABASE_URL is configured."""
24+
if not DATABASE_URL:
25+
return None
26+
27+
import mysql.connector
28+
29+
# Use individual env vars for more reliability
30+
return mysql.connector.connect(
31+
host=os.environ.get("MYSQL_HOST", "localhost"),
32+
port=int(os.environ.get("MYSQL_PORT", "3306")),
33+
user=os.environ.get("MYSQL_USER"),
34+
password=os.environ.get("MYSQL_PASSWORD"),
35+
database=os.environ.get("MYSQL_DATABASE"),
36+
)
37+
38+
39+
def init_db():
40+
"""Initialize the database table if it doesn't exist."""
41+
conn = get_db_connection()
42+
if not conn:
43+
return False
44+
45+
try:
46+
cursor = conn.cursor()
47+
cursor.execute("""
48+
CREATE TABLE IF NOT EXISTS page_views (
49+
id INT AUTO_INCREMENT PRIMARY KEY,
50+
page VARCHAR(100) NOT NULL,
51+
view_count INT DEFAULT 0,
52+
last_viewed TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
53+
UNIQUE KEY unique_page (page)
54+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
55+
""")
56+
conn.commit()
57+
cursor.close()
58+
conn.close()
59+
return True
60+
except Exception:
61+
return False
62+
63+
64+
def increment_counter(page: str = "home") -> int:
65+
"""Increment the page view counter and return the new count."""
66+
conn = get_db_connection()
67+
if not conn:
68+
return -1
69+
70+
try:
71+
cursor = conn.cursor()
72+
# Insert or update the counter
73+
cursor.execute("""
74+
INSERT INTO page_views (page, view_count)
75+
VALUES (%s, 1)
76+
ON DUPLICATE KEY UPDATE view_count = view_count + 1
77+
""", (page,))
78+
conn.commit()
79+
80+
# Get the current count
81+
cursor.execute("SELECT view_count FROM page_views WHERE page = %s", (page,))
82+
result = cursor.fetchone()
83+
count = result[0] if result else 0
84+
85+
cursor.close()
86+
conn.close()
87+
return count
88+
except Exception:
89+
return -1
90+
91+
92+
@app.route("/")
93+
def home():
94+
"""Home page with page counter."""
95+
db_configured = DATABASE_URL is not None
96+
97+
if db_configured:
98+
count = increment_counter("home")
99+
if count == -1:
100+
return jsonify({
101+
"app": "demo29",
102+
"type": "native + mysql",
103+
"message": "Welcome to demo29 - Native MySQL Page Counter!",
104+
"database_configured": True,
105+
"error": "Failed to increment counter",
106+
})
107+
return jsonify({
108+
"app": "demo29",
109+
"type": "native + mysql",
110+
"message": "Welcome to demo29 - Native MySQL Page Counter!",
111+
"database_configured": True,
112+
"page_views": count,
113+
"timestamp": datetime.now().isoformat(),
114+
})
115+
116+
return jsonify({
117+
"app": "demo29",
118+
"type": "native + mysql",
119+
"message": "Welcome to demo29 - Native MySQL Page Counter!",
120+
"database_configured": False,
121+
"hint": "Attach a MySQL addon to enable the counter",
122+
})
123+
124+
125+
@app.route("/db-status")
126+
def db_status():
127+
"""Check database connection status."""
128+
if not DATABASE_URL:
129+
return jsonify({"status": "not_configured", "message": "DATABASE_URL not set"})
130+
131+
try:
132+
conn = get_db_connection()
133+
cursor = conn.cursor()
134+
cursor.execute("SELECT VERSION()")
135+
version = cursor.fetchone()[0]
136+
cursor.close()
137+
conn.close()
138+
return jsonify({
139+
"status": "connected",
140+
"version": version,
141+
"database": os.environ.get("MYSQL_DATABASE"),
142+
})
143+
except Exception as e:
144+
return jsonify({"status": "error", "error": str(e)}), 500
145+
146+
147+
@app.route("/db-init")
148+
def db_init():
149+
"""Initialize the database table."""
150+
if not DATABASE_URL:
151+
return jsonify({"status": "not_configured"}), 400
152+
153+
success = init_db()
154+
if success:
155+
return jsonify({"status": "success", "message": "Database initialized"})
156+
return jsonify({"status": "error", "message": "Failed to initialize"}), 500
157+
158+
159+
@app.route("/counter")
160+
def counter():
161+
"""Get the current page view count without incrementing."""
162+
if not DATABASE_URL:
163+
return jsonify({"status": "not_configured"}), 400
164+
165+
try:
166+
conn = get_db_connection()
167+
cursor = conn.cursor()
168+
cursor.execute("SELECT page, view_count, last_viewed FROM page_views ORDER BY view_count DESC")
169+
rows = cursor.fetchall()
170+
cursor.close()
171+
conn.close()
172+
173+
counters = [
174+
{"page": row[0], "views": row[1], "last_viewed": row[2].isoformat() if row[2] else None}
175+
for row in rows
176+
]
177+
return jsonify({"status": "success", "counters": counters})
178+
except Exception as e:
179+
return jsonify({"status": "error", "error": str(e)}), 500
180+
181+
182+
@app.route("/db-test")
183+
def db_test():
184+
"""Test database operations (create table, insert, query)."""
185+
if not DATABASE_URL:
186+
return jsonify({"status": "not_configured"}), 400
187+
188+
try:
189+
# Initialize DB first
190+
init_db()
191+
192+
# Increment counter
193+
count = increment_counter("test")
194+
195+
return jsonify({
196+
"status": "success",
197+
"test_page_views": count,
198+
"message": "MySQL operations working!",
199+
})
200+
except Exception as e:
201+
return jsonify({"status": "error", "error": str(e)}), 500
202+
203+
204+
@app.route("/health")
205+
def health():
206+
"""Health check endpoint."""
207+
return jsonify({"status": "healthy"})
208+
209+
210+
if __name__ == "__main__":
211+
port = int(os.environ.get("PORT", 5000))
212+
app.run(host="0.0.0.0", port=port)

demos/demo29/app/hop3.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# hop3.toml - Native Python app with MySQL addon
2+
3+
[metadata]
4+
id = "demo29-mysql-counter"
5+
version = "0.1.0"
6+
title = "Native MySQL Page Counter Demo"
7+
description = "Demonstrates native Python deployment with MySQL addon - simple page view counter"
8+
9+
[build]
10+
# Use native Python builder (no Docker)
11+
builder = "python-3.12"
12+
pip-install = ["-r", "requirements.txt"]
13+
14+
[run]
15+
start = "gunicorn --workers 2 --bind 0.0.0.0:$PORT app:app"

demos/demo29/app/requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
flask>=3.0.0
2+
gunicorn>=21.0.0
3+
mysql-connector-python>=8.0.0

0 commit comments

Comments
 (0)