Chore: A few updates from code review etc.

This commit is contained in:
Andreas Motl
2025-01-18 21:36:21 +01:00
committed by Andreas Motl
parent b5723303c8
commit 1b63d2943a
6 changed files with 62 additions and 12 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
Responder - a familiar HTTP Service Framework.
This module exports the core functionality of the Responder framework,
including the API, Request, Response classes and CLI interface.
including the API, Request, and Response classes.
"""
from . import ext
+9 -1
View File
@@ -2,7 +2,6 @@ import os
from pathlib import Path
import uvicorn
from starlette.exceptions import ExceptionMiddleware
from starlette.middleware.cors import CORSMiddleware
from starlette.middleware.errors import ServerErrorMiddleware
from starlette.middleware.gzip import GZipMiddleware
@@ -11,6 +10,15 @@ from starlette.middleware.sessions import SessionMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
from starlette.testclient import TestClient
# Python 3.7+
try:
from starlette.middleware.exceptions import ExceptionMiddleware
# Python 3.6
except ImportError:
from starlette.exceptions import ( # type: ignore[attr-defined,no-redef]
ExceptionMiddleware,
)
from . import status_codes
from .background import BackgroundQueue
from .formats import get_formats
+3 -1
View File
@@ -69,12 +69,14 @@ def cli() -> None:
sys.exit(1)
npm_cmd = "npm.cmd" if platform.system() == "Windows" else "npm"
try:
# # S603, S607 are addressed by validating the target directory.
logger.info("Starting frontend asset build")
# S603, S607 are addressed by validating the target directory.
subprocess.check_call( # noqa: S603, S607
[npm_cmd, "run", "build"],
cwd=target_path,
timeout=300,
)
logger.info("Frontend asset build completed successfully")
except FileNotFoundError:
logger.error("npm not found. Please install Node.js and npm.")
sys.exit(1)
+40 -7
View File
@@ -4,6 +4,7 @@
# 1. Only execute the 'responder' binary from PATH
# 2. Validate all user inputs before passing to subprocess
# 3. Use Path.resolve() to prevent path traversal
import functools
import logging
import os
import shutil
@@ -20,10 +21,19 @@ logger = logging.getLogger(__name__)
class ResponderProgram:
"""
Provide full path to the `responder` program.
Utility class for managing Responder program execution.
This class provides methods for:
- Locating the responder executable in PATH
- Building frontend assets using npm
Example:
>>> program_path = ResponderProgram.path()
>>> build_status = ResponderProgram.build(Path("app_dir"))
"""
@staticmethod
@functools.lru_cache(maxsize=None)
def path():
name = "responder"
if sys.platform == "win32":
@@ -105,6 +115,13 @@ class ResponderServer(threading.Thread):
):
raise ValueError("limit_max_requests must be a positive integer if specified")
# Check if port is available.
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("localhost", port))
except OSError as ex:
raise ValueError(f"Port {port} is already in use") from ex
# Instance variables after validation.
self.target = target
self.port = port
@@ -114,6 +131,7 @@ class ResponderServer(threading.Thread):
# Allow the thread to be terminated when the main program exits.
self.process: subprocess.Popen
self.daemon = True
self._process_lock = threading.Lock()
# Setup signal handlers.
signal.signal(signal.SIGTERM, self._signal_handler)
@@ -134,19 +152,27 @@ class ResponderServer(threading.Thread):
if self.port is not None:
env["PORT"] = str(self.port)
self.process = subprocess.Popen(
command,
env=env,
universal_newlines=True,
)
with self._process_lock:
self.process = subprocess.Popen(
command,
env=env,
universal_newlines=True,
)
self.process.wait()
def stop(self):
"""
Gracefully stop the process.
Gracefully stop the process (API).
"""
if self._stopping:
return
with self._process_lock:
self._stop()
def _stop(self):
"""
Gracefully stop the process (impl).
"""
self._stopping = True
if self.process and self.process.poll() is None:
logger.info("Attempting to terminate server process...")
@@ -179,6 +205,7 @@ class ResponderServer(threading.Thread):
bool: True if server is ready and accepting connections, False otherwise.
"""
start_time = time.time()
last_error = None
while time.time() - start_time < timeout:
if not self.is_running():
if self.process is None:
@@ -198,8 +225,14 @@ class ResponderServer(threading.Thread):
socket.gaierror,
OSError,
) as ex:
last_error = ex
logger.debug(f"Server not ready yet: {ex}")
time.sleep(delay)
logger.error(
"Server failed to start within %d seconds. Last error: %s",
timeout,
last_error,
)
return False
def is_running(self):
+8 -1
View File
@@ -138,7 +138,14 @@ setup(
"graphql": ["graphene<3", "graphql-server-core>=1.2,<2"],
"openapi": ["apispec>=1.0.0"],
"release": ["build", "twine"],
"test": ["flask", "mypy", "pytest", "pytest-cov", "pytest-mock", "pytest-rerunfailures"],
"test": [
"flask",
"mypy",
"pytest",
"pytest-cov",
"pytest-mock",
"pytest-rerunfailures",
],
},
include_package_data=True,
license="Apache 2.0",
+1 -1
View File
@@ -7,7 +7,7 @@ This module tests the following CLI commands:
- responder run: Server execution
Requirements:
- The `docopt` package must be installed
- The `docopt-ng` package must be installed
- Example application must be present at `examples/helloworld.py`
- This file should implement a basic HTTP server with a "/hello" endpoint
that returns "hello, world!" as response