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 f92ba0e  Add blueprint code from branch by @netomi
f92ba0e is described below

commit f92ba0e20c8982e67f6f581bcf30c172feed665a
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Feb 17 19:54:58 2025 +0200

    Add blueprint code from branch by @netomi
---
 atr/blueprints/__init__.py               |  32 +++++++
 atr/blueprints/secret/__init__.py        |  49 ++++++++++
 atr/blueprints/secret/secret.py          | 160 +++++++++++++++++++++++++++++++
 atr/routes.py                            | 150 -----------------------------
 atr/server.py                            |   4 +
 atr/static/css/atr.css                   |   2 +-
 atr/templates/add-release-candidate.html |   2 +-
 atr/templates/data-browser.html          |   2 +-
 atr/templates/pages.html                 |  42 +-------
 atr/templates/user-keys-add.html         |   2 +-
 typestubs/asfquart/auth.pyi              |  87 +++++++++--------
 11 files changed, 297 insertions(+), 235 deletions(-)

diff --git a/atr/blueprints/__init__.py b/atr/blueprints/__init__.py
new file mode 100644
index 0000000..d5ccb09
--- /dev/null
+++ b/atr/blueprints/__init__.py
@@ -0,0 +1,32 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from importlib import import_module
+from importlib.util import find_spec
+
+from asfquart.base import QuartApp
+
+_BLUEPRINT_MODULES = ["secret"]
+
+
+def register_blueprints(app: QuartApp) -> None:
+    for routes_name in _BLUEPRINT_MODULES:
+        routes_fqn = f"atr.blueprints.{routes_name}.{routes_name}"
+        spec = find_spec(routes_fqn)
+        if spec is not None:
+            module = import_module(routes_fqn)
+            app.register_blueprint(module.blueprint)
diff --git a/atr/blueprints/secret/__init__.py 
b/atr/blueprints/secret/__init__.py
new file mode 100644
index 0000000..df80c2d
--- /dev/null
+++ b/atr/blueprints/secret/__init__.py
@@ -0,0 +1,49 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from quart import Blueprint
+
+from asfquart.auth import Requirements as R
+from asfquart.auth import require
+from asfquart.base import ASFQuartException
+from asfquart.session import read as session_read
+
+blueprint = Blueprint("secret_blueprint", __name__, url_prefix="/secret")
+
+ALLOWED_USERS = {
+    "cwells",
+    "fluxo",
+    "gmcdonald",
+    "humbedooh",
+    "sbp",
+    "tn",
+    "wave",
+}
+
+
[email protected]_request
+async def before_request_func() -> None:
+    @require(R.committer)
+    async def check_logged_in() -> None:
+        session = await session_read()
+        if session is None:
+            raise ASFQuartException("Not authenticated", errorcode=401)
+
+        if session.uid not in ALLOWED_USERS:
+            raise ASFQuartException("You are not authorized to access the 
admin interface", errorcode=403)
+
+    await check_logged_in()
diff --git a/atr/blueprints/secret/secret.py b/atr/blueprints/secret/secret.py
new file mode 100644
index 0000000..d0daa5b
--- /dev/null
+++ b/atr/blueprints/secret/secret.py
@@ -0,0 +1,160 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import json
+
+import httpx
+from quart import current_app, render_template, request
+from sqlmodel import select
+
+from asfquart.base import ASFQuartException
+
+from ...models import (
+    PMC,
+    DistributionChannel,
+    Package,
+    PMCKeyLink,
+    ProductLine,
+    PublicSigningKey,
+    Release,
+    VotePolicy,
+)
+from . import blueprint
+
+
[email protected]("/data")
[email protected]("/data/<model>")
+async def secret_data(model: str = "PMC") -> str:
+    "Browse all records in the database."
+
+    # Map of model names to their classes
+    models = {
+        "PMC": PMC,
+        "Release": Release,
+        "Package": Package,
+        "VotePolicy": VotePolicy,
+        "ProductLine": ProductLine,
+        "DistributionChannel": DistributionChannel,
+        "PublicSigningKey": PublicSigningKey,
+        "PMCKeyLink": PMCKeyLink,
+    }
+
+    if model not in models:
+        # Default to PMC if invalid model specified
+        model = "PMC"
+
+    async_session = current_app.config["async_session"]
+    async with async_session() as db_session:
+        # Get all records for the selected model
+        statement = select(models[model])
+        records = (await db_session.execute(statement)).scalars().all()
+
+        # Convert records to dictionaries for JSON serialization
+        records_dict = []
+        for record in records:
+            if hasattr(record, "dict"):
+                record_dict = record.dict()
+            else:
+                # Fallback for models without dict() method
+                record_dict = {
+                    "id": getattr(record, "id", None),
+                    "storage_key": getattr(record, "storage_key", None),
+                }
+                for key in record.__dict__:
+                    if not key.startswith("_"):
+                        record_dict[key] = getattr(record, key)
+            records_dict.append(record_dict)
+
+        return await render_template("data-browser.html", 
models=list(models.keys()), model=model, records=records_dict)
+
+
[email protected]("/pmcs/update", methods=["GET", "POST"])
+async def secret_pmcs_update() -> str:
+    "Update PMCs from remote, authoritative committee-info.json."
+
+    if request.method == "POST":
+        # Fetch committee-info.json from Whimsy
+        WHIMSY_URL = "https://whimsy.apache.org/public/committee-info.json";
+        async with httpx.AsyncClient() as client:
+            try:
+                response = await client.get(WHIMSY_URL)
+                response.raise_for_status()
+                data = response.json()
+            except (httpx.RequestError, json.JSONDecodeError) as e:
+                raise ASFQuartException(f"Failed to fetch committee data: 
{str(e)}", errorcode=500)
+
+        committees = data.get("committees", {})
+        updated_count = 0
+
+        async_session = current_app.config["async_session"]
+        async with async_session() as db_session:
+            async with db_session.begin():
+                for committee_id, info in committees.items():
+                    # Skip non-PMC committees
+                    if not info.get("pmc", False):
+                        continue
+
+                    # Get or create PMC
+                    statement = select(PMC).where(PMC.project_name == 
committee_id)
+                    pmc = (await 
db_session.execute(statement)).scalar_one_or_none()
+                    if not pmc:
+                        pmc = PMC(project_name=committee_id)
+                        db_session.add(pmc)
+
+                    # Update PMC data
+                    roster = info.get("roster", {})
+                    # TODO: Here we say that roster == pmc_members == 
committers
+                    # We ought to do this more accurately instead
+                    pmc.pmc_members = list(roster.keys())
+                    pmc.committers = list(roster.keys())
+
+                    # Mark chairs as release managers
+                    # TODO: Who else is a release manager? How do we know?
+                    chairs = [m["id"] for m in info.get("chairs", [])]
+                    pmc.release_managers = chairs
+
+                    updated_count += 1
+
+                # Add special entry for Tooling PMC
+                # Not clear why, but it's not in the Whimsy data
+                statement = select(PMC).where(PMC.project_name == "tooling")
+                tooling_pmc = (await 
db_session.execute(statement)).scalar_one_or_none()
+                if not tooling_pmc:
+                    tooling_pmc = PMC(project_name="tooling")
+                    db_session.add(tooling_pmc)
+                    updated_count += 1
+
+                # Update Tooling PMC data
+                # Could put this in the "if not tooling_pmc" block, perhaps
+                tooling_pmc.pmc_members = ["wave", "tn", "sbp"]
+                tooling_pmc.committers = ["wave", "tn", "sbp"]
+                tooling_pmc.release_managers = ["wave"]
+
+        return f"Successfully updated {updated_count} PMCs from Whimsy"
+
+    # For GET requests, show the update form
+    return await render_template("update-pmcs.html")
+
+
[email protected]("/debug/database")
+async def secret_debug_database() -> str:
+    """Debug information about the database."""
+    async_session = current_app.config["async_session"]
+    async with async_session() as db_session:
+        statement = select(PMC)
+        pmcs = (await db_session.execute(statement)).scalars().all()
+        return f"Database using {current_app.config['DATA_MODELS_FILE']} has 
{len(pmcs)} PMCs"
diff --git a/atr/routes.py b/atr/routes.py
index a1be503..4b8268a 100644
--- a/atr/routes.py
+++ b/atr/routes.py
@@ -20,7 +20,6 @@
 import asyncio
 import datetime
 import hashlib
-import json
 import pprint
 import secrets
 import shutil
@@ -33,7 +32,6 @@ from typing import Any, cast
 import aiofiles
 import aiofiles.os
 import gnupg
-import httpx
 from quart import Request, current_app, render_template, request
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.orm import selectinload
@@ -50,15 +48,12 @@ from asfquart.session import read as session_read
 
 from .models import (
     PMC,
-    DistributionChannel,
     Package,
     PMCKeyLink,
-    ProductLine,
     PublicSigningKey,
     Release,
     ReleasePhase,
     ReleaseStage,
-    VotePolicy,
 )
 
 if APP is ...:
@@ -199,145 +194,6 @@ async def root_add_release_candidate() -> str:
     )
 
 
[email protected]("/admin/database")
[email protected]("/admin/database/<model>")
-@require(R.committer)
-async def root_admin_database(model: str = "PMC") -> str:
-    "Browse all records in the database."
-    session = await session_read()
-    if session is None:
-        raise ASFQuartException("Not authenticated", errorcode=401)
-
-    if session.uid not in ALLOWED_USERS:
-        raise ASFQuartException("You are not authorized to browse data", 
errorcode=403)
-
-    # Map of model names to their classes
-    models = {
-        "PMC": PMC,
-        "Release": Release,
-        "Package": Package,
-        "VotePolicy": VotePolicy,
-        "ProductLine": ProductLine,
-        "DistributionChannel": DistributionChannel,
-        "PublicSigningKey": PublicSigningKey,
-        "PMCKeyLink": PMCKeyLink,
-    }
-
-    if model not in models:
-        # Default to PMC if invalid model specified
-        model = "PMC"
-
-    async_session = current_app.config["async_session"]
-    async with async_session() as db_session:
-        # Get all records for the selected model
-        statement = select(models[model])
-        records = (await db_session.execute(statement)).scalars().all()
-
-        # Convert records to dictionaries for JSON serialization
-        records_dict = []
-        for record in records:
-            if hasattr(record, "dict"):
-                record_dict = record.dict()
-            else:
-                # Fallback for models without dict() method
-                record_dict = {
-                    "id": getattr(record, "id", None),
-                    "storage_key": getattr(record, "storage_key", None),
-                }
-                for key in record.__dict__:
-                    if not key.startswith("_"):
-                        record_dict[key] = getattr(record, key)
-            records_dict.append(record_dict)
-
-        return await render_template("data-browser.html", 
models=list(models.keys()), model=model, records=records_dict)
-
-
[email protected]("/admin/update-pmcs", methods=["GET", "POST"])
-@require(R.committer)
-async def root_admin_update_pmcs() -> str:
-    "Update PMCs from remote, authoritative committee-info.json."
-    # Check authentication
-    session = await session_read()
-    if session is None:
-        raise ASFQuartException("Not authenticated", errorcode=401)
-
-    if session.uid not in ALLOWED_USERS:
-        raise ASFQuartException("You are not authorized to update PMCs", 
errorcode=403)
-
-    if request.method == "POST":
-        # Fetch committee-info.json from Whimsy
-        WHIMSY_URL = "https://whimsy.apache.org/public/committee-info.json";
-        async with httpx.AsyncClient() as client:
-            try:
-                response = await client.get(WHIMSY_URL)
-                response.raise_for_status()
-                data = response.json()
-            except (httpx.RequestError, json.JSONDecodeError) as e:
-                raise ASFQuartException(f"Failed to fetch committee data: 
{str(e)}", errorcode=500)
-
-        committees = data.get("committees", {})
-        updated_count = 0
-
-        async_session = current_app.config["async_session"]
-        async with async_session() as db_session:
-            async with db_session.begin():
-                for committee_id, info in committees.items():
-                    # Skip non-PMC committees
-                    if not info.get("pmc", False):
-                        continue
-
-                    # Get or create PMC
-                    statement = select(PMC).where(PMC.project_name == 
committee_id)
-                    pmc = (await 
db_session.execute(statement)).scalar_one_or_none()
-                    if not pmc:
-                        pmc = PMC(project_name=committee_id)
-                        db_session.add(pmc)
-
-                    # Update PMC data
-                    roster = info.get("roster", {})
-                    # TODO: Here we say that roster == pmc_members == 
committers
-                    # We ought to do this more accurately instead
-                    pmc.pmc_members = list(roster.keys())
-                    pmc.committers = list(roster.keys())
-
-                    # Mark chairs as release managers
-                    # TODO: Who else is a release manager? How do we know?
-                    chairs = [m["id"] for m in info.get("chairs", [])]
-                    pmc.release_managers = chairs
-
-                    updated_count += 1
-
-                # Add special entry for Tooling PMC
-                # Not clear why, but it's not in the Whimsy data
-                statement = select(PMC).where(PMC.project_name == "tooling")
-                tooling_pmc = (await 
db_session.execute(statement)).scalar_one_or_none()
-                if not tooling_pmc:
-                    tooling_pmc = PMC(project_name="tooling")
-                    db_session.add(tooling_pmc)
-                    updated_count += 1
-
-                # Update Tooling PMC data
-                # Could put this in the "if not tooling_pmc" block, perhaps
-                tooling_pmc.pmc_members = ["wave", "tn", "sbp"]
-                tooling_pmc.committers = ["wave", "tn", "sbp"]
-                tooling_pmc.release_managers = ["wave"]
-
-        return f"Successfully updated {updated_count} PMCs from Whimsy"
-
-    # For GET requests, show the update form
-    return await render_template("update-pmcs.html")
-
-
[email protected]("/database/debug")
-async def root_database_debug() -> str:
-    """Debug information about the database."""
-    async_session = current_app.config["async_session"]
-    async with async_session() as db_session:
-        statement = select(PMC)
-        pmcs = (await db_session.execute(statement)).scalars().all()
-        return f"Database using {current_app.config['DATA_MODELS_FILE']} has 
{len(pmcs)} PMCs"
-
-
 @APP.route("/release/signatures/verify/<release_key>")
 @require(R.committer)
 async def root_release_signatures_verify(release_key: str) -> str:
@@ -485,12 +341,6 @@ async def root_pmc_list() -> list[dict]:
         ]
 
 
[email protected]("/secret")
-@require(R.committer)
-async def root_secret() -> str:
-    return "Secret stuff!"
-
-
 @APP.route("/user/keys/add", methods=["GET", "POST"])
 @require(R.committer)
 async def root_user_keys_add() -> str:
diff --git a/atr/server.py b/atr/server.py
index 83c938e..97314b3 100644
--- a/atr/server.py
+++ b/atr/server.py
@@ -29,6 +29,7 @@ import asfquart
 import asfquart.generics
 import asfquart.session
 from asfquart.base import QuartApp
+from atr.blueprints import register_blueprints
 
 from .models import __file__ as data_models_file
 
@@ -115,12 +116,15 @@ def create_app() -> QuartApp:
             await conn.run_sync(SQLModel.metadata.create_all)
 
         app.config["engine"] = engine
+        # TODO: apply this only for debug
+        app.config["TEMPLATES_AUTO_RELOAD"] = True
 
     @app.after_serving
     async def shutdown() -> None:
         app.background_tasks.clear()
 
     register_routes()
+    register_blueprints(app)
 
     return app
 
diff --git a/atr/static/css/atr.css b/atr/static/css/atr.css
index 671dab5..f429fe6 100644
--- a/atr/static/css/atr.css
+++ b/atr/static/css/atr.css
@@ -66,7 +66,7 @@ ul { padding-left: 1rem; }
 
 label { font-weight: 500; border-bottom: 1px dashed #d1d2d3; padding-bottom: 
0.5rem; }
 
-form { background-color: #ffe; border: 1px solid #ddb; padding: 1rem; 
border-radius: 0.5rem; }
+form.striking { background-color: #ffe; border: 1px solid #ddb; padding: 1rem; 
border-radius: 0.5rem; }
 
 footer {
   padding: 2rem;
diff --git a/atr/templates/add-release-candidate.html 
b/atr/templates/add-release-candidate.html
index 84c1883..6186473 100644
--- a/atr/templates/add-release-candidate.html
+++ b/atr/templates/add-release-candidate.html
@@ -90,7 +90,7 @@
 
   <h2>Select files</h2>
 
-  <form method="post" enctype="multipart/form-data">
+  <form method="post" enctype="multipart/form-data" class="striking">
     <table class="form-table">
       <tbody>
         <tr>
diff --git a/atr/templates/data-browser.html b/atr/templates/data-browser.html
index 7643704..61866bc 100644
--- a/atr/templates/data-browser.html
+++ b/atr/templates/data-browser.html
@@ -70,7 +70,7 @@
 
   <div class="model-nav">
     {% for model_name in models %}
-      <a href="{{ url_for('root_admin_database', model=model_name) }}"
+      <a href="{{ url_for('secret_blueprint.secret_data', 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
index 787462a..4fc6127 100644
--- a/atr/templates/pages.html
+++ b/atr/templates/pages.html
@@ -83,7 +83,7 @@
         <h3>
           <a href="{{ url_for('root') }}">/</a>
         </h3>
-        <div class="endpoint-description">Simple welcome page with Apache 
Trusted Releases title.</div>
+        <div class="endpoint-description">Main welcome page.</div>
         <div class="endpoint-meta">
           Access: <span class="access-requirement public">Public</span>
         </div>
@@ -130,14 +130,6 @@
           Access: <span class="access-requirement public">Public</span>
         </div>
       </div>
-
-      <div class="endpoint">
-        <h3>/pmc/create/&lt;project_name&gt;</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">
@@ -191,11 +183,11 @@
         <h3>
           <a href="{{ url_for('root_user_keys_delete') 
}}">/user/keys/delete</a>
         </h3>
