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 f5a9529  Remove unused functions and reduce duplication
f5a9529 is described below

commit f5a95297487782f2acd6f51d8881641d472bed6b
Author: Sean B. Palmer <s...@miscoranda.com>
AuthorDate: Thu Mar 6 17:29:46 2025 +0200

    Remove unused functions and reduce duplication
---
 atr/routes/__init__.py  |  35 +++++++++++
 atr/routes/candidate.py |  37 +-----------
 atr/routes/download.py  | 154 +-----------------------------------------------
 atr/routes/package.py   |  34 +----------
 atr/routes/release.py   |  21 +------
 5 files changed, 40 insertions(+), 241 deletions(-)

diff --git a/atr/routes/__init__.py b/atr/routes/__init__.py
index e2763af..85a3524 100644
--- a/atr/routes/__init__.py
+++ b/atr/routes/__init__.py
@@ -20,13 +20,16 @@ import functools
 import logging
 import time
 from collections.abc import Awaitable, Callable, Coroutine
+from pathlib import Path
 from typing import Any, ParamSpec, TypeVar
 
 import aiofiles
+import aiofiles.os
 from quart import Request
 from werkzeug.datastructures import MultiDict
 
 from asfquart import APP
+from atr.db.models import Package
 
 if APP is ...:
     raise RuntimeError("APP is not set")
