This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-atr-experiments.git
The following commit(s) were added to refs/heads/main by this push:
new 46e5154 Refactor routes and other code, and add a site navigation page
46e5154 is described below
commit 46e51549c773e349baec6aa1851cf59b9147d410
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Feb 13 19:30:01 2025 +0200
Refactor routes and other code, and add a site navigation page
---
atr/routes.py | 147 ++++++++++------
atr/server.py | 24 +--
atr/templates/data-browser.html | 2 +-
atr/templates/pages.html | 213 ++++++++++++++++++++++++
atr/templates/{root.html => pmc-directory.html} | 0
atr/templates/user-uploads.html | 2 +-
pyproject.toml | 2 +-
7 files changed, 315 insertions(+), 75 deletions(-)
diff --git a/atr/routes.py b/atr/routes.py
index d9a482c..56ba7b8 100644
--- a/atr/routes.py
+++ b/atr/routes.py
@@ -64,30 +64,26 @@ def compute_sha512(file_path: Path) -> str:
return sha512.hexdigest()
-async def save_file_by_hash(file, base_dir: Path) -> Tuple[Path, str]:
- """
- Save a file using its SHA3-256 hash as the filename.
- Returns the path where the file was saved and its hash.
- """
- # FileStorage.read() returns bytes directly, no need to await
- data = file.read()
- file_hash = compute_sha3_256(data)
-
- # Create path with hash as filename
- path = base_dir / file_hash
- path.parent.mkdir(parents=True, exist_ok=True)
-
- # Only write if file doesn't exist
- # If it does exist, it'll be the same content anyway
- if not path.exists():
- path.write_bytes(data)
-
- return path, file_hash
[email protected]("/")
+async def root() -> str:
+ "Main PMC directory page."
+ return """\
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <title>ATR</title>
+</head>
+<body>
+ <h1>Apache Trusted Releases</h1>
+</body>
+</html>
+"""
@APP.route("/add-release-candidate", methods=["GET", "POST"])
@require(R.committer)
-async def add_release_candidate() -> str:
+async def root_add_release_candidate() -> str:
"Add a release candidate to the database."
session = await session_read()
if session is None:
@@ -178,10 +174,10 @@ async def add_release_candidate() -> str:
)
[email protected]("/admin/data-browser")
[email protected]("/admin/data-browser/<model>")
[email protected]("/admin/database")
[email protected]("/admin/database/<model>")
@require(R.committer)
-async def admin_data_browser(model: str = "PMC") -> str:
+async def root_admin_database(model: str = "PMC") -> str:
"Browse all records in the database."
session = await session_read()
if session is None:
@@ -231,7 +227,8 @@ async def admin_data_browser(model: str = "PMC") -> str:
@APP.route("/admin/update-pmcs", methods=["GET", "POST"])
-async def admin_update_pmcs() -> str:
+@require(R.committer)
+async def root_admin_update_pmcs() -> str:
"Update PMCs from remote, authoritative committee-info.json."
# Check authentication
session = await session_read()
@@ -307,8 +304,42 @@ async def admin_update_pmcs() -> str:
return await render_template("update-pmcs.html")
[email protected]("/database/debug")
+async def root_database_debug() -> str:
+ """Debug information about the database."""
+ with Session(current_app.config["engine"]) as session:
+ statement = select(PMC)
+ pmcs = session.exec(statement).all()
+ return f"Database using {current_app.config['DATA_MODELS_FILE']} has
{len(pmcs)} PMCs"
+
+
[email protected]("/pages")
+async def root_pages() -> str:
+ "List all pages on the website."
+ return await render_template("pages.html")
+
+
[email protected]("/pmc/<project_name>")
+async def root_pmc_arg(project_name: str) -> dict:
+ "Get a specific PMC by project name."
+ with Session(current_app.config["engine"]) as session:
+ statement = select(PMC).where(PMC.project_name == project_name)
+ pmc = session.exec(statement).first()
+
+ if not pmc:
+ raise ASFQuartException("PMC not found", errorcode=404)
+
+ return {
+ "id": pmc.id,
+ "project_name": pmc.project_name,
+ "pmc_members": pmc.pmc_members,
+ "committers": pmc.committers,
+ "release_managers": pmc.release_managers,
+ }
+
+
@APP.route("/pmc/create/<project_name>")
-async def pmc_create_arg(project_name: str) -> dict:
+async def root_pmc_create_arg(project_name: str) -> dict:
"Create a new PMC with some sample data."
pmc = PMC(
project_name=project_name,
@@ -338,8 +369,18 @@ async def pmc_create_arg(project_name: str) -> dict:
}
[email protected]("/pmc/directory")
+async def root_pmc_directory() -> str:
+ "Main PMC directory page."
+ with Session(current_app.config["engine"]) as session:
+ # Get all PMCs and their latest releases
+ statement = select(PMC)
+ pmcs = session.exec(statement).all()
+ return await render_template("pmc-directory.html", pmcs=pmcs)
+
+
@APP.route("/pmc/list")
-async def pmc_list() -> List[dict]:
+async def root_pmc_list() -> List[dict]:
"List all PMCs in the database."
with Session(current_app.config["engine"]) as session:
statement = select(PMC)
@@ -357,38 +398,15 @@ async def pmc_list() -> List[dict]:
]
[email protected]("/pmc/<project_name>")
-async def pmc_arg(project_name: str) -> dict:
- "Get a specific PMC by project name."
- with Session(current_app.config["engine"]) as session:
- statement = select(PMC).where(PMC.project_name == project_name)
- pmc = session.exec(statement).first()
-
- if not pmc:
- raise ASFQuartException("PMC not found", errorcode=404)
-
- return {
- "id": pmc.id,
- "project_name": pmc.project_name,
- "pmc_members": pmc.pmc_members,
- "committers": pmc.committers,
- "release_managers": pmc.release_managers,
- }
-
-
[email protected]("/")
-async def root() -> str:
- "Main PMC directory page."
- with Session(current_app.config["engine"]) as session:
- # Get all PMCs and their latest releases
- statement = select(PMC)
- pmcs = session.exec(statement).all()
- return await render_template("root.html", pmcs=pmcs)
[email protected]("/secret")
+@require(R.committer)
+async def root_secret() -> str:
+ return "Secret stuff!"
@APP.route("/user/uploads")
@require(R.committer)
-async def user_uploads() -> str:
+async def root_user_uploads() -> str:
"Show all release candidates uploaded by the current user."
session = await session_read()
if session is None:
@@ -410,3 +428,24 @@ async def user_uploads() -> str:
user_releases.append(r)
return await render_template("user-uploads.html",
releases=user_releases)
+
+
+async def save_file_by_hash(file, base_dir: Path) -> Tuple[Path, str]:
+ """
+ Save a file using its SHA3-256 hash as the filename.
+ Returns the path where the file was saved and its hash.
+ """
+ # FileStorage.read() returns bytes directly, no need to await
+ data = file.read()
+ file_hash = compute_sha3_256(data)
+
+ # Create path with hash as filename
+ path = base_dir / file_hash
+ path.parent.mkdir(parents=True, exist_ok=True)
+
+ # Only write if file doesn't exist
+ # If it does exist, it'll be the same content anyway
+ if not path.exists():
+ path.write_bytes(data)
+
+ return path, file_hash
diff --git a/atr/server.py b/atr/server.py
index 7282064..c8305e0 100644
--- a/atr/server.py
+++ b/atr/server.py
@@ -20,13 +20,12 @@
import os
import asfquart
-from asfquart.auth import Requirements as R, require
from asfquart.base import QuartApp
-from sqlmodel import Session, SQLModel, create_engine, select
+from sqlmodel import SQLModel, create_engine
from alembic import command
from alembic.config import Config
-from .models import PMC, __file__ as data_models_file
+from .models import __file__ as data_models_file
def register_routes() -> str:
@@ -42,7 +41,7 @@ def create_app() -> QuartApp:
app = asfquart.construct(__name__)
@app.before_serving
- async def create_database():
+ async def create_database() -> None:
# Get the project root directory (where alembic.ini is)
project_root =
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -57,6 +56,7 @@ def create_app() -> QuartApp:
release_storage = os.path.join(state_dir, "releases")
os.makedirs(release_storage, exist_ok=True)
app.config["RELEASE_STORAGE_DIR"] = release_storage
+ app.config["DATA_MODELS_FILE"] = data_models_file
sqlite_url = "sqlite:///./atr.db"
engine = create_engine(
@@ -91,23 +91,11 @@ def create_app() -> QuartApp:
# Create any tables that might be missing
SQLModel.metadata.create_all(engine)
- app.config["engine"] = engine
-
- @app.route("/secret")
- @require(R.committer)
- async def secret() -> str:
- return "Secret stuff!"
- @app.get("/database/debug")
- async def database_debug() -> str:
- """Debug information about the database."""
- with Session(app.config["engine"]) as session:
- statement = select(PMC)
- pmcs = session.exec(statement).all()
- return f"Database using {data_models_file} has {len(pmcs)} PMCs"
+ app.config["engine"] = engine
@app.after_serving
- async def shutdown():
+ async def shutdown() -> None:
app.background_tasks.clear()
register_routes()
diff --git a/atr/templates/data-browser.html b/atr/templates/data-browser.html
index 9f0a447..3281b48 100644
--- a/atr/templates/data-browser.html
+++ b/atr/templates/data-browser.html
@@ -65,7 +65,7 @@
<div class="model-nav">
{% for model_name in models %}
- <a href="{{ url_for('admin_data_browser', model=model_name) }}"
+ <a href="{{ url_for('root_admin_database', model=model_name) }}"
{% if model == model_name %}class="active"{% endif %}>{{ model_name
}}</a>
{% endfor %}
</div>
diff --git a/atr/templates/pages.html b/atr/templates/pages.html
new file mode 100644
index 0000000..1d56336
--- /dev/null
+++ b/atr/templates/pages.html
@@ -0,0 +1,213 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
+ <meta name="description" content="List of all pages and endpoints in ATR."
/>
+ <title>ATR | Pages</title>
+ <link rel="stylesheet" href="{{ url_for('static', filename='root.css') }}"
/>
+ <style>
+ .endpoint-list {
+ margin: 2rem 0;
+ }
+
+ .endpoint-group {
+ margin-bottom: 2rem;
+ }
+
+ .endpoint {
+ border: 1px solid #ddd;
+ padding: 1rem;
+ margin-bottom: 1rem;
+ border-radius: 4px;
+ }
+
+ .endpoint h3 {
+ margin: 0 0 0.5rem 0;
+ }
+
+ .endpoint-meta {
+ color: #666;
+ font-size: 0.9em;
+ margin-bottom: 0.5rem;
+ }
+
+ .endpoint-description {
+ margin-bottom: 0.5rem;
+ }
+
+ .access-requirement {
+ display: inline-block;
+ padding: 0.25rem 0.5rem;
+ border-radius: 2px;
+ font-size: 0.8em;
+ background: #f5f5f5;
+ }
+
+ .access-requirement.committer {
+ background: #e6f3ff;
+ border: 1px solid #cce5ff;
+ }
+
+ .access-requirement.admin {
+ background: #ffeeba;
+ border: 1px solid #f5d88c;
+ }
+
+ .access-requirement.public {
+ background: #e6ffe6;
+ border: 1px solid #ccebcc;
+ }
+ </style>
+ </head>
+ <body>
+ <h1>ATR Pages</h1>
+ <p class="intro">A complete list of all pages and endpoints available in
ATR.</p>
+
+ <div class="endpoint-list">
+ <div class="endpoint-group">
+ <h2>Main Pages</h2>
+
+ <div class="endpoint">
+ <h3>
+ <a href="{{ url_for('root') }}">/</a>
+ </h3>
+ <div class="endpoint-description">Simple welcome page with Apache
Trusted Releases title.</div>
+ <div class="endpoint-meta">
+ Access: <span class="access-requirement public">Public</span>
+ </div>
+ </div>
+
+ <div class="endpoint">
+ <h3>
+ <a href="{{ url_for('root_pages') }}">/pages</a>
+ </h3>
+ <div class="endpoint-description">List of all pages on the website
(this page).</div>
+ <div class="endpoint-meta">
+ Access: <span class="access-requirement public">Public</span>
+ </div>
+ </div>
+ </div>
+
+ <div class="endpoint-group">
+ <h2>PMC Management</h2>
+
+ <div class="endpoint">
+ <h3>
+ <a href="{{ url_for('root_pmc_directory') }}">/pmc/directory</a>
+ </h3>
+ <div class="endpoint-description">Main PMC directory page with all
PMCs and their latest releases.</div>
+ <div class="endpoint-meta">
+ Access: <span class="access-requirement public">Public</span>
+ </div>
+ </div>
+
+ <div class="endpoint">
+ <h3>
+ <a href="{{ url_for('root_pmc_list') }}">/pmc/list</a>
+ </h3>
+ <div class="endpoint-description">List all PMCs in the database
(JSON format).</div>
+ <div class="endpoint-meta">
+ Access: <span class="access-requirement public">Public</span>
+ </div>
+ </div>
+
+ <div class="endpoint">
+ <h3>/pmc/<project_name></h3>
+ <div class="endpoint-description">Get details for a specific PMC
(JSON format).</div>
+ <div class="endpoint-meta">
+ Access: <span class="access-requirement public">Public</span>
+ </div>
+ </div>
+
+ <div class="endpoint">
+ <h3>/pmc/create/<project_name></h3>
+ <div class="endpoint-description">Create a new PMC with sample data
(JSON format).</div>
+ <div class="endpoint-meta">
+ Access: <span class="access-requirement public">Public</span>
+ </div>
+ </div>
+ </div>
+
+ <div class="endpoint-group">
+ <h2>Release Management</h2>
+
+ <div class="endpoint">
+ <h3>
+ <a href="{{ url_for('root_add_release_candidate')
}}">/add-release-candidate</a>
+ </h3>
+ <div class="endpoint-description">Add a release candidate to the
database.</div>
+ <div class="endpoint-meta">
+ Access: <span class="access-requirement committer">Committer</span>
+ <br />
+ Additional requirement: Must be PMC member of the target project
+ </div>
+ </div>
+
+ <div class="endpoint">
+ <h3>
+ <a href="{{ url_for('root_user_uploads') }}">/user/uploads</a>
+ </h3>
+ <div class="endpoint-description">Show all release candidates
uploaded by the current user.</div>
+ <div class="endpoint-meta">
+ Access: <span class="access-requirement committer">Committer</span>
+ </div>
+ </div>
+ </div>
+
+ <div class="endpoint-group">
+ <h2>Administration</h2>
+
+ <div class="endpoint">
+ <h3>
+ <a href="{{ url_for('root_admin_database') }}">/admin/database</a>
+ </h3>
+ <div class="endpoint-description">Browse all records in the
database.</div>
+ <div class="endpoint-meta">
+ Access: <span class="access-requirement committer">Committer</span>
+ <span class="access-requirement admin">Admin</span>
+ <br />
+ Additional requirement: Must be in ALLOWED_USERS list
+ </div>
+ </div>
+
+ <div class="endpoint">
+ <h3>
+ <a href="{{ url_for('root_admin_update_pmcs')
}}">/admin/update-pmcs</a>
+ </h3>
+ <div class="endpoint-description">Update PMCs from remote,
authoritative committee-info.json.</div>
+ <div class="endpoint-meta">
+ Access: <span class="access-requirement committer">Committer</span>
+ <span class="access-requirement admin">Admin</span>
+ <br />
+ Additional requirement: Must be in ALLOWED_USERS list
+ </div>
+ </div>
+ </div>
+
+ <div class="endpoint-group">
+ <h2>Debug/Testing</h2>
+
+ <div class="endpoint">
+ <h3>
+ <a href="{{ url_for('root_secret') }}">/secret</a>
+ </h3>
+ <div class="endpoint-description">Test endpoint for
authentication.</div>
+ <div class="endpoint-meta">
+ Access: <span class="access-requirement committer">Committer</span>
+ </div>
+ </div>
+
+ <div class="endpoint">
+ <h3>
+ <a href="{{ url_for('root_database_debug') }}">/database/debug</a>
+ </h3>
+ <div class="endpoint-description">Debug information about the
database.</div>
+ <div class="endpoint-meta">
+ Access: <span class="access-requirement public">Public</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/atr/templates/root.html b/atr/templates/pmc-directory.html
similarity index 100%
rename from atr/templates/root.html
rename to atr/templates/pmc-directory.html
diff --git a/atr/templates/user-uploads.html b/atr/templates/user-uploads.html
index 3a61d39..c4dd822 100644
--- a/atr/templates/user-uploads.html
+++ b/atr/templates/user-uploads.html
@@ -80,7 +80,7 @@
{% endif %}
<p>
- <a href="{{ url_for('add_release_candidate') }}">Upload a new release
candidate</a>
+ <a href="{{ url_for('root_add_release_candidate') }}">Upload a new
release candidate</a>
</p>
</body>
</html>
diff --git a/pyproject.toml b/pyproject.toml
index e5c1d11..f8ca0ee 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -99,5 +99,5 @@ format_js = true
max_line_length = 120
use_gitignore = true
preserve_blank_lines = true
-ignore = "H006,H031" # Ignore img alt and meta description warnings
+ignore = "H006,H031"
include = "atr/templates"
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]