-        <div class="endpoint-description">Debug endpoint to delete all GPG 
keys associated with your account.</div>
+        <div class="endpoint-description">Delete all GPG keys associated with 
your account.</div>
         <div class="endpoint-meta">
           Access: <span class="access-requirement committer">Committer</span>
           <br />
-          <span class="access-requirement warning">Warning: This is a debug 
endpoint that immediately deletes all your keys without confirmation!</span>
+          <span class="access-requirement warning">Warning: This will delete 
all your keys without confirmation!</span>
         </div>
       </div>
     </div>
@@ -205,7 +197,7 @@
 
       <div class="endpoint">
         <h3>
-          <a href="{{ url_for('root_admin_database') }}">/admin/database</a>
+          <a href="{{ url_for('secret_blueprint.secret_data') 
}}">/secret/data</a>
         </h3>
         <div class="endpoint-description">Browse all records in the 
database.</div>
         <div class="endpoint-meta">
@@ -218,7 +210,7 @@
 
       <div class="endpoint">
         <h3>
-          <a href="{{ url_for('root_admin_update_pmcs') 
}}">/admin/update-pmcs</a>
+          <a href="{{ url_for('secret_blueprint.secret_pmcs_update') 
}}">/secret/pmcs/update</a>
         </h3>
         <div class="endpoint-description">Update PMCs from remote, 
