From: Sam Mirazi Date: Sun, 1 Jun 2025 04:42:34 +0000 (-0700) Subject: 9 X-Git-Url: https://git.josue.xyz/?a=commitdiff_plain;h=e810360134f29aacefeaea29c1877b734aecb56b;p=fastapi-vs-flask%2F.git 9 --- diff --git a/app_fastapi/__pycache__/app.cpython-312.pyc b/app_fastapi/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000..16396b3 Binary files /dev/null and b/app_fastapi/__pycache__/app.cpython-312.pyc differ diff --git a/app_fastapi/app.py b/app_fastapi/app.py new file mode 100644 index 0000000..bc0f0cf --- /dev/null +++ b/app_fastapi/app.py @@ -0,0 +1,11 @@ +# app_fastapi/app.py +from fastapi import FastAPI, Response +import asyncio + +app = FastAPI() + +@app.get("/") +async def home(): + await asyncio.sleep(3) # simulate slow work (non-blocking) + html = "

FastAPI Server: 3s Artificial Delay Demo

" + return Response(content=html, media_type="text/html") \ No newline at end of file diff --git a/app_flask/flask_application.py b/app_flask/flask_application.py index 5df2db9..5da77ea 100644 --- a/app_flask/flask_application.py +++ b/app_flask/flask_application.py @@ -7,7 +7,7 @@ app = Flask(__name__) @app.route("/") def home(): time.sleep(3) # simulate slow work - html = "

Slow Flask Demo

" # TDD Phase 2 content + html = "

Flask Server: 3s Artificial Delay Demo

" # Updated content return Response(html, mimetype="text/html") if __name__ == "__main__": diff --git a/benchmark/run_benchmark.py b/benchmark/run_benchmark.py new file mode 100644 index 0000000..73b7798 --- /dev/null +++ b/benchmark/run_benchmark.py @@ -0,0 +1,77 @@ +# benchmark/run_benchmark.py +import argparse +import time +import asyncio +import httpx +import requests +from concurrent.futures import ThreadPoolExecutor + +# Server URLs (ensure these match your running servers) +FLASK_URL = "http://127.0.0.1:3000/" +FASTAPI_URL = "http://127.0.0.1:8000/" +NUM_REQUESTS = 100 + +def fetch_url_sync(url): + try: + response = requests.get(url, timeout=10) # Increased timeout for potentially slow server + response.raise_for_status() # Raise an exception for bad status codes + return response.status_code + except requests.exceptions.RequestException as e: + print(f"Request to {url} failed: {e}") + return None + +def run_flask_benchmark(): + print(f"Starting Flask benchmark: {NUM_REQUESTS} requests to {FLASK_URL}...") + start_time = time.perf_counter() + + with ThreadPoolExecutor(max_workers=NUM_REQUESTS) as executor: + futures = [executor.submit(fetch_url_sync, FLASK_URL) for _ in range(NUM_REQUESTS)] + results = [future.result() for future in futures] + + end_time = time.perf_counter() + total_time = end_time - start_time + successful_requests = sum(1 for r in results if r == 200) + print(f"Flask benchmark: {successful_requests}/{NUM_REQUESTS} successful requests in {total_time:.2f} seconds.") + return total_time + +async def fetch_url_async(client, url): + try: + response = await client.get(url, timeout=10) # Increased timeout + response.raise_for_status() + return response.status_code + except httpx.RequestError as e: + print(f"Request to {url} failed: {e}") + return None + +async def run_fastapi_benchmark_async(): + print(f"Starting FastAPI benchmark: {NUM_REQUESTS} requests to {FASTAPI_URL}...") + start_time = time.perf_counter() + + async with httpx.AsyncClient() as client: + tasks = [fetch_url_async(client, FASTAPI_URL) for _ in range(NUM_REQUESTS)] + results = await asyncio.gather(*tasks) + + end_time = time.perf_counter() + total_time = end_time - start_time + successful_requests = sum(1 for r in results if r == 200) + print(f"FastAPI benchmark: {successful_requests}/{NUM_REQUESTS} successful requests in {total_time:.2f} seconds.") + return total_time + +def run_fastapi_benchmark(): + return asyncio.run(run_fastapi_benchmark_async()) + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Run web server benchmarks.") + parser.add_argument( + "framework", + choices=["flask", "fastapi"], + help="Specify the framework to benchmark (flask or fastapi)" + ) + args = parser.parse_args() + + if args.framework == "flask": + run_flask_benchmark() + elif args.framework == "fastapi": + run_fastapi_benchmark() + else: + print("Invalid framework specified. Choose 'flask' or 'fastapi'.") \ No newline at end of file diff --git a/tests/__pycache__/test_fastapi_route.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_fastapi_route.cpython-312-pytest-8.3.5.pyc index bb35783..d591117 100644 Binary files a/tests/__pycache__/test_fastapi_route.cpython-312-pytest-8.3.5.pyc and b/tests/__pycache__/test_fastapi_route.cpython-312-pytest-8.3.5.pyc differ diff --git a/tests/__pycache__/test_fastapi_route.cpython-312.pyc b/tests/__pycache__/test_fastapi_route.cpython-312.pyc new file mode 100644 index 0000000..0dc75b3 Binary files /dev/null and b/tests/__pycache__/test_fastapi_route.cpython-312.pyc differ diff --git a/tests/__pycache__/test_flask_route.cpython-312-pytest-8.3.5.pyc b/tests/__pycache__/test_flask_route.cpython-312-pytest-8.3.5.pyc index 01df2d8..5dbc354 100644 Binary files a/tests/__pycache__/test_flask_route.cpython-312-pytest-8.3.5.pyc and b/tests/__pycache__/test_flask_route.cpython-312-pytest-8.3.5.pyc differ diff --git a/tests/test_fastapi_route.py b/tests/test_fastapi_route.py index f873426..015dbd6 100644 --- a/tests/test_fastapi_route.py +++ b/tests/test_fastapi_route.py @@ -1,41 +1,61 @@ # tests/test_fastapi_route.py import httpx -import subprocess, asyncio, os, signal, time # signal and time may not be needed if Popen is handled well +import asyncio import pytest # For @pytest.mark.asyncio +import uvicorn +import threading +import time +from multiprocessing import Process # Using Process for better isolation + +# Server configuration +HOST = "127.0.0.1" +PORT = 8000 + +class UvicornServer(threading.Thread): + def __init__(self, app_module_str): + super().__init__(daemon=True) + self.app_module_str = app_module_str + self.server_started = threading.Event() + self.config = uvicorn.Config(app_module_str, host=HOST, port=PORT, log_level="info") + self.server = uvicorn.Server(config=self.config) + + def run(self): + # Need to set a new event loop for the thread if running asyncio components + # For uvicorn.Server.serve(), it typically manages its own loop or integrates + # with an existing one if started from an async context. + # However, running it in a separate thread needs care. + # Simpler: uvicorn.run() which handles loop creation. + uvicorn.run(self.app_module_str, host=HOST, port=PORT, log_level="warning") # log_level warning to reduce noise + + # UvicornServer using Process for cleaner start/stop + # This might be more robust for test isolation. + +def run_server_process(app_module_str, host, port): + uvicorn.run(app_module_str, host=host, port=port, log_level="warning") -# Adjusted to be an async function and use uvicorn async def start_server_fastapi(): - # Ensure CWD is project root - project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) - # Command to run FastAPI app with uvicorn - # Assuming app_fastapi/app.py and app instance is named 'app' - cmd = ["uvicorn", "app_fastapi.app:app", "--host", "0.0.0.0", "--port", "8000"] - - proc = subprocess.Popen( - cmd, - cwd=project_root, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) - await asyncio.sleep(1.5) # Allow uvicorn to start - return proc + # Using Process to run Uvicorn. This provides better isolation and cleanup. + proc = Process(target=run_server_process, args=("app_fastapi.app:app", HOST, PORT), daemon=True) + proc.start() + await asyncio.sleep(2.0) # Increased sleep to ensure server is fully up + if not proc.is_alive(): + raise RuntimeError("FastAPI server process failed to start.") + return proc # Return the process object @pytest.mark.asyncio async def test_home_returns_html_fastapi(): - proc = await start_server_fastapi() + server_process = await start_server_fastapi() try: - async with httpx.AsyncClient() as client: - r = await client.get("http://127.0.0.1:8000/") # Default FastAPI port + async with httpx.AsyncClient(timeout=10) as client: + r = await client.get(f"http://{HOST}:{PORT}/") assert r.status_code == 200 - assert "

