From 934d70650ebb3cbc802074774ba45c73ed4fc5be Mon Sep 17 00:00:00 2001 From: Jay Gaha Date: Wed, 16 Jul 2025 13:26:09 +0900 Subject: [PATCH] day #43 sanic sqlite --- workspace/7_framework/sanic/README.md | 75 ++- .../7_framework/sanic/day3_sqlite/README.md | 630 ++++++++++++++++++ .../sanic/day3_sqlite/app/__init__.py | 30 + .../sanic/day3_sqlite/app/config.py | 87 +++ .../sanic/day3_sqlite/app/database.py | 70 ++ .../sanic/day3_sqlite/app/error_handlers.py | 24 + .../sanic/day3_sqlite/app/routes.py | 71 ++ .../7_framework/sanic/day3_sqlite/main.py | 19 + .../sanic/day3_sqlite/requirements.txt | 3 + 9 files changed, 1000 insertions(+), 9 deletions(-) create mode 100644 workspace/7_framework/sanic/day3_sqlite/README.md create mode 100644 workspace/7_framework/sanic/day3_sqlite/app/__init__.py create mode 100644 workspace/7_framework/sanic/day3_sqlite/app/config.py create mode 100644 workspace/7_framework/sanic/day3_sqlite/app/database.py create mode 100644 workspace/7_framework/sanic/day3_sqlite/app/error_handlers.py create mode 100644 workspace/7_framework/sanic/day3_sqlite/app/routes.py create mode 100644 workspace/7_framework/sanic/day3_sqlite/main.py create mode 100644 workspace/7_framework/sanic/day3_sqlite/requirements.txt diff --git a/workspace/7_framework/sanic/README.md b/workspace/7_framework/sanic/README.md index a85bc38..fd4930e 100644 --- a/workspace/7_framework/sanic/README.md +++ b/workspace/7_framework/sanic/README.md @@ -78,6 +78,16 @@ sanic/ │ │ └── error_handlers.py │ ├── main.py │ └── README.md +├── day3_sqlite/ # Advanced Sanic with database integration +│ ├── app/ +│ │ ├── __init__.py +│ │ ├── routes.py +│ │ ├── database.py +│ │ ├── error_handlers.py +│ │ └── config.py +│ ├── main.py +│ ├── requirements.txt +│ └── README.md └── README.md # This file ``` @@ -120,15 +130,21 @@ async def handler(request): Sanic is designed for speed. Here's how it compares to other Python frameworks: -| Framework | Requests/sec | Relative Speed | -|-----------|-------------|----------------| -| Sanic | ~40,000 | 1.0x | -| FastAPI | ~30,000 | 0.75x | -| Flask | ~5,000 | 0.125x | -| Django | ~3,000 | 0.075x | +| Framework | Requests/sec | Relative Speed | Use Case | +|-----------|-------------|----------------|----------| +| Sanic | ~40,000 | 1.0x | High-performance APIs, real-time apps | +| FastAPI | ~30,000 | 0.75x | Modern APIs with auto-documentation | +| Flask | ~5,000 | 0.125x | Simple web apps, prototyping | +| Django | ~3,000 | 0.075x | Full-featured web applications | *Note: Performance varies based on application complexity and server configuration* +### Day 3 Performance Features +- **Database Connection Pooling**: Efficient async SQLite operations +- **HTTP Client Optimization**: Connection reuse and timeout management +- **Memory Management**: Proper resource cleanup and cursor handling +- **Configuration Caching**: Environment-based settings loaded once + ## When to Use Sanic ### Good For: @@ -150,8 +166,11 @@ Sanic is designed for speed. Here's how it compares to other Python frameworks: 3. **Practice Routing** - Try different route patterns 4. **Learn Intermediate Concepts** - Explore the `day2_intermediate/` directory 5. **Add Middleware** - Implement request/response processing -6. **Database Integration** - Connect to databases with async drivers -7. **Production Deployment** - Learn deployment strategies +6. **Master Advanced Features** - Explore the `day3_sqlite/` directory +7. **Database Integration** - Learn async SQLite operations and data persistence +8. **External Service Integration** - Make HTTP requests to external APIs +9. **Configuration Management** - Implement environment-based configuration +10. **Production Deployment** - Learn deployment strategies and monitoring ## Resources @@ -185,6 +204,16 @@ The examples in this directory use Sanic 22.3.0, which provides: - Application lifecycle listeners - Multi-worker deployment +### Day 3 - Advanced Features (`day3_sqlite/`) +- **Query Parameters**: Handle URL query parameters with validation +- **Typed Route Parameters**: Automatic type conversion and validation +- **JSON Validation**: Robust request body validation with detailed error messages +- **Async HTTP Client**: External API calls with httpx and comprehensive error handling +- **Async SQLite Integration**: Complete database operations with aiosqlite +- **Configuration Management**: Environment-based configuration with dataclasses +- **Enhanced Logging**: Structured logging throughout the application +- **Production Patterns**: Error handling, resource management, and security best practices + ## Getting Started 1. **Begin with Day 1**: Navigate to the `day1_basic/` directory @@ -193,6 +222,34 @@ The examples in this directory use Sanic 22.3.0, which provides: 4. **Progress to Day 2**: Navigate to the `day2_intermediate/` directory 5. Follow the setup instructions in `day2_intermediate/README.md` 6. Explore middleware, error handling, and blueprints -7. Experiment with the code to understand how it works +7. **Advance to Day 3**: Navigate to the `day3_sqlite/` directory +8. Follow the setup instructions in `day3_sqlite/README.md` +9. Learn database integration, external APIs, and advanced validation +10. Practice with query parameters, typed routes, and JSON validation +11. Experiment with the configuration system and logging +12. Test all endpoints and error handling scenarios + +## Tutorial Progression Comparison + +| Feature | Day 1 | Day 2 | Day 3 | +|---------|-------|-------|-------| +| **Routing** | Basic GET/POST | Blueprints | Query params, typed routes | +| **Responses** | Text, JSON | Custom headers | Enhanced validation | +| **Error Handling** | Basic exceptions | Centralized handlers | Comprehensive logging | +| **Architecture** | Simple structure | Middleware, listeners | Configuration management | +| **Data Storage** | In-memory | None | Async SQLite database | +| **External Services** | None | None | HTTP client with httpx | +| **Configuration** | Hardcoded | Basic setup | Environment-based | +| **Logging** | Print statements | Basic logging | Structured logging | +| **Production Ready** | Development only | Multi-worker | Full production patterns | +| **Complexity** | Beginner | Intermediate | Advanced | + +### Learning Path Summary + +- **Day 1**: Master the fundamentals of Sanic routing, responses, and async concepts +- **Day 2**: Learn application organization with middleware, blueprints, and error handling +- **Day 3**: Build production-ready applications with databases, external APIs, and proper configuration + +Each tutorial builds upon the previous one, creating a comprehensive learning experience that takes you from basic concepts to advanced production patterns. Happy coding with Sanic! diff --git a/workspace/7_framework/sanic/day3_sqlite/README.md b/workspace/7_framework/sanic/day3_sqlite/README.md new file mode 100644 index 0000000..7a793ba --- /dev/null +++ b/workspace/7_framework/sanic/day3_sqlite/README.md @@ -0,0 +1,630 @@ +# Sanic Advanced Tutorial — Day 3 + +An advanced Sanic web framework tutorial covering query parameters, typed route parameters, JSON validation, async HTTP client requests, and async SQLite database integration. + +**Prerequisites:** Complete Day 1 and Day 2 tutorials, understanding of async/await, basic SQL knowledge. + +--- + +## Project Structure + +``` +day3_sqlite/ +├── app/ +│ ├── __init__.py # App initialization with database lifecycle management +│ ├── routes.py # All route definitions with advanced features +│ ├── database.py # Async SQLite database connection and operations +│ ├── error_handlers.py # Enhanced error handling with logging +│ └── config.py # Configuration management system +├── main.py # Application entry point with detailed logging +├── requirements.txt # Dependencies (sanic, httpx, aiosqlite) +├── README.md # This file +└── app.db # SQLite database file (created automatically) +``` + +## Setup Instructions + +### Install dependencies: + +```bash +pip install -r requirements.txt +``` + +### Run the application: + +```bash +python main.py +``` + +The server will start on `http://localhost:8880` with comprehensive logging enabled. + +--- + +## New Advanced Features + +### 1. Query Parameters +Handle URL query parameters with validation and error handling. + +### 2. Typed Route Parameters +Use typed URL parameters for automatic type conversion and validation. + +### 3. JSON Validation +Robust JSON request body validation with detailed error messages. + +### 4. Async HTTP Client +Make external HTTP requests using httpx with proper error handling and timeouts. + +### 5. Async SQLite Integration +Complete database integration with aiosqlite for persistent data storage. + +### 6. Configuration Management +Centralized configuration system with environment variable support. + +### 7. Enhanced Logging +Comprehensive logging throughout the application for debugging and monitoring. + +--- + +## API Endpoints + +### 1. Search with Query Parameters +- **URL**: `/api/search?q=` +- **Method**: GET +- **Description**: Search functionality demonstrating query parameter handling +- **Query Parameters**: + - `q` (required) - Search keyword +- **Response**: `{"result": "You searched for: "}` +- **Error**: `{"error": "Missing query parameter 'q'"}` (400) if q is missing + +**Examples:** +```bash +# Successful search +curl "http://localhost:8880/api/search?q=python" + +# Missing parameter error +curl "http://localhost:8880/api/search" +``` + +### 2. Typed Route Parameters +- **URL**: `/api/square/` +- **Method**: GET +- **Description**: Calculate square of a number using typed route parameters +- **Parameters**: `number` (int) - Integer to square +- **Response**: `{"number": 5, "square": 25}` +- **Error**: 404 if parameter is not an integer + +**Examples:** +```bash +# Valid integer +curl http://localhost:8880/api/square/5 + +# Invalid type (returns 404) +curl http://localhost:8880/api/square/abc +``` + +### 3. Create Note (JSON Validation) +- **URL**: `/api/notes` +- **Method**: POST +- **Description**: Create a new note with JSON validation +- **Request Body**: `{"content": "Note content"}` +- **Response**: `{"message": "Note added!", "content": "Note content"}` +- **Validation**: Content must be present and non-empty after trimming + +**Examples:** +```bash +# Valid note creation +curl -X POST http://localhost:8880/api/notes \ + -H "Content-Type: application/json" \ + -d '{"content": "My first note"}' + +# Invalid JSON +curl -X POST http://localhost:8880/api/notes \ + -H "Content-Type: application/json" \ + -d 'invalid json' + +# Missing content +curl -X POST http://localhost:8880/api/notes \ + -H "Content-Type: application/json" \ + -d '{}' + +# Empty content +curl -X POST http://localhost:8880/api/notes \ + -H "Content-Type: application/json" \ + -d '{"content": " "}' +``` + +### 4. Get All Notes +- **URL**: `/api/notes` +- **Method**: GET +- **Description**: Retrieve all notes from the database +- **Response**: `{"notes": [{"id": 1, "content": "Note content"}]}` + +**Example:** +```bash +curl http://localhost:8880/api/notes +``` + +### 5. External IP Service +- **URL**: `/api/external-ip` +- **Method**: GET +- **Description**: Get external IP address using async HTTP client +- **Response**: `{"your_ip": "123.456.789.123"}` +- **Error Handling**: Timeout (504), HTTP errors (502), Service unavailable (503) + +**Example:** +```bash +curl http://localhost:8880/api/external-ip +``` + +--- + +## Code Features Demonstrated + +### 1. Query Parameter Handling +```python +@app.route("/api/search") +async def search(request): + keyword = request.args.get("q", None) + if not keyword: + return json({"error": "Missing query parameter 'q'"}, status=400) + return json({"result": f"You searched for: {keyword}"}) +``` + +**Features:** +- Access query parameters via `request.args.get()` +- Validation with custom error responses +- Default values and None handling + +### 2. Typed Route Parameters +```python +@app.route("/api/square/") +async def square(request, number): + return json({"number": number, "square": number ** 2}) +``` + +**Features:** +- Automatic type conversion from URL string to int +- Built-in validation (non-integers return 404) +- Direct use of typed parameter in handler + +### 3. JSON Validation +```python +@app.post("/api/notes") +async def create_note(request): + try: + data = await request.json() + except Exception: + raise InvalidUsage("Invalid JSON in request body") + + if not data or "content" not in data: + raise InvalidUsage("Missing 'content' in request body") + + content = data.get("content", "").strip() + if not content: + raise InvalidUsage("Content cannot be empty") +``` + +**Features:** +- JSON parsing with error handling +- Field presence validation +- Content validation (non-empty after trimming) +- Custom error messages using `InvalidUsage` + +### 4. Async HTTP Client +```python +@app.route("/api/external-ip") +async def external_ip(request): + try: + async with httpx.AsyncClient(timeout=10.0) as client: + response = await client.get("https://httpbin.org/ip") + response.raise_for_status() + data = response.json() + return json({"your_ip": data["origin"]}) + except httpx.TimeoutException: + return json({"error": "Request timeout"}, status=504) + except httpx.HTTPStatusError as e: + return json({"error": "External service error"}, status=502) + except Exception as e: + return json({"error": "Service unavailable"}, status=503) +``` + +**Features:** +- Async HTTP requests with httpx +- Timeout configuration +- Comprehensive error handling +- Proper status code mapping + +### 5. Async SQLite Integration +```python +class Database: + def __init__(self): + self.db = None + + async def connect(self): + self.db = await aiosqlite.connect('app.db') + await self.db.execute('CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, content TEXT)') + await self.db.commit() + + async def add_note(self, content): + await self.db.execute('INSERT INTO notes (content) VALUES (?)', (content,)) + await self.db.commit() + + async def get_notes(self): + cursor = await self.db.execute('SELECT id, content FROM notes') + notes = await cursor.fetchall() + await cursor.close() + return [{"id": row[0], "content": row[1]} for row in notes] +``` + +**Features:** +- Async database operations with aiosqlite +- Connection lifecycle management +- Parameterized queries for security +- Proper cursor handling +- Error handling and logging + +### 6. Configuration Management +```python +@dataclass +class AppConfig: + database: DatabaseConfig + server: ServerConfig + http_client: HTTPClientConfig + logging: LoggingConfig + + @classmethod + def from_env(cls) -> 'AppConfig': + return cls( + database=DatabaseConfig.from_env(), + server=ServerConfig.from_env(), + http_client=HTTPClientConfig.from_env(), + logging=LoggingConfig.from_env() + ) +``` + +**Features:** +- Dataclass-based configuration +- Environment variable support +- Type hints for better IDE support +- Modular configuration sections + +### 7. Enhanced Error Handling +```python +@app.exception(Exception) +async def server_error(request, exception): + logger.error(f"Server error: {str(exception)} - Path: {request.path}", exc_info=True) + return json({"error": "Internal server error"}, status=500) +``` + +**Features:** +- Comprehensive logging with stack traces +- Request path logging for debugging +- Different error types with appropriate responses +- Security (no internal details in production errors) + +--- + +## Database Schema + +The application uses a simple SQLite database with the following schema: + +```sql +CREATE TABLE notes ( + id INTEGER PRIMARY KEY, + content TEXT +); +``` + +**Features:** +- Auto-incrementing primary key +- Simple text content storage +- Created automatically on first run + +--- + +## Configuration Options + +The application supports environment-based configuration: + +### Database Configuration +- `DATABASE_PATH`: Path to SQLite database file (default: "app.db") +- `DATABASE_TIMEOUT`: Connection timeout in seconds (default: 30.0) + +### Server Configuration +- `SERVER_HOST`: Server host address (default: "0.0.0.0") +- `SERVER_PORT`: Server port number (default: 8880) +- `DEBUG`: Enable debug mode (default: "true") +- `ACCESS_LOG`: Enable access logging (default: "true") + +### HTTP Client Configuration +- `HTTP_TIMEOUT`: HTTP request timeout in seconds (default: 10.0) +- `HTTP_MAX_RETRIES`: Maximum retry attempts (default: 3) + +### Logging Configuration +- `LOG_LEVEL`: Logging level (default: "INFO") +- `LOG_FORMAT`: Log message format (default: timestamp, name, level, message) + +**Example Environment Setup:** +```bash +export DATABASE_PATH="/path/to/production.db" +export SERVER_PORT=8000 +export DEBUG=false +export LOG_LEVEL=WARNING +python main.py +``` + +--- + +## Testing the Application + +### 1. Test Query Parameters +```bash +# Valid search +curl "http://localhost:8880/api/search?q=python" + +# Test missing parameter +curl "http://localhost:8880/api/search" + +# Test with special characters +curl "http://localhost:8880/api/search?q=hello%20world" +``` + +### 2. Test Typed Parameters +```bash +# Valid integer +curl http://localhost:8880/api/square/42 + +# Test negative number +curl http://localhost:8880/api/square/-5 + +# Test invalid type +curl http://localhost:8880/api/square/abc +``` + +### 3. Test JSON Validation +```bash +# Create valid note +curl -X POST http://localhost:8880/api/notes \ + -H "Content-Type: application/json" \ + -d '{"content": "Test note"}' + +# Test invalid JSON +curl -X POST http://localhost:8880/api/notes \ + -H "Content-Type: application/json" \ + -d 'invalid' + +# Test missing content +curl -X POST http://localhost:8880/api/notes \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +### 4. Test Database Operations +```bash +# Create several notes +curl -X POST http://localhost:8880/api/notes \ + -H "Content-Type: application/json" \ + -d '{"content": "First note"}' + +curl -X POST http://localhost:8880/api/notes \ + -H "Content-Type: application/json" \ + -d '{"content": "Second note"}' + +# Retrieve all notes +curl http://localhost:8880/api/notes +``` + +### 5. Test External HTTP Client +```bash +# Test successful request +curl http://localhost:8880/api/external-ip + +# Test error handling (if httpbin.org is down) +curl http://localhost:8880/api/external-ip +``` + +--- + +## Key Learning Objectives + +After completing this tutorial, you should understand: + +### 1. Advanced Request Handling +- Query parameter validation and processing +- Typed route parameters with automatic conversion +- JSON request body validation and error handling + +### 2. Database Integration +- Async SQLite operations with aiosqlite +- Connection lifecycle management +- Parameterized queries for security +- Database schema creation and management + +### 3. External Service Integration +- Making async HTTP requests with httpx +- Error handling for network operations +- Timeout configuration and retry logic +- Status code mapping and error responses + +### 4. Configuration Management +- Environment-based configuration +- Type-safe configuration with dataclasses +- Modular configuration organization + +### 5. Production Considerations +- Comprehensive logging and monitoring +- Error handling and user-friendly error messages +- Security best practices (parameterized queries, no internal error exposure) +- Resource management and cleanup + +--- + +## Performance Considerations + +### Database Performance +- **Connection Pooling**: Single connection for simplicity (consider connection pooling for production) +- **Query Optimization**: Use indexes for frequently queried fields +- **Batch Operations**: Consider bulk inserts for high-throughput scenarios + +### HTTP Client Performance +- **Connection Reuse**: httpx automatically handles connection pooling +- **Timeout Configuration**: Appropriate timeouts prevent hanging requests +- **Error Handling**: Graceful degradation when external services fail + +### Memory Management +- **Cursor Cleanup**: Proper cursor closing prevents memory leaks +- **Connection Management**: Lifecycle listeners ensure proper cleanup +- **Logging**: Structured logging for better performance monitoring + +--- + +## Error Handling Patterns + +### 1. Validation Errors (400) +```python +if not keyword: + return json({"error": "Missing query parameter 'q'"}, status=400) +``` + +### 2. Client Errors (4xx) +```python +# Automatic 404 for invalid typed parameters +@app.route("/api/square/") +``` + +### 3. Server Errors (5xx) +```python +except Exception as e: + logger.error(f"Unexpected error: {e}") + return json({"error": "Service unavailable"}, status=503) +``` + +### 4. External Service Errors +```python +except httpx.TimeoutException: + return json({"error": "Request timeout"}, status=504) +except httpx.HTTPStatusError: + return json({"error": "External service error"}, status=502) +``` + +--- + +## Best Practices Demonstrated + +### 1. Input Validation +- Always validate query parameters +- Check JSON structure before processing +- Sanitize input data (strip whitespace) +- Provide meaningful error messages + +### 2. Database Operations +- Use parameterized queries +- Handle connection errors gracefully +- Implement proper resource cleanup +- Log database operations for debugging + +### 3. HTTP Client Usage +- Set appropriate timeouts +- Handle different types of HTTP errors +- Use context managers for resource management +- Implement retry logic where appropriate + +### 4. Configuration Management +- Use environment variables for configuration +- Provide sensible defaults +- Organize configuration logically +- Support type conversion + +### 5. Logging and Monitoring +- Log important events and errors +- Include context information (request path, user input) +- Use appropriate log levels +- Structure logs for easy parsing + +--- + +## Next Steps + +After mastering this tutorial, consider exploring: + +### 1. Advanced Database Features +- Connection pooling with asyncpg or similar +- Database migrations +- ORM integration (SQLAlchemy with async support) +- Database transactions and rollbacks + +### 2. Authentication and Authorization +- JWT token authentication +- Session management +- Role-based access control +- API key authentication + +### 3. Testing +- Unit tests for database operations +- Integration tests for API endpoints +- Mock external service calls +- Performance testing + +### 4. Production Deployment +- Docker containerization +- Environment-specific configuration +- Health checks and monitoring +- Load balancing and scaling + +### 5. Advanced Features +- WebSocket support for real-time features +- Background task processing +- Caching strategies +- Rate limiting and throttling + +--- + +## Troubleshooting + +### Common Issues + +#### Database Connection Problems +```python +# Check if database file is writable +# Verify aiosqlite installation +# Check database path configuration +``` + +#### JSON Parsing Errors +```python +# Verify Content-Type header +# Check JSON syntax +# Validate request body size +``` + +#### HTTP Client Timeouts +```python +# Check network connectivity +# Verify external service availability +# Adjust timeout configuration +``` + +#### Configuration Issues +```python +# Check environment variable names +# Verify type conversions +# Validate configuration values +``` + +### Debugging Tips + +1. **Enable Debug Mode**: Set `DEBUG=true` for detailed error messages +2. **Check Logs**: Monitor console output for error messages and warnings +3. **Test Endpoints**: Use curl or Postman to test individual endpoints +4. **Database Inspection**: Use SQLite browser to inspect database contents +5. **Network Testing**: Test external services independently + +--- + +## Conclusion + +This tutorial demonstrates advanced Sanic features including database integration, external service calls, and robust error handling. The combination of async operations, proper configuration management, and comprehensive logging creates a foundation for production-ready applications. + +The skills learned here are directly applicable to building scalable web APIs, microservices, and data-driven applications with Sanic. + +Happy coding with Sanic! \ No newline at end of file diff --git a/workspace/7_framework/sanic/day3_sqlite/app/__init__.py b/workspace/7_framework/sanic/day3_sqlite/app/__init__.py new file mode 100644 index 0000000..019a330 --- /dev/null +++ b/workspace/7_framework/sanic/day3_sqlite/app/__init__.py @@ -0,0 +1,30 @@ +""" +Initialize the app, error handlers, and database connection. +""" +import logging +from sanic import Sanic +from app import error_handlers, database +from app.config import config + +# Configure logging +logging.basicConfig( + level=getattr(logging, config.logging.level), + format=config.logging.format +) +logger = logging.getLogger(__name__) + +app = Sanic("SanicDBApp") + +error_handlers.register(app) + +@app.listener("before_server_start") +async def setup_db(app, loop): + logger.info("Setting up database connection...") + await database.connect() + +@app.listener("before_server_stop") +async def teardown_db(app, loop): + logger.info("Closing database connection...") + await database.disconnect() + +from app import routes diff --git a/workspace/7_framework/sanic/day3_sqlite/app/config.py b/workspace/7_framework/sanic/day3_sqlite/app/config.py new file mode 100644 index 0000000..1456138 --- /dev/null +++ b/workspace/7_framework/sanic/day3_sqlite/app/config.py @@ -0,0 +1,87 @@ +""" +Configuration settings for the Sanic Notes API application. +""" +import os +from dataclasses import dataclass +from typing import Optional + +@dataclass +class DatabaseConfig: + """Database configuration settings.""" + database_path: str = "app.db" + connection_timeout: float = 30.0 + + @classmethod + def from_env(cls) -> 'DatabaseConfig': + """Create database config from environment variables.""" + return cls( + database_path=os.getenv("DATABASE_PATH", "app.db"), + connection_timeout=float(os.getenv("DATABASE_TIMEOUT", "30.0")) + ) + +@dataclass +class ServerConfig: + """Server configuration settings.""" + host: str = "0.0.0.0" + port: int = 8880 + debug: bool = True + access_log: bool = True + + @classmethod + def from_env(cls) -> 'ServerConfig': + """Create server config from environment variables.""" + return cls( + host=os.getenv("SERVER_HOST", "0.0.0.0"), + port=int(os.getenv("SERVER_PORT", "8880")), + debug=os.getenv("DEBUG", "true").lower() == "true", + access_log=os.getenv("ACCESS_LOG", "true").lower() == "true" + ) + +@dataclass +class HTTPClientConfig: + """HTTP client configuration settings.""" + timeout: float = 10.0 + max_retries: int = 3 + + @classmethod + def from_env(cls) -> 'HTTPClientConfig': + """Create HTTP client config from environment variables.""" + return cls( + timeout=float(os.getenv("HTTP_TIMEOUT", "10.0")), + max_retries=int(os.getenv("HTTP_MAX_RETRIES", "3")) + ) + +@dataclass +class LoggingConfig: + """Logging configuration settings.""" + level: str = "INFO" + format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + + @classmethod + def from_env(cls) -> 'LoggingConfig': + """Create logging config from environment variables.""" + return cls( + level=os.getenv("LOG_LEVEL", "INFO"), + format=os.getenv("LOG_FORMAT", "%(asctime)s - %(name)s - %(levelname)s - %(message)s") + ) + +@dataclass +class AppConfig: + """Main application configuration.""" + database: DatabaseConfig + server: ServerConfig + http_client: HTTPClientConfig + logging: LoggingConfig + + @classmethod + def from_env(cls) -> 'AppConfig': + """Create application config from environment variables.""" + return cls( + database=DatabaseConfig.from_env(), + server=ServerConfig.from_env(), + http_client=HTTPClientConfig.from_env(), + logging=LoggingConfig.from_env() + ) + +# Global configuration instance +config = AppConfig.from_env() diff --git a/workspace/7_framework/sanic/day3_sqlite/app/database.py b/workspace/7_framework/sanic/day3_sqlite/app/database.py new file mode 100644 index 0000000..0b5e45f --- /dev/null +++ b/workspace/7_framework/sanic/day3_sqlite/app/database.py @@ -0,0 +1,70 @@ +""" +Simple async SQLite connection with a single table. +""" +import aiosqlite +import logging + +logger = logging.getLogger(__name__) + +class Database: + def __init__(self): + self.db = None + + async def connect(self): + try: + self.db = await aiosqlite.connect('app.db') + await self.db.execute('CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, content TEXT)') + await self.db.commit() + logger.info("Database connected successfully") + except Exception as e: + logger.error(f"Failed to connect to database: {e}") + raise + + async def disconnect(self): + if self.db: + try: + await self.db.close() + logger.info("Database disconnected successfully") + except Exception as e: + logger.error(f"Error disconnecting from database: {e}") + + async def add_note(self, content): + if not self.db: + raise RuntimeError("Database not connected") + + try: + await self.db.execute('INSERT INTO notes (content) VALUES (?)', (content,)) + await self.db.commit() + logger.info(f"Note added: {content[:50]}...") + except Exception as e: + logger.error(f"Failed to add note: {e}") + raise + + async def get_notes(self): + if not self.db: + raise RuntimeError("Database not connected") + + try: + cursor = await self.db.execute('SELECT id, content FROM notes') + notes = await cursor.fetchall() + await cursor.close() + return [{"id": row[0], "content": row[1]} for row in notes] + except Exception as e: + logger.error(f"Failed to get notes: {e}") + raise + +# Global database instance +db_instance = Database() + +# Convenience functions for backward compatibility +async def connect(): + await db_instance.connect() + +async def disconnect(): + await db_instance.disconnect() + +async def add_note(content): + await db_instance.add_note(content) + +async def get_notes(): + return await db_instance.get_notes() diff --git a/workspace/7_framework/sanic/day3_sqlite/app/error_handlers.py b/workspace/7_framework/sanic/day3_sqlite/app/error_handlers.py new file mode 100644 index 0000000..0f2290d --- /dev/null +++ b/workspace/7_framework/sanic/day3_sqlite/app/error_handlers.py @@ -0,0 +1,24 @@ +""" +Centralized error handler setup. +""" +from sanic.exceptions import NotFound, InvalidUsage +from sanic.response import json +import logging + +logger = logging.getLogger(__name__) + +def register(app): + @app.exception(NotFound) + async def not_found(request, exception): + logger.warning(f"Route not found: {request.path}") + return json({"error": "Route not found", "path": request.path}, status=404) + + @app.exception(InvalidUsage) + async def invalid_usage(request, exception): + logger.warning(f"Invalid usage: {str(exception)} - Path: {request.path}") + return json({"error": str(exception)}, status=400) + + @app.exception(Exception) + async def server_error(request, exception): + logger.error(f"Server error: {str(exception)} - Path: {request.path}", exc_info=True) + return json({"error": "Internal server error"}, status=500) diff --git a/workspace/7_framework/sanic/day3_sqlite/app/routes.py b/workspace/7_framework/sanic/day3_sqlite/app/routes.py new file mode 100644 index 0000000..1a1b3d7 --- /dev/null +++ b/workspace/7_framework/sanic/day3_sqlite/app/routes.py @@ -0,0 +1,71 @@ +""" +Routes for the notes application. +""" +from sanic.response import json +from sanic.exceptions import InvalidUsage +from app import app, database +import httpx +import logging + +logger = logging.getLogger(__name__) + +# Query parameters +@app.route("/api/search") +async def search(request): + keyword = request.args.get("q", None) + if not keyword: + return json({"error": "Missing query parameter 'q'"}, status=400) + + return json({"result": f"You searched for: {keyword}"}) + +# Typed URL parameter +@app.route("/api/square/") +async def square(request, number): + return json({"number": number, "square": number ** 2}) + +# JSON Validation +@app.post("/api/notes") +async def create_note(request): + try: + data = await request.json() + except Exception: + raise InvalidUsage("Invalid JSON in request body") + + if not data or "content" not in data: + raise InvalidUsage("Missing 'content' in request body") + + content = data.get("content", "").strip() + if not content: + raise InvalidUsage("Content cannot be empty") + + await database.add_note(content) + + return json({"message": "Note added!", "content": content}) + +@app.route("/api/notes") +async def get_notes(request): + notes = await database.get_notes() + + return json({"notes": notes}) + +# Async HTTP client call +@app.route("/api/external-ip") +async def external_ip(request): + try: + async with httpx.AsyncClient(timeout=10.0) as client: + response = await client.get("https://httpbin.org/ip") + response.raise_for_status() # Raise exception for bad status codes + data = response.json() + + logger.info(f"External IP request successful: {data.get('origin', 'Unknown')}") + return json({"your_ip": data["origin"]}) + + except httpx.TimeoutException: + logger.error("Timeout when requesting external IP") + return json({"error": "Request timeout"}, status=504) + except httpx.HTTPStatusError as e: + logger.error(f"HTTP error when requesting external IP: {e}") + return json({"error": "External service error"}, status=502) + except Exception as e: + logger.error(f"Unexpected error when requesting external IP: {e}") + return json({"error": "Service unavailable"}, status=503) diff --git a/workspace/7_framework/sanic/day3_sqlite/main.py b/workspace/7_framework/sanic/day3_sqlite/main.py new file mode 100644 index 0000000..8f14bad --- /dev/null +++ b/workspace/7_framework/sanic/day3_sqlite/main.py @@ -0,0 +1,19 @@ +""" +App entrypoint. +""" +import logging +from app import app + +logger = logging.getLogger(__name__) + +if __name__ == "__main__": + logger.info("Starting Sanic Notes API server...") + logger.info("Server will be available at http://0.0.0.0:8880") + logger.info("Available endpoints:") + logger.info(" GET /api/search?q= - Search functionality") + logger.info(" GET /api/square/ - Calculate square of number") + logger.info(" POST /api/notes - Create a new note") + logger.info(" GET /api/notes - Get all notes") + logger.info(" GET /api/external-ip - Get external IP address") + + app.run(host="0.0.0.0", port=8880, debug=True) diff --git a/workspace/7_framework/sanic/day3_sqlite/requirements.txt b/workspace/7_framework/sanic/day3_sqlite/requirements.txt new file mode 100644 index 0000000..bdcd0f5 --- /dev/null +++ b/workspace/7_framework/sanic/day3_sqlite/requirements.txt @@ -0,0 +1,3 @@ +sanic +httpx +aiosqlite