authoritative committee-info.json.</div>
         <div class="endpoint-meta">
@@ -229,30 +221,6 @@
         </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>
 
 {% endblock content %}
diff --git a/atr/templates/user-keys-add.html b/atr/templates/user-keys-add.html
index 7db121f..46b9741 100644
--- a/atr/templates/user-keys-add.html
+++ b/atr/templates/user-keys-add.html
@@ -188,7 +188,7 @@
     </div>
   {% endif %}
 
-  <form method="post">
+  <form method="post" class="striking">
     <div class="form-group">
       <label for="public_key">Public Key:</label>
       <textarea id="public_key"
diff --git a/typestubs/asfquart/auth.pyi b/typestubs/asfquart/auth.pyi
index 27c06a6..156cb6c 100644
--- a/typestubs/asfquart/auth.pyi
+++ b/typestubs/asfquart/auth.pyi
@@ -3,83 +3,82 @@ This type stub file was generated by pyright.
 """
 
 import typing
+from typing import Any, Callable, Coroutine, Iterable, Optional, TypeVar, 
Union, overload
 from . import base, session
 
 """ASFQuart - Authentication methods and decorators"""
 
+T = TypeVar('T')
+P = TypeVar('P', bound=Callable[..., Coroutine[Any, Any, Any]])
+ReqFunc = Callable[[session.ClientSession], tuple[bool, str]]
+
 class Requirements:
     """Various pre-defined access requirements"""
 
-    E_NOT_LOGGED_IN = ...
-    E_NOT_MEMBER = ...
-    E_NOT_CHAIR = ...
-    E_NO_MFA = ...
-    E_NOT_ROOT = ...
-    E_NOT_PMC = ...
-    E_NOT_ROLEACCOUNT = ...
+    E_NOT_LOGGED_IN: str
+    E_NOT_MEMBER: str
+    E_NOT_CHAIR: str
+    E_NO_MFA: str
+    E_NOT_ROOT: str
+    E_NOT_PMC: str
+    E_NOT_ROLEACCOUNT: str
+
     @classmethod
-    def mfa_enabled(cls, client_session: session.ClientSession):  # -> 
tuple[bool, LiteralString]:
+    def mfa_enabled(cls, client_session: session.ClientSession) -> tuple[bool, 
str]:
         """Tests for MFA enabled in the client session"""
         ...
+
     @classmethod
-    def committer(
-        cls, client_session: session.ClientSession
-    ):  # -> tuple[bool, Literal['You need to be logged in to access this 
endpoint.']]:
+    def committer(cls, client_session: session.ClientSession) -> tuple[bool, 
str]:
         """Tests for whether the user is a committer on any project"""
         ...
+
     @classmethod
-    def member(cls, client_session: session.ClientSession):  # -> tuple[bool, 
LiteralString]:
+    def member(cls, client_session: session.ClientSession) -> tuple[bool, str]:
         """Tests for whether the user is a foundation member"""
         ...
+
     @classmethod
-    def chair(cls, client_session: session.ClientSession):  # -> tuple[bool, 
LiteralString]:
+    def chair(cls, client_session: session.ClientSession) -> tuple[bool, str]:
         """tests for whether the user is a chair of any top-level project"""
         ...
+
     @classmethod
-    def root(cls, client_session: session.ClientSession):  # -> tuple[bool, 
LiteralString]:
+    def root(cls, client_session: session.ClientSession) -> tuple[bool, str]:
         """tests for whether the user is a member of infra-root"""
         ...
+
     @classmethod
-    def pmc_member(cls, client_session: session.ClientSession):  # -> 
tuple[bool, LiteralString]:
+    def pmc_member(cls, client_session: session.ClientSession) -> tuple[bool, 
str]:
         """tests for whether the user is a PMC member of any top-level 
project"""
         ...
+
     @classmethod
-    def roleacct(
-        cls, client_session: session.ClientSession
-    ):  # -> tuple[bool, Literal['This endpoint is only accessible to role 
accounts.']]:
+    def roleacct(cls, client_session: session.ClientSession) -> tuple[bool, 
str]:
         """tests for whether the user is a service account"""
         ...
 
 class AuthenticationFailed(base.ASFQuartException):
     def __init__(self, message: str = ..., errorcode: int = ...) -> None: ...
 
-def requirements_to_iter(args: typing.Any):  # -> list[Any] | Any | 
Iterable[Any]:
+def requirements_to_iter(args: Any) -> Iterable[Any]:
     """Converts any auth req args (single arg, list, tuple) to an iterable if 
not already one"""
     ...
 
+@overload
+def require(func: P) -> P: ...
+
+@overload
 def require(
-    func: typing.Optional[typing.Callable] = ...,
-    all_of: typing.Optional[typing.Iterable] = ...,
-    any_of: typing.Optional[typing.Iterable] = ...,
-):
-    """Adds authentication/authorization requirements to an endpoint. Can be a 
single requirement or a list
-    of requirements. By default, all requirements must be satisfied, though 
this can be made optional by
-    explicitly using the `all_of` or `any_of` keywords to specify optionality. 
Requirements must be part
-    of the asfquart.auth.Requirements class, which consists of the following 
test:
-
-    - mfa_enabled: The client must authenticate with a method that has MFA 
enabled
-    - committer: The client must be a committer
-    - member: The client must be a foundation member
-    - chair: The client must be a chair of a project
-
-    In addition, any endpoint decorated with @require will implicitly require 
ANY form of
-    authenticated session. This is mandatory and also works as a bare 
decorator.
-
-    Examples:
-        @require(Requirements.member)  # Require session, require ASF member
-        @require  # Require any authed session
-        @require({Requirements.mfa_enabled, Requirements.chair})  # Require 
any project chair with MFA-enabled session
-        @require(all_of=Requirements.mfa_enabled, any_of={Requirements.member, 
Requirements.chair})
-          # Require either ASF member OR project chair, but also require MFA 
enabled in any case.
-    """
-    ...
+    func: Optional[ReqFunc] = None,
+    all_of: Optional[Union[ReqFunc, Iterable[ReqFunc]]] = None,
+    any_of: Optional[Union[ReqFunc, Iterable[ReqFunc]]] = None,
+) -> Callable[[P], P]: ...
+
+@overload
+def require(
+    func: Union[Callable[..., tuple[bool, str]], Iterable[Callable[..., 
tuple[bool, str]]]] = None,
+    *,
+    all_of: Optional[Union[Callable[..., tuple[bool, str]], 
Iterable[Callable[..., tuple[bool, str]]]]] = None,
+    any_of: Optional[Union[Callable[..., tuple[bool, str]], 
Iterable[Callable[..., tuple[bool, str]]]]] = None,
+) -> Callable[[P], P]: ...


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to