Skip to content

Commit

Permalink
Chore: A few updates from code review etc.
Browse files Browse the repository at this point in the history
  • Loading branch information
amotl committed Jan 18, 2025
1 parent a729c25 commit 6aaad8e
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 12 deletions.
2 changes: 1 addition & 1 deletion responder/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion responder/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
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
Expand All @@ -11,6 +10,15 @@
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
Expand Down
4 changes: 3 additions & 1 deletion responder/ext/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
47 changes: 40 additions & 7 deletions responder/util/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,10 +21,19 @@

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":
Expand Down Expand Up @@ -105,6 +115,13 @@ def __init__(self, target: str, port: int = 5042, limit_max_requests: int = None
):
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
Expand All @@ -114,6 +131,7 @@ def __init__(self, target: str, port: int = 5042, limit_max_requests: int = None
# 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)
Expand All @@ -134,19 +152,27 @@ def run(self):
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...")
Expand Down Expand Up @@ -179,6 +205,7 @@ def wait_until_ready(self, timeout=30, request_timeout=1, delay=0.1) -> bool:
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:
Expand All @@ -198,8 +225,14 @@ def wait_until_ready(self, timeout=30, request_timeout=1, delay=0.1) -> bool:
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):
Expand Down
9 changes: 8 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,14 @@ def run(self):
"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",
Expand Down
2 changes: 1 addition & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
- 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
Expand Down

0 comments on commit 6aaad8e

Please sign in to comment.