Skip to content

Commit e5b3903

Browse files
committed
demos: add more demos, fix issues.
1 parent 9531c3e commit e5b3903

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+4515
-25
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ packages-ignored
1111
packages-ignored2
1212
/local-notes
1313
/docs/site/
14+
/demos/logs/
1415

1516
# Temp
1617
/packages/hop3-web/src/hop3_web/lib/

demos/demo.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from lib.context import DemoContext, OutputLevel
3939
from lib.discovery import discover_demos, resolve_demo
4040
from lib.display import list_demos, print_banner, print_config, show_inventory
41+
from lib.logging import get_log_session, init_logging
4142
from lib.output import (
4243
pause,
4344
print_demo_result,
@@ -85,6 +86,13 @@ def main() -> int:
8586
if getattr(args, "debug", False):
8687
set_debug_mode(True)
8788

89+
# Initialize logging (unless --no-logs)
90+
if not getattr(args, "no_logs", False):
91+
logs_dir = getattr(args, "logs_dir", None)
92+
log_session = init_logging(logs_dir)
93+
if output_level >= OutputLevel.NORMAL:
94+
print_info(f"Logs: {log_session.session_dir}")
95+
8896
# Discover and resolve demos
8997
available_demos = discover_demos(args.demo_dirs)
9098
demos_to_run = _resolve_demos(args, available_demos)
@@ -245,6 +253,13 @@ def _show_summary(ctx: DemoContext, results: list, overall_start: float) -> int:
245253

246254
print_summary_stats(passed, failed, skipped, overall_duration)
247255

256+
# Show log directory if there were failures
257+
if failed > 0:
258+
log_session = get_log_session()
259+
if log_session:
260+
print()
261+
print_info(f"Detailed logs: {log_session.session_dir}")
262+
248263
# Show admin credentials and UI URL if keeping apps
249264
if ctx.no_cleanup and ctx.output_level >= OutputLevel.NORMAL:
250265
print()

demos/demo33/app/app.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Copyright (c) 2025, Abilian SAS
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Demo 33: Native Python app with declarative PostgreSQL provider.
4+
5+
A Flask application demonstrating the [[provider]] section in hop3.toml
6+
for declaring addon dependencies.
7+
"""
8+
from __future__ import annotations
9+
10+
import os
11+
12+
from flask import Flask, jsonify
13+
14+
app = Flask(__name__)
15+
16+
# Get DATABASE_URL from environment (set by Hop3 when addon is attached)
17+
DATABASE_URL = os.environ.get("DATABASE_URL")
18+
19+
20+
def get_db_connection():
21+
"""Get a database connection if DATABASE_URL is configured."""
22+
if not DATABASE_URL:
23+
return None
24+
import psycopg2
25+
26+
return psycopg2.connect(DATABASE_URL)
27+
28+
29+
@app.route("/")
30+
def home():
31+
"""Home page."""
32+
return jsonify({
33+
"app": "demo33",
34+
"type": "native",
35+
"feature": "declarative-provider",
36+
"message": "Welcome to demo33 - Declarative PostgreSQL Provider!",
37+
"database_configured": DATABASE_URL is not None,
38+
})
39+
40+
41+
@app.route("/db-status")
42+
def db_status():
43+
"""Check database connection status."""
44+
if not DATABASE_URL:
45+
return jsonify({"status": "not_configured", "message": "DATABASE_URL not set"})
46+
47+
try:
48+
conn = get_db_connection()
49+
with conn.cursor() as cur:
50+
cur.execute("SELECT version();")
51+
version = cur.fetchone()[0]
52+
conn.close()
53+
return jsonify({
54+
"status": "connected",
55+
"version": version,
56+
})
57+
except Exception as e:
58+
return jsonify({"status": "error", "error": str(e)}), 500
59+
60+
61+
@app.route("/db-test")
62+
def db_test():
63+
"""Test database operations (create table, insert, query)."""
64+
if not DATABASE_URL:
65+
return jsonify({"status": "not_configured"}), 400
66+
67+
try:
68+
conn = get_db_connection()
69+
with conn.cursor() as cur:
70+
# Create test table
71+
cur.execute("""
72+
CREATE TABLE IF NOT EXISTS provider_items (
73+
id SERIAL PRIMARY KEY,
74+
name VARCHAR(100),
75+
created_at TIMESTAMP DEFAULT NOW()
76+
)
77+
""")
78+
# Insert a test row
79+
cur.execute(
80+
"INSERT INTO provider_items (name) VALUES (%s) RETURNING id",
81+
(f"item-{os.urandom(4).hex()}",)
82+
)
83+
new_id = cur.fetchone()[0]
84+
# Count rows
85+
cur.execute("SELECT COUNT(*) FROM provider_items")
86+
count = cur.fetchone()[0]
87+
conn.commit()
88+
conn.close()
89+
return jsonify({
90+
"status": "success",
91+
"inserted_id": new_id,
92+
"total_items": count,
93+
})
94+
except Exception as e:
95+
return jsonify({"status": "error", "error": str(e)}), 500
96+
97+
98+
@app.route("/health")
99+
def health():
100+
"""Health check endpoint."""
101+
return jsonify({"status": "healthy"})
102+
103+
104+
if __name__ == "__main__":
105+
port = int(os.environ.get("PORT", 5000))
106+
app.run(host="0.0.0.0", port=port)

demos/demo33/app/hop3.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# hop3.toml - Native Python app with declarative PostgreSQL provider
2+
#
3+
# This demo showcases the [[provider]] section for declaring addon dependencies.
4+
# The [[provider]] section declares that this app needs a PostgreSQL database.
5+
6+
[metadata]
7+
id = "demo33-provider-postgres"
8+
version = "0.1.0"
9+
title = "Declarative PostgreSQL Demo"
10+
description = "Demonstrates declarative addon provisioning with [[provider]] section"
11+
tags = ["python", "postgres", "provider"]
12+
13+
[build]
14+
builder = "python-3.12"
15+
pip-install = ["-r", "requirements.txt"]
16+
17+
[run]
18+
start = "gunicorn --workers 2 --bind 0.0.0.0:$PORT app:app"
19+
20+
# Declarative provider section - this declares that the app needs PostgreSQL
21+
[[provider]]
22+
name = "postgres"
23+
plan = "standard"

demos/demo33/app/requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
flask
2+
gunicorn
3+
psycopg2-binary

demos/demo33/demo-script.py

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# Copyright (c) 2025, Abilian SAS
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Demo 33: Declarative PostgreSQL Provider.
4+
5+
Demonstrates the [[provider]] section in hop3.toml for declaring addon dependencies.
6+
This demo shows how to use the declarative format to specify that an app requires
7+
a PostgreSQL database.
8+
9+
Note: Currently, addons are still created manually via CLI commands.
10+
The [[provider]] section documents the app's requirements and will be used
11+
for automatic provisioning in a future version.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
from pathlib import Path
17+
from typing import TYPE_CHECKING
18+
19+
if TYPE_CHECKING:
20+
from lib import DemoContext
21+
22+
# Demo metadata
23+
TITLE = "Demo 33: Declarative PostgreSQL Provider"
24+
DESCRIPTION = """
25+
Demonstrates the [[provider]] section in hop3.toml:
26+
- Declaring addon requirements with [[provider]]
27+
- App uses 'name = "postgres"' in provider section
28+
- Shows how declarative provisioning will work
29+
- Currently creates addons manually (auto-provisioning coming)
30+
"""
31+
32+
APP_NAME = "demo33"
33+
APP_DIR = Path(__file__).parent / "app"
34+
DEFAULT_HOSTNAME = "demo33.hop"
35+
DB_NAME = "demo33-db"
36+
37+
38+
def run(ctx: DemoContext) -> None:
39+
"""Run the demo."""
40+
from lib import (
41+
check_app_status,
42+
cleanup_app,
43+
deploy_app,
44+
pause,
45+
print_blank,
46+
print_header,
47+
print_info,
48+
print_step,
49+
print_success,
50+
print_warning,
51+
redeploy_app,
52+
set_hostname,
53+
show_app_structure,
54+
show_file_content,
55+
test_app_via_curl,
56+
wait_for_app,
57+
)
58+
from lib.commands import run_hop3
59+
60+
app_hostname = DEFAULT_HOSTNAME
61+
app_url = f"https://{app_hostname}"
62+
63+
# Clean up any leftover database from previous failed runs
64+
run_hop3(f"addons:destroy {DB_NAME} --service-type postgres", check=False, show=False)
65+
66+
# Show app structure
67+
print_header("Deploying App with Declarative PostgreSQL Provider")
68+
69+
show_app_structure(
70+
APP_NAME,
71+
[
72+
("app.py", "Flask application with PostgreSQL support"),
73+
("requirements.txt", "Python dependencies (flask, psycopg2)"),
74+
("hop3.toml", "Hop3 configuration WITH [[provider]] section"),
75+
],
76+
)
77+
print_info("This demo showcases the [[provider]] section in hop3.toml.")
78+
print_info("The app declares its PostgreSQL requirement declaratively.")
79+
print_blank()
80+
pause(ctx.pause_between_steps)
81+
82+
# Show hop3.toml - emphasize the [[provider]] section
83+
print_header("hop3.toml with [[provider]] Section")
84+
show_file_content(APP_DIR / "hop3.toml", "hop3.toml:")
85+
print_blank()
86+
print_info("Note the [[provider]] section at the end!")
87+
print_info("This declares that the app needs a 'postgres' addon.")
88+
pause(ctx.pause_between_steps)
89+
90+
# Deploy the application
91+
print_header("Step 1: Deploy Application")
92+
deploy_app(ctx, APP_NAME, APP_DIR)
93+
set_hostname(ctx, APP_NAME, app_hostname)
94+
redeploy_app(ctx, APP_NAME, APP_DIR)
95+
wait_for_app(seconds=5, message="Waiting for application to start...")
96+
check_app_status(ctx, APP_NAME)
97+
98+
# Test main endpoint
99+
print_header("Step 2: Test Application (Without Database)")
100+
test_app_via_curl(ctx, app_url, expected_content="Welcome to demo33")
101+
pause(ctx.pause_between_steps)
102+
103+
# Test database status - should show not_configured
104+
print_header("Step 3: Check Database Status (Before Addon)")
105+
print_step("Testing /db-status endpoint...")
106+
test_app_via_curl(ctx, f"{app_url}/db-status", expected_content="not_configured")
107+
print_blank()
108+
print_info("The [[provider]] section declared the need for PostgreSQL,")
109+
print_info("but the addon hasn't been provisioned yet.")
110+
pause(ctx.pause_between_steps)
111+
112+
# Create PostgreSQL addon (manual for now)
113+
print_header("Step 4: Provision PostgreSQL (Manual)")
114+
print_info("In the future, this will be automatic based on [[provider]].")
115+
print_info("For now, we create the addon manually:")
116+
print_blank()
117+
print_step(f"Creating PostgreSQL database '{DB_NAME}'...")
118+
result = run_hop3(f"addons:create postgres {DB_NAME}", check=False)
119+
120+
postgres_available = result.returncode == 0
121+
if not postgres_available:
122+
print_warning("PostgreSQL creation failed.")
123+
if result.stderr:
124+
print_info(f" Error: {result.stderr.strip()}")
125+
else:
126+
print_success(f"PostgreSQL database '{DB_NAME}' created.")
127+
pause(ctx.pause_between_steps)
128+
129+
# Attach database to app
130+
if postgres_available:
131+
print_header("Step 5: Attach Database to Application")
132+
print_step(f"Attaching '{DB_NAME}' to '{APP_NAME}'...")
133+
result = run_hop3(
134+
f"addons:attach {DB_NAME} --app {APP_NAME} --service-type postgres",
135+
check=False,
136+
)
137+
138+
if result.returncode != 0:
139+
print_warning("Failed to attach database.")
140+
postgres_available = False
141+
else:
142+
print_success("Database attached. DATABASE_URL is now set.")
143+
pause(ctx.pause_between_steps)
144+
145+
# Redeploy to pick up DATABASE_URL
146+
if postgres_available:
147+
print_header("Step 6: Redeploy to Apply Configuration")
148+
print_step("Redeploying application with DATABASE_URL...")
149+
redeploy_app(ctx, APP_NAME, APP_DIR)
150+
wait_for_app(seconds=5)
151+
152+
# Test database connection
153+
print_header("Step 7: Verify Database Connection")
154+
print_step("Testing /db-status endpoint...")
155+
test_app_via_curl(ctx, f"{app_url}/db-status", expected_content="connected")
156+
print_success("App connected to PostgreSQL!")
157+
pause(ctx.pause_between_steps)
158+
159+
# Test database operations
160+
print_header("Step 8: Test Database Operations")
161+
print_step("Testing /db-test endpoint...")
162+
test_app_via_curl(ctx, f"{app_url}/db-test", expected_content="success")
163+
print_success("Database operations working!")
164+
pause(ctx.pause_between_steps)
165+
166+
# Cleanup database
167+
print_header("Step 9: Cleanup Database")
168+
print_step("Detaching and destroying database...")
169+
run_hop3(
170+
f"addons:detach {DB_NAME} --app {APP_NAME} --service-type postgres",
171+
check=False,
172+
)
173+
run_hop3(f"addons:destroy {DB_NAME} --service-type postgres", check=False)
174+
print_success("Database cleaned up.")
175+
pause(ctx.pause_between_steps)
176+
else:
177+
print_header("PostgreSQL Not Available")
178+
print_info("The [[provider]] section would enable automatic provisioning")
179+
print_info("once that feature is implemented.")
180+
print_blank()
181+
pause(ctx.pause_between_steps)
182+
183+
# Cleanup app
184+
cleanup_app(ctx, APP_NAME, app_url)
185+
186+
print_blank()
187+
print_header("Summary: [[provider]] Section")
188+
print_info("This demo showed the declarative [[provider]] section:")
189+
print_info(" [[provider]]")
190+
print_info(' name = "postgres"')
191+
print_info(' plan = "standard"')
192+
print_blank()
193+
print_info("Benefits of declarative providers:")
194+
print_info(" - Documents app requirements in hop3.toml")
195+
print_info(" - Enables future automatic provisioning")
196+
print_info(" - Marketplace displays required services")
197+
print_blank()
198+
199+
if postgres_available:
200+
print_success("Demo 33 completed: Declarative PostgreSQL provider demonstrated.")
201+
else:
202+
print_success("Demo 33 completed: [[provider]] section showcased (PostgreSQL unavailable).")

0 commit comments

Comments
 (0)