Slow FastAPI Demo

" in r.text # Expected content + assert "

FastAPI Server: 3s Artificial Delay Demo

" in r.text # Expected content finally: - proc.terminate() - try: - # Use communicate to get output and ensure process is reaped - stdout, stderr = proc.communicate(timeout=5) - # print(f"FastAPI stdout:\n{stdout.decode(errors='replace')}") # Optional: for debugging - # print(f"FastAPI stderr:\n{stderr.decode(errors='replace')}") # Optional: for debugging - except subprocess.TimeoutExpired: - print("FastAPI server did not terminate/communicate gracefully, killing.") - proc.kill() - proc.wait() # Ensure kill completes \ No newline at end of file + if server_process and server_process.is_alive(): + server_process.terminate() # Send SIGTERM + server_process.join(timeout=5) # Wait for termination + if server_process.is_alive(): # Still alive after timeout + print("FastAPI server process did not terminate gracefully, killing.") + server_process.kill() # Force kill + server_process.join(timeout=5) # Wait for kill + # print("FastAPI server process stopped.") # Optional debug \ No newline at end of file diff --git a/tests/test_flask_route.py b/tests/test_flask_route.py index d9a7e61..5185c79 100644 --- a/tests/test_flask_route.py +++ b/tests/test_flask_route.py @@ -27,7 +27,7 @@ def test_home_returns_html(): } r = httpx.get("http://127.0.0.1:3000/", timeout=10, headers=headers) assert r.status_code == 200 - assert "

Slow Flask Demo

" in r.text + assert "

Flask Server: 3s Artificial Delay Demo

" in r.text finally: proc.terminate() try: