diff --git a/app_python/.gitignore b/app_python/.gitignore new file mode 100644 index 0000000000..4de420a8f7 --- /dev/null +++ b/app_python/.gitignore @@ -0,0 +1,12 @@ +# Python +__pycache__/ +*.py[cod] +venv/ +*.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store \ No newline at end of file diff --git a/app_python/README.md b/app_python/README.md new file mode 100644 index 0000000000..cee4fcf5c0 --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,74 @@ +# DevOps Info Service + +## Overview + +Web service that reports system information and health status. Provides API endpoints for service info, hostname, platform, uptime, and request details. + +## Prerequisites + +- Python 3.11+ +- pip + +## Installation + +```bash +python -m venv venv +venv\Scripts\activate +pip install -r requirements.txt +``` + +## Running + +```bash +python app.py +``` + +Default: `http://0.0.0.0:5000` + +**Custom config:** + +```bash +PORT=8080 python app.py +HOST=127.0.0.1 PORT=3000 python app.py +``` + +## API Endpoints + +### `GET /` + +Returns service and system information. + +```bash +curl http://localhost:5000/ +``` + +### `GET /health` + +Health check endpoint. + +```bash +curl http://localhost:5000/health +``` + +## Configuration + +| Variable | Default | Description | +| -------- | --------- | ------------ | +| `HOST` | `0.0.0.0` | Host address | +| `PORT` | `5000` | Port number | +| `DEBUG` | `False` | Debug mode | + +## Project Structure + +``` +app_python/ +├── app.py # Main app +├── config.py # Config +├── routes/ # API routes +├── services/ # Business logic +├── tests/ +├── docs/ # Lab docs, screenshots +├── requirements.txt +├── .gitignore +└── README.md +``` diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 0000000000..4f29723835 --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,42 @@ +import os +import logging +from fastapi import FastAPI, Request, HTTPException +from fastapi.responses import JSONResponse +from routes.system_info import router as system_info_router +from config import HOST, PORT, DEBUG +import uvicorn + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +app = FastAPI() + +app.include_router(system_info_router) + +@app.exception_handler(404) +async def not_found(request: Request, exc: HTTPException): + return JSONResponse( + status_code=404, + content={ + "error": "Not Found", + "message": "Endpoint does not exist" + } + ) + +@app.exception_handler(Exception) +async def internal_error(request: Request, exc: Exception): + return JSONResponse( + status_code=500, + content={ + "error": "Internal Server Error", + "message": "An unexpected error occurred" + } + ) + +if __name__ == "__main__": + logger.info(f"Application started on {HOST}:{PORT}") + uvicorn.run(app, host=HOST, port=PORT) + diff --git a/app_python/config.py b/app_python/config.py new file mode 100644 index 0000000000..6cb4e62bcd --- /dev/null +++ b/app_python/config.py @@ -0,0 +1,5 @@ +import os + +HOST = os.getenv('HOST', '0.0.0.0') +PORT = int(os.getenv('PORT', 5000)) +DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' \ No newline at end of file diff --git a/app_python/docs/LAB01.md b/app_python/docs/LAB01.md new file mode 100644 index 0000000000..62bf8c4e7a --- /dev/null +++ b/app_python/docs/LAB01.md @@ -0,0 +1,67 @@ +# Lab 1 Submission + +## Framework Selection + +**FastAPI** - Modern, fast, has auto-docs and async support. + +| Criteria | FastAPI | Flask | Django | +| --------- | --------- | ---------- | ------ | +| Speed | Very Fast | Fast | Medium | +| Auto Docs | Yes | No | No | +| Async | Yes | Limited | Yes | +| Size | Small | Very Small | Large | + +**Why not Flask?** Flask is simpler but FastAPI has better async and auto-docs. + +**Why not Django?** Too big for this simple service. + +## Best Practices Applied + +1. **Code Organization** - Separated into routes/, services/, config.py +2. **Error Handling** - 404 and 500 handlers +3. **Logging** - Basic logging setup +4. **Environment Variables** - Config via env vars +5. **Clear Function Names** - get_system_info(), get_uptime(), etc. + +## API Documentation + +### `GET /` + +Returns service info, system info, runtime, request details, and endpoints list. + +```bash +curl http://localhost:5000/ +``` + +### `GET /health` + +Returns health status and uptime. + +```bash +curl http://localhost:5000/health +``` + +### Error Responses + +```bash +curl http://localhost:5000/something +# {"error": "Not Found", "message": "Endpoint does not exist"} +``` + +## Testing Evidence + +Screenshots in `docs/screenshots/`: + +- `01-main-endpoint.png` +- `02-health-check.png` +- `03-formatted-output.png` + +## Challenges & Solutions + +1. **Function name conflict** — Named the route handler `get_system_info()` which conflicted with the imported function from `services.system_info`. When calling `get_system_info()` inside the route handler, Python was calling the route handler itself instead of the imported function, causing recursion errors. Fixed by importing the entire module as `import services.system_info as system_info_service` and accessing functions via `system_info_service.get_system_info()`. + +2. **Timezone method call error** — Used `timezone.utc.tzname()` without arguments, but `tzname()` method requires a datetime object as parameter. This caused `TypeError: timezone.tzname() takes exactly one argument (0 given)`. Fixed by calling `tzname()` on a datetime object: `datetime.now(timezone.utc).tzname()`. + +## GitHub Community + +\*Starring repositories helps with discovery and bookmarking — it signals project quality to the community and encourages maintainers. Following developers builds professional connections and keeps you informed about relevant projects and industry trends. diff --git a/app_python/docs/screenshots/01-main-endpoint.png b/app_python/docs/screenshots/01-main-endpoint.png new file mode 100644 index 0000000000..6d0a41efdd Binary files /dev/null and b/app_python/docs/screenshots/01-main-endpoint.png differ diff --git a/app_python/docs/screenshots/02-health-check.png b/app_python/docs/screenshots/02-health-check.png new file mode 100644 index 0000000000..88f95b7b67 Binary files /dev/null and b/app_python/docs/screenshots/02-health-check.png differ diff --git a/app_python/docs/screenshots/03-formatted-output.png b/app_python/docs/screenshots/03-formatted-output.png new file mode 100644 index 0000000000..0cca35f0bd Binary files /dev/null and b/app_python/docs/screenshots/03-formatted-output.png differ diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 0000000000..9abb353041 --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1,2 @@ +fastapi==0.115.0 +uvicorn[standard]==0.32.0 \ No newline at end of file diff --git a/app_python/routes/system_info.py b/app_python/routes/system_info.py new file mode 100644 index 0000000000..fc26d42cfe --- /dev/null +++ b/app_python/routes/system_info.py @@ -0,0 +1,24 @@ +from fastapi import APIRouter, Request +import logging +from datetime import datetime, timezone +import services.system_info as system_info_service + +router = APIRouter() + +@router.get("/") +async def get_system_info(request: Request): + return { + "service": system_info_service.get_service_info(), + "system": system_info_service.get_system_info(), + "runtime": system_info_service.get_runtime_info(), + "request": system_info_service.get_request_info(request), + "endpoints": system_info_service.get_endpoints_info() + } + +@router.get("/health") +async def health(): + return { + "status": "healthy", + "timestamp": datetime.now(timezone.utc).isoformat(), + "uptime_seconds": system_info_service.get_uptime()['seconds'] + } \ No newline at end of file diff --git a/app_python/services/system_info.py b/app_python/services/system_info.py new file mode 100644 index 0000000000..6465ae545f --- /dev/null +++ b/app_python/services/system_info.py @@ -0,0 +1,68 @@ +from fastapi import Request +import os +import platform +import socket +from datetime import datetime, timezone + +start_time = datetime.now(timezone.utc) + +def get_system_info(): + hostname = socket.gethostname() + platform_name = platform.system() + platform_version = platform.platform() + architecture = platform.machine() + cpu_count = os.cpu_count() + python_version = platform.python_version() + + return { + "hostname": hostname, + "platform": platform_name, + "platform_version": platform_version, + "architecture": architecture, + "cpu_count": cpu_count, + "python_version": python_version + } + +def get_uptime(): + delta = datetime.now(timezone.utc) - start_time + seconds = int(delta.total_seconds()) + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + + return { + 'seconds': seconds, + 'human': f"{hours} hours, {minutes} minutes" + } + +def get_runtime_info(): + uptime_info = get_uptime() + + return { + "uptime_seconds": uptime_info['seconds'], + "uptime_human": uptime_info['human'], + 'current_time': datetime.now(timezone.utc).isoformat(), + 'timezone': datetime.now(timezone.utc).tzname() + } + +def get_service_info(): + return { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "FastAPI" + } + +def get_request_info(request: Request): + return { + "client_ip": request.client.host, + "user_agent": request.headers.get('user-agent'), + "method": request.method, + "path": request.url.path + } + +def get_endpoints_info(): + return [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"} + ] + diff --git a/app_python/tests/__init__.py b/app_python/tests/__init__.py new file mode 100644 index 0000000000..d4839a6b14 --- /dev/null +++ b/app_python/tests/__init__.py @@ -0,0 +1 @@ +# Tests package