@@ -251,6 +254,25 @@ def app_route_performance_measure(route_path: str, 
http_methods: list[str] | Non
     return decorator
 
 
+def format_file_size(size_in_bytes: int) -> str:
+    """Format a file size with appropriate units and comma-separated digits."""
+    # Format the raw bytes with commas
+    formatted_bytes = f"{size_in_bytes:,}"
+
+    # Calculate the appropriate unit
+    if size_in_bytes >= 1_000_000_000:
+        size_in_gb = size_in_bytes // 1_000_000_000
+        return f"{size_in_gb:,} GB ({formatted_bytes} bytes)"
+    elif size_in_bytes >= 1_000_000:
+        size_in_mb = size_in_bytes // 1_000_000
+        return f"{size_in_mb:,} MB ({formatted_bytes} bytes)"
+    elif size_in_bytes >= 1_000:
+        size_in_kb = size_in_bytes // 1_000
+        return f"{size_in_kb:,} KB ({formatted_bytes} bytes)"
+    else:
+        return f"{formatted_bytes} bytes"
+
+
 async def get_form(request: Request) -> MultiDict:
     # The request.form() method in Quart calls a synchronous tempfile method
     # It calls quart.wrappers.request.form _load_form_data
@@ -274,3 +296,16 @@ async def get_form(request: Request) -> MultiDict:
     if blockbuster is not None:
         blockbuster.activate()
     return form
+
+
+async def package_files_delete(package: Package, uploads_path: Path) -> None:
+    """Delete the artifact and signature files associated with a package."""
+    if package.artifact_sha3:
+        artifact_path = uploads_path / package.artifact_sha3
+        if await aiofiles.os.path.exists(artifact_path):
+            await aiofiles.os.remove(artifact_path)
+
+    if package.signature_sha3:
+        signature_path = uploads_path / package.signature_sha3
+        if await aiofiles.os.path.exists(signature_path):
+            await aiofiles.os.remove(signature_path)
diff --git a/atr/routes/candidate.py b/atr/routes/candidate.py
index ee7529a..0934c50 100644
--- a/atr/routes/candidate.py
+++ b/atr/routes/candidate.py
@@ -17,12 +17,8 @@
 
 """candidate.py"""
 
-import asyncio
 import datetime
 import secrets
-import shutil
-import tempfile
-from contextlib import asynccontextmanager
 from typing import cast
 
 from quart import Request, redirect, render_template, request, url_for
@@ -46,43 +42,12 @@ from atr.db.models import (
     ReleaseStage,
     Task,
 )
-from atr.routes import app_route, get_form
+from atr.routes import app_route, format_file_size, get_form
 
 if APP is ...:
     raise RuntimeError("APP is not set")
 
 
-@asynccontextmanager
-async def ephemeral_gpg_home():
-    """Create a temporary directory for an isolated GPG home, and clean it up 
on exit."""
-    # TODO: This is only used in key_user_add
-    # We could even inline it there
-    temp_dir = await asyncio.to_thread(tempfile.mkdtemp, prefix="gpg-")
-    try:
-        yield temp_dir
-    finally:
-        await asyncio.to_thread(shutil.rmtree, temp_dir)
-
-
-def format_file_size(size_in_bytes: int) -> str:
-    """Format a file size with appropriate units and comma-separated digits."""
-    # Format the raw bytes with commas
-    formatted_bytes = f"{size_in_bytes:,}"
-
-    # Calculate the appropriate unit
-    if size_in_bytes >= 1_000_000_000:
-        size_in_gb = size_in_bytes // 1_000_000_000
-        return f"{size_in_gb:,} GB ({formatted_bytes} bytes)"
-    elif size_in_bytes >= 1_000_000:
-        size_in_mb = size_in_bytes // 1_000_000
-        return f"{size_in_mb:,} MB ({formatted_bytes} bytes)"
-    elif size_in_bytes >= 1_000:
-        size_in_kb = size_in_bytes // 1_000
-        return f"{size_in_kb:,} KB ({formatted_bytes} bytes)"
-    else:
-        return f"{formatted_bytes} bytes"
-
-
 def format_artifact_name(project_name: str, product_name: str, version: str, 
is_podling: bool = False) -> str:
     """Format an artifact name according to Apache naming conventions.
 
diff --git a/atr/routes/download.py b/atr/routes/download.py
index defc080..c8e0749 100644
--- a/atr/routes/download.py
+++ b/atr/routes/download.py
@@ -17,15 +17,6 @@
 
 """download.py"""
 
-import asyncio
-import datetime
-import hashlib
-import pprint
-import secrets
-import shutil
-import tempfile
-from collections.abc import AsyncGenerator
-from contextlib import asynccontextmanager
 from pathlib import Path
 from typing import cast
 
@@ -33,163 +24,22 @@ import aiofiles
 import aiofiles.os
 from quart import flash, redirect, send_file, url_for
 from quart.wrappers.response import Response as QuartResponse
-from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.orm import selectinload
 from sqlalchemy.orm.attributes import InstrumentedAttribute
 from sqlmodel import select
-from werkzeug.datastructures import FileStorage
 from werkzeug.wrappers.response import Response
 
 from asfquart.auth import Requirements, require
 from asfquart.base import ASFQuartException
-from asfquart.session import ClientSession
 from asfquart.session import read as session_read
 from atr.db import create_async_db_session
 from atr.db.models import (
     PMC,
     Package,
-    PMCKeyLink,
-    PublicSigningKey,
     Release,
 )
-from atr.routes import FlashError, app_route
-from atr.util import compute_sha512, get_release_storage_dir
-
-
-@asynccontextmanager
-async def ephemeral_gpg_home() -> AsyncGenerator[str]:
-    """Create a temporary directory for an isolated GPG home, and clean it up 
on exit."""
-    # TODO: This is only used in key_user_add
-    # We could even inline it there
-    temp_dir = await asyncio.to_thread(tempfile.mkdtemp, prefix="gpg-")
-    try:
-        yield temp_dir
-    finally:
-        await asyncio.to_thread(shutil.rmtree, temp_dir)
-
-
-async def file_hash_save(base_dir: Path, file: FileStorage) -> tuple[str, int]:
-    """
-    Save a file using its SHA3-256 hash as the filename.
-    Returns the hash and size in bytes of the saved file.
-    """
-    sha3 = hashlib.sha3_256()
-    total_bytes = 0
-
-    # Create temporary file to stream to while computing hash
-    temp_path = base_dir / f"temp-{secrets.token_hex(8)}"
-    try:
-        stream = file.stream
-
-        async with aiofiles.open(temp_path, "wb") as f:
-            while True:
-                chunk = await asyncio.to_thread(stream.read, 8192)
-                if not chunk:
-                    break
-                sha3.update(chunk)
-                total_bytes += len(chunk)
-                await f.write(chunk)
-
-        file_hash = sha3.hexdigest()
-        final_path = base_dir / file_hash
-
-        # Only move to final location if it doesn't exist
-        # This can race, but it's hash based so it's okay
-        if not await aiofiles.os.path.exists(final_path):
-            await aiofiles.os.rename(temp_path, final_path)
-        else:
-            # If file already exists, just remove the temp file
-            await aiofiles.os.remove(temp_path)
-
-        return file_hash, total_bytes
-    except Exception as e:
-        if await aiofiles.os.path.exists(temp_path):
-            await aiofiles.os.remove(temp_path)
-        raise e
-
-
-async def key_user_session_add(
-    session: ClientSession,
-    public_key: str,
-    key: dict,
-    selected_pmcs: list[str],
-    db_session: AsyncSession,
-) -> dict | None:
-    # TODO: Check if key already exists
-    # psk_statement = 
select(PublicSigningKey).where(PublicSigningKey.apache_uid == session.uid)
-
-    # # If uncommented, this will prevent a user from adding a second key
-    # existing_key = (await db_session.execute(statement)).scalar_one_or_none()
-    # if existing_key:
-    #     return ("You already have a key registered", None)
-
-    if not session.uid:
-        raise FlashError("You must be signed in to add a key")
-
-    fingerprint = key.get("fingerprint")
-    if not isinstance(fingerprint, str):
-        raise FlashError("Invalid key fingerprint")
-    fingerprint = fingerprint.lower()
-    uids = key.get("uids")
-    async with db_session.begin():
-        # Create new key record
-        key_record = PublicSigningKey(
-            fingerprint=fingerprint,
-            algorithm=int(key["algo"]),
-            length=int(key.get("length", "0")),
-            created=datetime.datetime.fromtimestamp(int(key["date"])),
-            expires=datetime.datetime.fromtimestamp(int(key["expires"])) if 
key.get("expires") else None,
-            declared_uid=uids[0] if uids else None,
-            apache_uid=session.uid,
-            ascii_armored_key=public_key,
-        )
-        db_session.add(key_record)
-
-        # Link key to selected PMCs
-        for pmc_name in selected_pmcs:
-            pmc_statement = select(PMC).where(PMC.project_name == pmc_name)
-            pmc = (await 
db_session.execute(pmc_statement)).scalar_one_or_none()
-            if pmc and pmc.id:
-                link = PMCKeyLink(pmc_id=pmc.id, 
key_fingerprint=key_record.fingerprint)
-                db_session.add(link)
-            else:
-                # TODO: Log? Add to "error"?
-                continue
-
-    return {
-        "key_id": key["keyid"],
-        "fingerprint": key["fingerprint"].lower() if key.get("fingerprint") 
else "Unknown",
-        "user_id": key["uids"][0] if key.get("uids") else "Unknown",
-        "creation_date": datetime.datetime.fromtimestamp(int(key["date"])),
-        "expiration_date": 
datetime.datetime.fromtimestamp(int(key["expires"])) if key.get("expires") else 
None,
-        "data": pprint.pformat(key),
-    }
-
-
-# Package functions
-
-
-async def package_add_artifact_info_get(
-    db_session: AsyncSession, uploads_path: Path, artifact_file: FileStorage
-) -> tuple[str, str, int]:
-    """Get artifact information during package addition process.
-
-    Returns a tuple of (sha3_hash, sha512_hash, size) for the artifact file.
-    Validates that the artifact hasn't already been uploaded to another 
release.
-    """
-    # In a separate function to appease the complexity checker
-    artifact_sha3, artifact_size = await file_hash_save(uploads_path, 
artifact_file)
-
-    # Check for duplicates by artifact_sha3 before proceeding
-    package_statement = select(Package).where(Package.artifact_sha3 == 
artifact_sha3)
-    duplicate = (await db_session.execute(package_statement)).first()
-    if duplicate:
-        # Remove the saved file since we won't be using it
-        await aiofiles.os.remove(uploads_path / artifact_sha3)
-        raise FlashError("This exact file has already been uploaded to another 
release")
-
-    # Compute SHA-512 of the artifact for the package record
-    return artifact_sha3, await compute_sha512(uploads_path / artifact_sha3), 
artifact_size
+from atr.routes import app_route
+from atr.util import get_release_storage_dir
 
 
 @app_route("/download/<release_key>/<artifact_sha3>")
diff --git a/atr/routes/package.py b/atr/routes/package.py
index 8358f01..f6c7ac5 100644
--- a/atr/routes/package.py
+++ b/atr/routes/package.py
@@ -50,7 +50,7 @@ from atr.db.models import (
     Task,
     TaskStatus,
 )
-from atr.routes import FlashError, app_route, get_form
+from atr.routes import FlashError, app_route, format_file_size, get_form, 
package_files_delete
 from atr.util import compute_sha512, get_release_storage_dir
 
 
@@ -94,25 +94,6 @@ async def file_hash_save(base_dir: Path, file: FileStorage) 
-> tuple[str, int]:
         raise e
 
 
-def format_file_size(size_in_bytes: int) -> str:
-    """Format a file size with appropriate units and comma-separated digits."""
-    # Format the raw bytes with commas
-    formatted_bytes = f"{size_in_bytes:,}"
-
-    # Calculate the appropriate unit
-    if size_in_bytes >= 1_000_000_000:
-        size_in_gb = size_in_bytes // 1_000_000_000
-        return f"{size_in_gb:,} GB ({formatted_bytes} bytes)"
-    elif size_in_bytes >= 1_000_000:
-        size_in_mb = size_in_bytes // 1_000_000
-        return f"{size_in_mb:,} MB ({formatted_bytes} bytes)"
-    elif size_in_bytes >= 1_000:
-        size_in_kb = size_in_bytes // 1_000
-        return f"{size_in_kb:,} KB ({formatted_bytes} bytes)"
-    else:
-        return f"{formatted_bytes} bytes"
-
-
 # Package functions
 
 
@@ -260,19 +241,6 @@ async def package_data_get(db_session: AsyncSession, 
artifact_sha3: str, release
     return package
 
 
-async def package_files_delete(package: Package, uploads_path: Path) -> None:
-    """Delete the artifact and signature files associated with a package."""
-    if package.artifact_sha3:
-        artifact_path = uploads_path / package.artifact_sha3
-        if await aiofiles.os.path.exists(artifact_path):
-            await aiofiles.os.remove(artifact_path)
-
-    if package.signature_sha3:
-        signature_path = uploads_path / package.signature_sha3
-        if await aiofiles.os.path.exists(signature_path):
-            await aiofiles.os.remove(signature_path)
-
-
 # Release functions
 
 
diff --git a/atr/routes/release.py b/atr/routes/release.py
index 0176530..78967f3 100644
--- a/atr/routes/release.py
+++ b/atr/routes/release.py
@@ -22,8 +22,6 @@ import logging.handlers
 from pathlib import Path
 from typing import cast
 
-import aiofiles
-import aiofiles.os
 from quart import flash, redirect, render_template, request, url_for
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.orm import selectinload
@@ -38,34 +36,17 @@ from asfquart.session import read as session_read
 from atr.db import create_async_db_session
 from atr.db.models import (
     PMC,
-    Package,
     Release,
     Task,
     TaskStatus,
 )
-from atr.routes import FlashError, app_route, get_form
+from atr.routes import FlashError, app_route, get_form, package_files_delete
 from atr.util import get_release_storage_dir
 
 if APP is ...:
     raise RuntimeError("APP is not set")
 
 
-# Package functions
-
-
-async def package_files_delete(package: Package, uploads_path: Path) -> None:
-    """Delete the artifact and signature files associated with a package."""
-    if package.artifact_sha3:
-        artifact_path = uploads_path / package.artifact_sha3
-        if await aiofiles.os.path.exists(artifact_path):
-            await aiofiles.os.remove(artifact_path)
-
-    if package.signature_sha3:
-        signature_path = uploads_path / package.signature_sha3
-        if await aiofiles.os.path.exists(signature_path):
-            await aiofiles.os.remove(signature_path)
-
-
 # Release functions
 
 


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@tooling.apache.org
For additional commands, e-mail: commits-h...@tooling.apache.org

Reply via email to