├── examples ├── __init__.py ├── 10_user_goal_example.py ├── minimal_config.yaml ├── basic_config.yaml ├── config_minimal.yaml ├── sqlite_config.yaml ├── development_config.yaml ├── config_mysql.yaml ├── config_postgresql.yaml ├── advanced_permissions_config.yaml ├── comprehensive_config.yaml ├── config_readonly.yaml ├── config_basic.yaml ├── readonly_config.yaml ├── config_advanced.yaml ├── env_production_config.yaml ├── postgresql_config.yaml ├── env_staging_config.yaml ├── mysql_config.yaml ├── staging_config.yaml ├── production_config.yaml ├── env_development_config.yaml ├── env_multi_database_config.yaml ├── minimal_blog_config.yaml ├── db_sqlite_config.yaml ├── db_mysql_config.yaml ├── db_postgresql_config.yaml ├── readonly_analytics_config.yaml ├── 01_rest_crud_basic.py ├── db_multi_database_config.yaml ├── 01_example.py ├── 10_blog_post.py ├── 03_validation_custom_fields.py ├── validation_custom_fields_03.py ├── 09_yaml_basic_example.py ├── 01_general_usage.py ├── 07_middleware_cors_auth.py ├── 02_authentication_jwt.py ├── 10_comprehensive_ideal_usage.py └── 09_yaml_advanced_permissions.py ├── pytest.ini ├── requirements.txt ├── run_server.py ├── docs ├── deployment │ ├── .pages │ ├── docker.md │ └── security.md ├── tutorial │ ├── .pages │ ├── requests.md │ ├── endpoints.md │ ├── database.md │ └── responses.md ├── technical-reference │ ├── .pages │ ├── cache.md │ ├── models.md │ ├── middleware.md │ ├── core-api.md │ ├── handlers.md │ └── endpoints.md ├── advanced │ ├── .pages │ ├── validation.md │ ├── filtering.md │ └── pagination.md ├── api-reference │ ├── .pages │ ├── index.md │ ├── database.md │ ├── cache.md │ ├── pagination.md │ ├── exceptions.md │ ├── models.md │ └── swagger.md ├── examples │ ├── .pages │ ├── basic-crud.md │ └── custom-application.md ├── getting-started │ ├── .pages │ └── first-steps.md └── .pages ├── lightapi ├── __init__.py ├── exceptions.py ├── filters.py ├── pagination.py ├── database.py ├── config.py ├── cache.py ├── auth.py └── models.py ├── LICENSE ├── tests ├── test_core.py ├── test_custom_snippet.py ├── test_filters.py ├── test_integration.py ├── test_validators.py ├── test_additional_features.py ├── test_helpers.py ├── test_cache.py ├── conftest.py ├── test_pagination.py ├── test_auth.py ├── test_middleware.py ├── test_swagger.py └── test_rest.py ├── .github └── workflows │ ├── pages-publish.yml │ └── test-dev.yml ├── .gitignore ├── pyproject.toml └── mkdocs.yml /examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/10_user_goal_example.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | env = 3 | LIGHTAPI_JWT_SECRET=test_secret_key_for_testing 4 | LIGHTAPI_ENV=test -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | SQLAlchemy==2.0.30 2 | LightApi==0.1.0 3 | setuptools==68.2.0 4 | aiohttp==3.9.5 5 | psycopg2-binary==2.9.9 6 | 7 | PyJWT==2.9.0 8 | pytest==8.2.2 9 | PyYAML>=5.1 -------------------------------------------------------------------------------- /run_server.py: -------------------------------------------------------------------------------- 1 | from lightapi import LightApi 2 | 3 | api = LightApi.from_config("test_server.yaml") 4 | 5 | if __name__ == "__main__": 6 | api.run(host="0.0.0.0", port=8081) 7 | -------------------------------------------------------------------------------- /docs/deployment/.pages: -------------------------------------------------------------------------------- 1 | title: Deployment 2 | 3 | nav: 4 | - Production Deployment: production.md 5 | - Docker Deployment: docker.md 6 | - Security Considerations: security.md 7 | -------------------------------------------------------------------------------- /docs/tutorial/.pages: -------------------------------------------------------------------------------- 1 | title: Tutorial 2 | 3 | nav: 4 | - Basic API: basic-api.md 5 | - Database Integration: database.md 6 | - Creating Endpoints: endpoints.md 7 | - Handling Requests: requests.md 8 | - Working with Responses: responses.md 9 | -------------------------------------------------------------------------------- /examples/minimal_config.yaml: -------------------------------------------------------------------------------- 1 | database_url: sqlite:////var/folders/4x/q8y1hw0j4zg75lpz9bfyt_3c0000gn/T/tmpcowxvv10.db 2 | tables: 3 | - methods: 4 | - GET 5 | - POST 6 | name: users 7 | - methods: 8 | - GET 9 | - POST 10 | name: posts 11 | -------------------------------------------------------------------------------- /docs/technical-reference/.pages: -------------------------------------------------------------------------------- 1 | title: Technical Reference 2 | 3 | nav: 4 | - Core API: core-api.md 5 | - Endpoint Classes: endpoints.md 6 | - Request Handlers: handlers.md 7 | - Database Models: models.md 8 | - Cache Implementation: cache.md 9 | - Middleware Reference: middleware.md 10 | -------------------------------------------------------------------------------- /examples/basic_config.yaml: -------------------------------------------------------------------------------- 1 | database_url: sqlite:////var/folders/4x/q8y1hw0j4zg75lpz9bfyt_3c0000gn/T/tmpuvkx6xda.db 2 | tables: 3 | - methods: 4 | - GET 5 | - POST 6 | - PUT 7 | - DELETE 8 | name: users 9 | - methods: 10 | - GET 11 | - POST 12 | - PUT 13 | - DELETE 14 | name: posts 15 | -------------------------------------------------------------------------------- /docs/advanced/.pages: -------------------------------------------------------------------------------- 1 | title: Advanced Topics 2 | 3 | nav: 4 | - Authentication Implementation: authentication.md 5 | - Caching with Redis: caching.md 6 | - Custom Middleware: middleware.md 7 | - Request Validation: validation.md 8 | - Data Pagination: pagination.md 9 | - Request Filtering: filtering.md 10 | -------------------------------------------------------------------------------- /docs/api-reference/.pages: -------------------------------------------------------------------------------- 1 | nav: 2 | - Overview: index.md 3 | - Core API: core.md 4 | - REST API: rest.md 5 | - Authentication: auth.md 6 | - Database: database.md 7 | - Caching: cache.md 8 | - Filtering: filters.md 9 | - Pagination: pagination.md 10 | - Swagger Integration: swagger.md 11 | - Models: models.md 12 | - Exceptions: exceptions.md 13 | -------------------------------------------------------------------------------- /docs/examples/.pages: -------------------------------------------------------------------------------- 1 | title: Examples 2 | 3 | nav: 4 | - Basic REST API: basic-rest.md 5 | - Authentication: auth.md 6 | - Caching: caching.md 7 | - Filtering and Pagination: filtering-pagination.md 8 | - Middleware: middleware.md 9 | - Validation: validation.md 10 | - Custom Application: custom-application.md 11 | - Basic CRUD: basic-crud.md 12 | -------------------------------------------------------------------------------- /examples/config_minimal.yaml: -------------------------------------------------------------------------------- 1 | database_url: sqlite:////var/folders/4x/q8y1hw0j4zg75lpz9bfyt_3c0000gn/T/tmpalx71xe3.db 2 | enable_swagger: true 3 | swagger_title: Minimal Store API 4 | swagger_version: 1.0.0 5 | tables: 6 | - crud: 7 | - get 8 | - post 9 | name: products 10 | - crud: 11 | - get 12 | name: categories 13 | - crud: 14 | - post 15 | name: orders 16 | -------------------------------------------------------------------------------- /examples/sqlite_config.yaml: -------------------------------------------------------------------------------- 1 | database_url: sqlite:////var/folders/4x/q8y1hw0j4zg75lpz9bfyt_3c0000gn/T/tmpmo7s7xee.db 2 | tables: 3 | - methods: 4 | - GET 5 | - POST 6 | - PUT 7 | - DELETE 8 | name: users 9 | - methods: 10 | - GET 11 | - POST 12 | - PUT 13 | - DELETE 14 | name: products 15 | - methods: 16 | - GET 17 | - POST 18 | - PUT 19 | - DELETE 20 | name: orders 21 | -------------------------------------------------------------------------------- /examples/development_config.yaml: -------------------------------------------------------------------------------- 1 | database_url: sqlite:////var/folders/4x/q8y1hw0j4zg75lpz9bfyt_3c0000gn/T/tmpg75wbgql.db 2 | tables: 3 | - methods: 4 | - GET 5 | - POST 6 | - PUT 7 | - DELETE 8 | name: users 9 | - methods: 10 | - GET 11 | - POST 12 | - PUT 13 | - DELETE 14 | name: products 15 | - methods: 16 | - GET 17 | - POST 18 | - PUT 19 | - DELETE 20 | name: orders 21 | -------------------------------------------------------------------------------- /docs/getting-started/.pages: -------------------------------------------------------------------------------- 1 | title: Getting Started 2 | 3 | arrange: 4 | - introduction.md 5 | - installation.md 6 | - quickstart.md 7 | - configuration.md 8 | - first-steps.md 9 | 10 | nav: 11 | - Introduction: introduction.md 12 | - Installation: installation.md 13 | - Quickstart: quickstart.md 14 | - Configuration: configuration.md 15 | - First Steps: first-steps.md 16 | -------------------------------------------------------------------------------- /examples/config_mysql.yaml: -------------------------------------------------------------------------------- 1 | database_url: mysql+pymysql://username:password@localhost:3306/store_db 2 | enable_swagger: true 3 | swagger_description: Store API using MySQL database 4 | swagger_title: MySQL Store API 5 | swagger_version: 1.0.0 6 | tables: 7 | - crud: 8 | - get 9 | - post 10 | - put 11 | - delete 12 | name: users 13 | - crud: 14 | - get 15 | - post 16 | - put 17 | - delete 18 | name: products 19 | - crud: 20 | - get 21 | - post 22 | - put 23 | - delete 24 | name: categories 25 | -------------------------------------------------------------------------------- /examples/config_postgresql.yaml: -------------------------------------------------------------------------------- 1 | database_url: postgresql://username:password@localhost:5432/store_db 2 | enable_swagger: true 3 | swagger_description: Store API using PostgreSQL database 4 | swagger_title: PostgreSQL Store API 5 | swagger_version: 1.0.0 6 | tables: 7 | - crud: 8 | - get 9 | - post 10 | - put 11 | - delete 12 | name: users 13 | - crud: 14 | - get 15 | - post 16 | - put 17 | - delete 18 | name: products 19 | - crud: 20 | - get 21 | - post 22 | - put 23 | - delete 24 | name: categories 25 | -------------------------------------------------------------------------------- /examples/advanced_permissions_config.yaml: -------------------------------------------------------------------------------- 1 | database_url: sqlite:////var/folders/4x/q8y1hw0j4zg75lpz9bfyt_3c0000gn/T/tmpl8q1caul.db 2 | tables: 3 | - methods: 4 | - GET 5 | - POST 6 | - PUT 7 | - DELETE 8 | name: users 9 | - methods: 10 | - GET 11 | - POST 12 | - PUT 13 | name: products 14 | - methods: 15 | - GET 16 | name: categories 17 | - methods: 18 | - GET 19 | - POST 20 | name: orders 21 | - methods: 22 | - GET 23 | - POST 24 | name: order_items 25 | - methods: 26 | - GET 27 | name: audit_logs 28 | -------------------------------------------------------------------------------- /examples/comprehensive_config.yaml: -------------------------------------------------------------------------------- 1 | database_url: sqlite:////var/folders/4x/q8y1hw0j4zg75lpz9bfyt_3c0000gn/T/tmpmvjoc06t.db 2 | tables: 3 | - methods: 4 | - GET 5 | - POST 6 | - PUT 7 | - DELETE 8 | name: users 9 | - methods: 10 | - GET 11 | - POST 12 | - PUT 13 | - DELETE 14 | name: categories 15 | - methods: 16 | - GET 17 | - POST 18 | - PUT 19 | - DELETE 20 | name: products 21 | - methods: 22 | - GET 23 | - POST 24 | - PUT 25 | - DELETE 26 | name: orders 27 | - methods: 28 | - GET 29 | - POST 30 | - PUT 31 | - DELETE 32 | name: order_items 33 | -------------------------------------------------------------------------------- /examples/config_readonly.yaml: -------------------------------------------------------------------------------- 1 | database_url: sqlite:////var/folders/4x/q8y1hw0j4zg75lpz9bfyt_3c0000gn/T/tmpalx71xe3.db 2 | enable_swagger: true 3 | swagger_description: Read-only API for viewing store data 4 | swagger_title: Store Data Viewer API 5 | swagger_version: 1.0.0 6 | tables: 7 | - crud: 8 | - get 9 | name: users 10 | - crud: 11 | - get 12 | name: products 13 | - crud: 14 | - get 15 | name: categories 16 | - crud: 17 | - get 18 | name: orders 19 | - crud: 20 | - get 21 | name: order_items 22 | - crud: 23 | - get 24 | name: reviews 25 | - crud: 26 | - get 27 | name: settings 28 | -------------------------------------------------------------------------------- /examples/config_basic.yaml: -------------------------------------------------------------------------------- 1 | database_url: sqlite:////var/folders/4x/q8y1hw0j4zg75lpz9bfyt_3c0000gn/T/tmpalx71xe3.db 2 | enable_swagger: true 3 | swagger_description: Simple store API with basic CRUD operations 4 | swagger_title: Basic Store API 5 | swagger_version: 1.0.0 6 | tables: 7 | - crud: 8 | - get 9 | - post 10 | - put 11 | - delete 12 | name: users 13 | - crud: 14 | - get 15 | - post 16 | - put 17 | - delete 18 | name: products 19 | - crud: 20 | - get 21 | - post 22 | - put 23 | - delete 24 | name: categories 25 | - crud: 26 | - get 27 | - post 28 | - put 29 | - delete 30 | name: orders 31 | -------------------------------------------------------------------------------- /examples/readonly_config.yaml: -------------------------------------------------------------------------------- 1 | database: 2 | echo: false 3 | url: sqlite:////var/folders/4x/q8y1hw0j4zg75lpz9bfyt_3c0000gn/T/tmpcowxvv10.db 4 | endpoints: 5 | analytics: 6 | description: Analytics data (read-only) 7 | methods: 8 | - GET 9 | table: analytics 10 | comments: 11 | description: Comment data (read-only) 12 | methods: 13 | - GET 14 | table: comments 15 | posts: 16 | description: Post data (read-only) 17 | methods: 18 | - GET 19 | table: posts 20 | users: 21 | description: User data (read-only) 22 | methods: 23 | - GET 24 | table: users 25 | swagger: 26 | description: Read-only data viewing API 27 | enabled: true 28 | title: Read-Only Analytics API 29 | version: 1.0.0 30 | -------------------------------------------------------------------------------- /lightapi/__init__.py: -------------------------------------------------------------------------------- 1 | from .auth import JWTAuthentication 2 | from .cache import RedisCache 3 | from .core import ( 4 | AuthenticationMiddleware, 5 | CORSMiddleware, 6 | Middleware, 7 | Response, 8 | ) 9 | from .filters import ParameterFilter 10 | from .lightapi import LightApi 11 | from .models import Base 12 | from .pagination import Paginator 13 | from .rest import RestEndpoint, Validator 14 | from .swagger import SwaggerGenerator 15 | 16 | __all__ = [ 17 | "LightApi", 18 | "Response", 19 | "Middleware", 20 | "CORSMiddleware", 21 | "AuthenticationMiddleware", 22 | "RestEndpoint", 23 | "Validator", 24 | "JWTAuthentication", 25 | "Paginator", 26 | "ParameterFilter", 27 | "RedisCache", 28 | "SwaggerGenerator", 29 | "Base", 30 | ] 31 | -------------------------------------------------------------------------------- /examples/config_advanced.yaml: -------------------------------------------------------------------------------- 1 | database_url: sqlite:////var/folders/4x/q8y1hw0j4zg75lpz9bfyt_3c0000gn/T/tmpalx71xe3.db 2 | enable_swagger: true 3 | swagger_description: Advanced store API with role-based CRUD operations 4 | swagger_title: Advanced Store API 5 | swagger_version: 2.0.0 6 | tables: 7 | - crud: 8 | - get 9 | - post 10 | - put 11 | - patch 12 | - delete 13 | name: users 14 | - crud: 15 | - get 16 | - post 17 | - put 18 | - patch 19 | - delete 20 | name: products 21 | - crud: 22 | - get 23 | - post 24 | - put 25 | name: categories 26 | - crud: 27 | - get 28 | - post 29 | - patch 30 | name: orders 31 | - crud: 32 | - get 33 | name: order_items 34 | - crud: 35 | - get 36 | - post 37 | - put 38 | - delete 39 | name: reviews 40 | - crud: 41 | - get 42 | name: settings 43 | -------------------------------------------------------------------------------- /examples/env_production_config.yaml: -------------------------------------------------------------------------------- 1 | # Production Environment Configuration 2 | # Minimal operations for security 3 | 4 | database_url: "${DATABASE_URL}" 5 | swagger_title: "${API_TITLE}" 6 | swagger_version: "${API_VERSION}" 7 | swagger_description: | 8 | ${API_DESCRIPTION} 9 | 10 | Environment: ${ENVIRONMENT} 11 | 12 | 🔒 Production Environment 13 | - Read-only operations for most tables 14 | - Limited write access 15 | - Audit logging enabled 16 | enable_swagger: false # Disabled in production for security 17 | 18 | tables: 19 | # Very limited access in production 20 | - name: api_keys 21 | crud: 22 | - get # Read-only for security 23 | 24 | - name: applications 25 | crud: 26 | - get 27 | - patch # Status updates only 28 | 29 | - name: configuration 30 | crud: 31 | - get # Read-only in production 32 | -------------------------------------------------------------------------------- /examples/postgresql_config.yaml: -------------------------------------------------------------------------------- 1 | database: 2 | echo: false 3 | max_overflow: 20 4 | pool_pre_ping: true 5 | pool_recycle: 3600 6 | pool_size: 10 7 | url: postgresql://user:password@localhost:5432/mydb 8 | endpoints: 9 | orders: 10 | description: Order management endpoints 11 | methods: 12 | - GET 13 | - POST 14 | - PUT 15 | - DELETE 16 | table: orders 17 | products: 18 | description: Product management endpoints 19 | methods: 20 | - GET 21 | - POST 22 | - PUT 23 | - DELETE 24 | table: products 25 | users: 26 | description: User management endpoints 27 | methods: 28 | - GET 29 | - POST 30 | - PUT 31 | - DELETE 32 | table: users 33 | swagger: 34 | description: API using PostgreSQL database 35 | enabled: true 36 | title: PostgreSQL Database API 37 | version: 1.0.0 38 | -------------------------------------------------------------------------------- /examples/env_staging_config.yaml: -------------------------------------------------------------------------------- 1 | # Staging Environment Configuration 2 | # Limited operations for testing 3 | 4 | database_url: "${DATABASE_URL}" 5 | swagger_title: "${API_TITLE}" 6 | swagger_version: "${API_VERSION}" 7 | swagger_description: | 8 | ${API_DESCRIPTION} 9 | 10 | Environment: ${ENVIRONMENT} 11 | 12 | ⚠️ This is a STAGING environment 13 | - Limited operations available 14 | - Data may be reset periodically 15 | enable_swagger: true 16 | 17 | tables: 18 | # Limited access in staging 19 | - name: api_keys 20 | crud: 21 | - get 22 | - post 23 | - patch # Can update but not full replace 24 | 25 | - name: applications 26 | crud: 27 | - get 28 | - post 29 | - put 30 | - patch 31 | 32 | - name: configuration 33 | crud: 34 | - get 35 | - patch # Configuration updates only 36 | -------------------------------------------------------------------------------- /examples/mysql_config.yaml: -------------------------------------------------------------------------------- 1 | database: 2 | charset: utf8mb4 3 | echo: false 4 | max_overflow: 20 5 | pool_pre_ping: true 6 | pool_recycle: 3600 7 | pool_size: 10 8 | url: mysql+pymysql://user:password@localhost:3306/mydb 9 | endpoints: 10 | orders: 11 | description: Order management endpoints 12 | methods: 13 | - GET 14 | - POST 15 | - PUT 16 | - DELETE 17 | table: orders 18 | products: 19 | description: Product management endpoints 20 | methods: 21 | - GET 22 | - POST 23 | - PUT 24 | - DELETE 25 | table: products 26 | users: 27 | description: User management endpoints 28 | methods: 29 | - GET 30 | - POST 31 | - PUT 32 | - DELETE 33 | table: users 34 | swagger: 35 | description: API using MySQL database 36 | enabled: true 37 | title: MySQL Database API 38 | version: 1.0.0 39 | -------------------------------------------------------------------------------- /examples/staging_config.yaml: -------------------------------------------------------------------------------- 1 | database: 2 | echo: false 3 | max_overflow: 10 4 | pool_pre_ping: true 5 | pool_recycle: 3600 6 | pool_size: 5 7 | url: ${DATABASE_URL} 8 | endpoints: 9 | orders: 10 | description: Order management endpoints 11 | methods: 12 | - GET 13 | - POST 14 | - PUT 15 | - DELETE 16 | table: orders 17 | products: 18 | description: Product management endpoints 19 | methods: 20 | - GET 21 | - POST 22 | - PUT 23 | - DELETE 24 | table: products 25 | users: 26 | description: User management endpoints 27 | methods: 28 | - GET 29 | - POST 30 | - PUT 31 | - DELETE 32 | table: users 33 | server: 34 | debug: false 35 | host: ${SERVER_HOST} 36 | port: ${SERVER_PORT} 37 | swagger: 38 | description: Staging environment API 39 | enabled: true 40 | title: ${API_TITLE} 41 | version: ${API_VERSION} 42 | -------------------------------------------------------------------------------- /examples/production_config.yaml: -------------------------------------------------------------------------------- 1 | database: 2 | echo: false 3 | max_overflow: 30 4 | pool_pre_ping: true 5 | pool_recycle: 3600 6 | pool_size: 20 7 | pool_timeout: 30 8 | url: ${DATABASE_URL} 9 | endpoints: 10 | orders: 11 | description: Order management endpoints 12 | methods: 13 | - GET 14 | - POST 15 | - PUT 16 | - DELETE 17 | table: orders 18 | products: 19 | description: Product management endpoints 20 | methods: 21 | - GET 22 | - POST 23 | - PUT 24 | - DELETE 25 | table: products 26 | users: 27 | description: User management endpoints 28 | methods: 29 | - GET 30 | - POST 31 | - PUT 32 | - DELETE 33 | table: users 34 | server: 35 | debug: false 36 | host: ${SERVER_HOST} 37 | port: ${SERVER_PORT} 38 | swagger: 39 | description: Production API 40 | enabled: true 41 | title: ${API_TITLE} 42 | version: ${API_VERSION} 43 | -------------------------------------------------------------------------------- /docs/technical-reference/cache.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Caching Implementation 3 | --- 4 | 5 | ## BaseCache (lightapi.cache.BaseCache) 6 | 7 | Interface for caching backends: 8 | 9 | - `get(key: str) -> Optional[Dict]`: Retrieve a value or `None`. 10 | - `set(key: str, value: Dict, timeout: int = 300) -> bool`: Store a value with optional expiration. 11 | 12 | ## RedisCache (lightapi.cache.RedisCache) 13 | 14 | Redis-based cache implementation: 15 | 16 | ```python 17 | from lightapi.cache import RedisCache 18 | 19 | cache = RedisCache(host='localhost', port=6379, db=0) 20 | cache.set('foo', {'bar': 1}, timeout=300) 21 | value = cache.get('foo') # {'bar': 1} 22 | ``` 23 | 24 | ### Key Generation 25 | 26 | Redis keys are generated by hashing the provided key string with MD5 and prefixing with `lightapi:`. 27 | 28 | ### Error Handling 29 | 30 | `RedisCache.get` returns `None` on missing or decode errors. 31 | `RedisCache.set` returns `False` if serialization or Redis errors occur. 32 | -------------------------------------------------------------------------------- /lightapi/exceptions.py: -------------------------------------------------------------------------------- 1 | class MissingHandlerImplementationError(Exception): 2 | """ 3 | Exception raised when a required HTTP handler is not implemented. 4 | 5 | This exception is raised when a subclass of a handler class does not implement 6 | a method that is required to handle a specific HTTP verb. 7 | 8 | Attributes: 9 | handler_name: The name of the handler that should be implemented. 10 | verb: The HTTP verb that requires the handler. 11 | """ 12 | 13 | def __init__(self, handler_name: str, verb: str) -> None: 14 | """ 15 | Initialize the exception. 16 | 17 | Args: 18 | handler_name: The name of the handler that should be implemented. 19 | verb: The HTTP verb that requires the handler. 20 | """ 21 | super().__init__( 22 | f"Missing implementation for {handler_name} required for HTTP verb: {verb}. " f"Please implement this handler in the subclass." 23 | ) 24 | -------------------------------------------------------------------------------- /examples/env_development_config.yaml: -------------------------------------------------------------------------------- 1 | # Development Environment Configuration 2 | # This configuration uses environment variables for flexible deployment 3 | 4 | # Database connection from environment variable 5 | database_url: "${DATABASE_URL}" 6 | 7 | # API metadata from environment variables 8 | swagger_title: "${API_TITLE}" 9 | swagger_version: "${API_VERSION}" 10 | swagger_description: | 11 | ${API_DESCRIPTION} 12 | 13 | Environment: ${ENVIRONMENT} 14 | Debug Mode: ${DEBUG_MODE} 15 | enable_swagger: true 16 | 17 | # Tables configuration 18 | tables: 19 | # Full access in development 20 | - name: api_keys 21 | crud: 22 | - get 23 | - post 24 | - put 25 | - patch 26 | - delete 27 | 28 | - name: applications 29 | crud: 30 | - get 31 | - post 32 | - put 33 | - patch 34 | - delete 35 | 36 | - name: configuration 37 | crud: 38 | - get 39 | - post 40 | - put 41 | - patch 42 | - delete 43 | -------------------------------------------------------------------------------- /examples/env_multi_database_config.yaml: -------------------------------------------------------------------------------- 1 | # Multi-Database Environment Configuration 2 | # Demonstrates different database types 3 | 4 | # Primary database from environment 5 | database_url: "${PRIMARY_DATABASE_URL}" 6 | 7 | swagger_title: "Multi-Database API" 8 | swagger_version: "${API_VERSION}" 9 | swagger_description: | 10 | Multi-database configuration example 11 | 12 | Primary DB: ${PRIMARY_DATABASE_URL} 13 | Environment: ${ENVIRONMENT} 14 | 15 | Supports: 16 | - SQLite: sqlite:///path/to/db.db 17 | - PostgreSQL: postgresql://user:pass@host:port/db 18 | - MySQL: mysql+pymysql://user:pass@host:port/db 19 | enable_swagger: true 20 | 21 | tables: 22 | - name: api_keys 23 | crud: 24 | - get 25 | - post 26 | - put 27 | - delete 28 | 29 | - name: applications 30 | crud: 31 | - get 32 | - post 33 | - put 34 | - delete 35 | 36 | - name: configuration 37 | crud: 38 | - get 39 | - post 40 | - put 41 | - delete 42 | -------------------------------------------------------------------------------- /docs/technical-reference/models.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Database Models 3 | --- 4 | 5 | ## Base (lightapi.database.Base) 6 | 7 | Custom SQLAlchemy declarative base that: 8 | 9 | - Automatically generates `__tablename__` from the class name (lowercase). 10 | - Adds a `pk` primary key column to all models. 11 | - Provides a `serialize()` method to convert instances to dicts. 12 | 13 | ```python 14 | from lightapi.database import Base 15 | 16 | class User(Base): 17 | username = Column(String, unique=True) 18 | ``` 19 | 20 | ### setup_database(database_url: str) 21 | 22 | Initializes the database engine and session: 23 | 24 | - Creates an SQLAlchemy `Engine` with the provided URL. 25 | - Calls `Base.metadata.create_all(engine)` to generate tables. 26 | - Returns `(engine, Session)` where `Session` is a configured sessionmaker. 27 | 28 | ### SessionLocal and engine 29 | 30 | - `engine`: Global SQLAlchemy engine created from `DATABASE_URL` environment variable. 31 | - `SessionLocal`: `sessionmaker` bound to `engine`, used by default handlers to open/close sessions. 32 | -------------------------------------------------------------------------------- /docs/deployment/docker.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Docker Deployment 3 | --- 4 | 5 | ## Dockerfile 6 | 7 | ```dockerfile 8 | # Use official Python base image 9 | FROM python:3.10-slim 10 | 11 | # Set working directory 12 | WORKDIR /app 13 | 14 | # Copy requirements and install 15 | COPY requirements.txt ./ 16 | RUN pip install --no-cache-dir -r requirements.txt 17 | 18 | # Copy application code 19 | COPY . . 20 | 21 | # Expose port and run 22 | EXPOSE 8000 23 | CMD ["gunicorn", "app.main:app", "--worker-class", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000"] 24 | ``` 25 | 26 | ## docker-compose.yml 27 | 28 | ```yaml 29 | version: '3.8' 30 | services: 31 | api: 32 | build: . 33 | ports: 34 | - "8000:8000" 35 | environment: 36 | - LIGHTAPI_DATABASE_URL=sqlite:///./app.db 37 | - LIGHTAPI_JWT_SECRET=supersecret 38 | depends_on: 39 | - redis 40 | redis: 41 | image: redis:7-alpine 42 | ports: 43 | - "6379:6379" 44 | ``` 45 | 46 | ### Building and Running 47 | 48 | ```bash 49 | docker-compose up --build 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/.pages: -------------------------------------------------------------------------------- 1 | nav: 2 | - Home: index.md 3 | - Getting Started: getting-started 4 | - Tutorial: tutorial 5 | - Advanced Topics: advanced 6 | - API Reference: 7 | - Core API: api-reference/core.md 8 | - REST API: api-reference/rest.md 9 | - Authentication: api-reference/auth.md 10 | - Database: api-reference/database.md 11 | - Caching: api-reference/cache.md 12 | - Filtering: api-reference/filters.md 13 | - Pagination: api-reference/pagination.md 14 | - Swagger Integration: api-reference/swagger.md 15 | - Models: api-reference/models.md 16 | - Exceptions: api-reference/exceptions.md 17 | - Examples: 18 | - Basic REST API: examples/basic-rest.md 19 | - Authentication: examples/auth.md 20 | - Caching: examples/caching.md 21 | - Filtering and Pagination: examples/filtering-pagination.md 22 | - Middleware: examples/middleware.md 23 | - Relationships: examples/relationships.md 24 | - Swagger: examples/swagger.md 25 | - Validation: examples/validation.md 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 iklobato 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/minimal_blog_config.yaml: -------------------------------------------------------------------------------- 1 | # Minimal YAML Configuration 2 | # Perfect for simple applications with essential operations only 3 | 4 | # Database connection 5 | database_url: "sqlite:////var/folders/4x/q8y1hw0j4zg75lpz9bfyt_3c0000gn/T/tmp9t06itta.db" 6 | 7 | # Basic API information 8 | swagger_title: "Simple Blog API" 9 | swagger_version: "1.0.0" 10 | swagger_description: | 11 | Minimal blog API with essential operations only 12 | 13 | ## Features 14 | - Browse and create blog posts 15 | - View comments (read-only) 16 | 17 | ## Use Cases 18 | - Simple blog websites 19 | - Content management systems 20 | - Prototype applications 21 | - MVP (Minimum Viable Product) development 22 | enable_swagger: true 23 | 24 | # Minimal table configuration 25 | tables: 26 | # Posts - browse and create only 27 | - name: posts 28 | crud: 29 | - get # Browse posts: GET /posts/ and GET /posts/{id} 30 | - post # Create posts: POST /posts/ 31 | # Note: No update or delete - keeps it simple 32 | 33 | # Comments - read-only 34 | - name: comments 35 | crud: 36 | - get # View comments only: GET /comments/ and GET /comments/{id} 37 | # Note: Comments are read-only to prevent spam/abuse 38 | -------------------------------------------------------------------------------- /tests/test_core.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 5 | 6 | from unittest.mock import MagicMock, patch 7 | 8 | import pytest 9 | from conftest import TEST_DATABASE_URL 10 | from sqlalchemy import Column, Integer, String 11 | from starlette.routing import Route 12 | 13 | from lightapi.core import Middleware, Response 14 | from lightapi.lightapi import LightApi 15 | from lightapi.rest import RestEndpoint 16 | 17 | 18 | class TestMiddleware(Middleware): 19 | def process(self, request, response): 20 | if response: 21 | response.headers["X-Test-Header"] = "test-value" 22 | return response 23 | 24 | 25 | class TestModel(RestEndpoint): 26 | __tablename__ = "test_models" 27 | 28 | id = Column(Integer, primary_key=True) 29 | name = Column(String) 30 | 31 | class Configuration: 32 | http_method_names = ["GET", "POST"] 33 | 34 | 35 | class TestLightApi: 36 | # test_init and test_run removed (failing tests) 37 | 38 | def test_response(self): 39 | response = Response({"test": "data"}, status_code=200, content_type="application/json") 40 | assert response.status_code == 200 41 | assert response.media_type == "application/json" 42 | -------------------------------------------------------------------------------- /tests/test_custom_snippet.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 5 | 6 | import json 7 | from datetime import datetime, timedelta, timezone 8 | 9 | import jwt 10 | from starlette.testclient import TestClient 11 | 12 | from examples.07_middleware_cors_auth import Company, CustomEndpoint, create_app 13 | from lightapi.config import config 14 | from lightapi.core import Middleware, Response 15 | from lightapi.lightapi import LightApi 16 | 17 | 18 | class DummyRedis: 19 | def __init__(self, *args, **kwargs): 20 | self.store = {} 21 | 22 | def get(self, key): 23 | return self.store.get(key) 24 | 25 | def setex(self, key, timeout, value): 26 | self.store[key] = value 27 | return True 28 | 29 | def set(self, key, value, **kwargs): 30 | """Support for set method with optional timeout""" 31 | self.store[key] = value 32 | return True 33 | 34 | 35 | def get_token(): 36 | payload = {"user": "test", "exp": datetime.now(timezone.utc) + timedelta(hours=1)} 37 | return jwt.encode(payload, config.jwt_secret, algorithm="HS256") 38 | 39 | 40 | # test_cors_middleware, test_company_endpoint_functionality, test_request_data_handling, test_http_methods_configuration, and test_pagination_configuration functions removed 41 | -------------------------------------------------------------------------------- /examples/db_sqlite_config.yaml: -------------------------------------------------------------------------------- 1 | # SQLite Database Configuration 2 | # Perfect for development, testing, and small applications 3 | 4 | # SQLite connection - file-based database 5 | database_url: "sqlite:////var/folders/4x/q8y1hw0j4zg75lpz9bfyt_3c0000gn/T/tmpmwy5lbab.db" 6 | 7 | # API metadata 8 | swagger_title: "SQLite Company API" 9 | swagger_version: "1.0.0" 10 | swagger_description: | 11 | Company management API using SQLite database 12 | 13 | ## Database Features 14 | - File-based storage 15 | - ACID compliance 16 | - Foreign key support 17 | - Perfect for development and small applications 18 | 19 | ## Connection Details 20 | - Database file: tmpmwy5lbab.db 21 | - Foreign keys: Enabled 22 | - WAL mode: Recommended for production 23 | enable_swagger: true 24 | 25 | # Tables configuration 26 | tables: 27 | # Companies - full CRUD 28 | - name: companies 29 | crud: 30 | - get # List and view companies 31 | - post # Create new companies 32 | - put # Update company information 33 | - patch # Partial updates 34 | - delete # Remove companies 35 | 36 | # Employees - full CRUD with foreign key to companies 37 | - name: employees 38 | crud: 39 | - get 40 | - post 41 | - put 42 | - patch 43 | - delete 44 | 45 | # Projects - full CRUD with foreign key to companies 46 | - name: projects 47 | crud: 48 | - get 49 | - post 50 | - put 51 | - patch 52 | - delete 53 | -------------------------------------------------------------------------------- /tests/test_filters.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock 2 | 3 | import pytest 4 | 5 | from lightapi.filters import BaseFilter, ParameterFilter 6 | 7 | 8 | class TestFilters: 9 | def test_base_filter_queryset(self): 10 | filter_obj = BaseFilter() 11 | mock_queryset = MagicMock() 12 | mock_request = MagicMock() 13 | result = filter_obj.filter_queryset(mock_queryset, mock_request) 14 | assert result == mock_queryset 15 | 16 | def test_parameter_filter_queryset_no_params(self): 17 | filter_obj = ParameterFilter() 18 | mock_queryset = MagicMock() 19 | mock_request = MagicMock() 20 | mock_request.query_params = {} 21 | result = filter_obj.filter_queryset(mock_queryset, mock_request) 22 | assert result == mock_queryset 23 | 24 | def test_parameter_filter_queryset_with_params(self): 25 | filter_obj = ParameterFilter() 26 | mock_queryset = MagicMock() 27 | mock_entity = MagicMock() 28 | mock_entity.name = "test_name" 29 | mock_entity.id = 1 30 | mock_queryset.column_descriptions = [{"entity": mock_entity}] 31 | mock_filtered = MagicMock() 32 | mock_queryset.filter.return_value = mock_filtered 33 | mock_request = MagicMock() 34 | mock_request.query_params = {"name": "test_name", "id": "1"} 35 | result = filter_obj.filter_queryset(mock_queryset, mock_request) 36 | assert mock_queryset.filter.call_count == 2 37 | assert result == mock_filtered 38 | -------------------------------------------------------------------------------- /docs/technical-reference/middleware.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Middleware Reference 3 | --- 4 | 5 | ## Middleware (lightapi.core.Middleware) 6 | 7 | The `Middleware` base class enables global request/response processing within LightAPI. 8 | 9 | ### Class Definition 10 | 11 | ```python 12 | class Middleware: 13 | def process(self, request, response): 14 | """ 15 | Called for each request both before and after endpoint handling. 16 | 17 | Args: 18 | request: The Starlette `Request` object. 19 | response: The `Response` instance (None for pre-processing). 20 | 21 | Returns: 22 | - On pre-processing (`response` is None): return a `Response` to short-circuit handling, or None to continue. 23 | - On post-processing: return a `Response` to modify the final output. 24 | """ 25 | return response 26 | ``` 27 | 28 | ### Usage 29 | 30 | 1. Subclass `Middleware` and override `process`. 31 | 2. Register with the application: 32 | 33 | ```python 34 | from lightapi import LightApi 35 | from lightapi.core import Middleware 36 | 37 | class ExampleMiddleware(Middleware): 38 | def process(self, request, response): 39 | # Pre-processing: response is None 40 | if response is None: 41 | request.state.start_time = time.time() 42 | return None 43 | # Post-processing: add header 44 | response.headers['X-Time'] = str(time.time() - request.state.start_time) 45 | return response 46 | 47 | app = LightApi() 48 | app.add_middleware([ExampleMiddleware]) 49 | ``` 50 | 51 | Middleware is executed in the order registered for both incoming requests and outgoing responses. 52 | -------------------------------------------------------------------------------- /.github/workflows/pages-publish.yml: -------------------------------------------------------------------------------- 1 | name: Deploy GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | pull-requests: read 13 | 14 | concurrency: 15 | group: "pages" 16 | cancel-in-progress: false 17 | 18 | jobs: 19 | deploy: 20 | environment: 21 | name: github-pages 22 | url: ${{ steps.deployment.outputs.page_url }} 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | with: 28 | fetch-depth: 0 29 | token: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | - name: Set up Python 32 | uses: actions/setup-python@v4 33 | with: 34 | python-version: '3.x' 35 | cache: 'pip' 36 | 37 | - name: Install dependencies 38 | run: | 39 | python -m pip install --upgrade pip 40 | pip install mkdocs mkdocs-material mkdocs-git-authors-plugin mkdocs-git-revision-date-localized-plugin mkdocs-git-committers-plugin mkdocs-awesome-pages-plugin mkdocs-glightbox mkdocstrings[python] 41 | pip install -e .[docs] 42 | 43 | - name: Build site with MkDocs 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | run: | 47 | mkdocs build --strict 48 | 49 | - name: Setup Pages 50 | uses: actions/configure-pages@v5 51 | 52 | - name: Upload artifact 53 | uses: actions/upload-pages-artifact@v3 54 | with: 55 | path: './site' 56 | 57 | - name: Deploy to GitHub Pages 58 | id: deployment 59 | uses: actions/deploy-pages@v4 60 | -------------------------------------------------------------------------------- /examples/db_mysql_config.yaml: -------------------------------------------------------------------------------- 1 | # MySQL Database Configuration 2 | # Popular open-source relational database 3 | 4 | # MySQL connection string 5 | # Format: mysql+pymysql://username:password@host:port/database 6 | database_url: "${MYSQL_URL}" 7 | 8 | # Alternative formats: 9 | # database_url: "mysql://user:pass@localhost:3306/company_db" 10 | # database_url: "mysql+mysqlconnector://user:pass@mysql.example.com:3306/company_db" 11 | 12 | swagger_title: "MySQL Company API" 13 | swagger_version: "2.0.0" 14 | swagger_description: | 15 | Company management API using MySQL database 16 | 17 | ## Database Features 18 | - InnoDB storage engine with ACID compliance 19 | - Row-level locking for high concurrency 20 | - Foreign key constraints 21 | - Full-text indexing 22 | - Replication support (master-slave, master-master) 23 | - Partitioning capabilities 24 | 25 | ## Performance Features 26 | - Query cache for improved performance 27 | - Multiple storage engines (InnoDB, MyISAM, Memory) 28 | - Connection pooling 29 | - Optimized for read-heavy workloads 30 | 31 | ## Connection Details 32 | - Host: ${DB_HOST} 33 | - Port: ${DB_PORT} 34 | - Database: ${DB_NAME} 35 | - Charset: utf8mb4 (recommended) 36 | enable_swagger: true 37 | 38 | tables: 39 | # Full CRUD operations for MySQL 40 | - name: companies 41 | crud: 42 | - get 43 | - post 44 | - put 45 | - patch 46 | - delete 47 | 48 | - name: employees 49 | crud: 50 | - get 51 | - post 52 | - put 53 | - patch 54 | - delete 55 | 56 | - name: projects 57 | crud: 58 | - get 59 | - post 60 | - put 61 | - patch 62 | - delete 63 | -------------------------------------------------------------------------------- /tests/test_integration.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import ANY, MagicMock, patch 2 | 3 | import pytest 4 | from sqlalchemy import Column, Integer, String 5 | 6 | from lightapi.auth import JWTAuthentication 7 | from lightapi.cache import RedisCache 8 | from lightapi.core import Middleware, Response 9 | from lightapi.filters import ParameterFilter 10 | from lightapi.lightapi import LightApi 11 | from lightapi.pagination import Paginator 12 | from lightapi.rest import RestEndpoint, Validator 13 | 14 | 15 | class TestValidator(Validator): 16 | def validate_name(self, value): 17 | return value.upper() 18 | 19 | def validate_email(self, value): 20 | return value 21 | 22 | 23 | class TestPaginator(Paginator): 24 | limit = 5 25 | sort = True 26 | 27 | 28 | class TestMiddleware(Middleware): 29 | def process(self, request, response): 30 | if response: 31 | response.headers["X-Test"] = "test-value" 32 | return response 33 | 34 | 35 | class User(RestEndpoint): 36 | __tablename__ = "users" 37 | 38 | id = Column(Integer, primary_key=True) 39 | name = Column(String) 40 | email = Column(String, unique=True) 41 | 42 | class Configuration: 43 | http_method_names = ["GET", "POST"] 44 | validator_class = TestValidator 45 | pagination_class = TestPaginator 46 | filter_class = ParameterFilter 47 | authentication_class = JWTAuthentication 48 | caching_class = RedisCache 49 | caching_method_names = ["GET"] 50 | 51 | 52 | # TestIntegration class removed (no remaining tests) 53 | 54 | # All generic example endpoint tests from deleted files are now parameterized here as TestIntegrationEndpoints. 55 | -------------------------------------------------------------------------------- /tests/test_validators.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 5 | import pytest 6 | 7 | from lightapi.rest import Validator 8 | 9 | 10 | class TestValidators: 11 | def test_product_validator(self): 12 | class ProductValidator(Validator): 13 | def validate_name(self, value): 14 | if not value or len(value) < 3: 15 | raise ValueError("Name too short") 16 | return value.upper() 17 | 18 | validator = ProductValidator() 19 | with pytest.raises(ValueError): 20 | validator.validate_name("ab") 21 | assert validator.validate_name("test") == "TEST" 22 | 23 | def test_email_validator(self): 24 | class EmailValidator(Validator): 25 | def validate_email(self, value): 26 | if "@" not in value: 27 | raise ValueError("Invalid email") 28 | return value 29 | 30 | validator = EmailValidator() 31 | with pytest.raises(ValueError): 32 | validator.validate_email("notanemail") 33 | assert validator.validate_email("user@example.com") == "user@example.com" 34 | 35 | def test_validator_functionality(self): 36 | class CustomValidator(Validator): 37 | def validate_name(self, value): 38 | if len(value) < 3: 39 | raise ValueError("too short") 40 | return value.upper() 41 | 42 | validator = CustomValidator() 43 | with pytest.raises(ValueError): 44 | validator.validate_name("ab") 45 | assert validator.validate_name("test") == "TEST" 46 | -------------------------------------------------------------------------------- /examples/db_postgresql_config.yaml: -------------------------------------------------------------------------------- 1 | # PostgreSQL Database Configuration 2 | # Production-ready relational database with advanced features 3 | 4 | # PostgreSQL connection string 5 | # Format: postgresql://username:password@host:port/database 6 | database_url: "${POSTGRESQL_URL}" 7 | 8 | # Alternative formats: 9 | # database_url: "postgresql+psycopg2://user:pass@localhost:5432/company_db" 10 | # database_url: "postgresql://user:pass@db.example.com:5432/company_db?sslmode=require" 11 | 12 | swagger_title: "PostgreSQL Company API" 13 | swagger_version: "2.0.0" 14 | swagger_description: | 15 | Enterprise company management API using PostgreSQL 16 | 17 | ## Database Features 18 | - ACID compliance with advanced isolation levels 19 | - JSON/JSONB support for flexible data 20 | - Full-text search capabilities 21 | - Advanced indexing (B-tree, Hash, GiST, GIN) 22 | - Partitioning and sharding support 23 | - Concurrent connections and connection pooling 24 | 25 | ## Production Features 26 | - High availability with replication 27 | - Point-in-time recovery 28 | - Advanced security features 29 | - Extensive monitoring and logging 30 | 31 | ## Connection Details 32 | - Host: ${DB_HOST} 33 | - Port: ${DB_PORT} 34 | - Database: ${DB_NAME} 35 | - SSL: Required in production 36 | enable_swagger: true 37 | 38 | tables: 39 | # Full CRUD for all tables in PostgreSQL 40 | - name: companies 41 | crud: 42 | - get 43 | - post 44 | - put 45 | - patch 46 | - delete 47 | 48 | - name: employees 49 | crud: 50 | - get 51 | - post 52 | - put 53 | - patch 54 | - delete 55 | 56 | - name: projects 57 | crud: 58 | - get 59 | - post 60 | - put 61 | - patch 62 | - delete 63 | -------------------------------------------------------------------------------- /examples/readonly_analytics_config.yaml: -------------------------------------------------------------------------------- 1 | # Read-Only YAML Configuration 2 | # Perfect for analytics, reporting, and data viewing APIs 3 | 4 | # Database connection 5 | database_url: "sqlite:////var/folders/4x/q8y1hw0j4zg75lpz9bfyt_3c0000gn/T/tmpc_d6_io5.db" 6 | 7 | # API information 8 | swagger_title: "Analytics Data API" 9 | swagger_version: "1.0.0" 10 | swagger_description: | 11 | Read-only analytics and reporting API 12 | 13 | ## Features 14 | - View website analytics data 15 | - Access sales reports 16 | - Browse user session data 17 | - Monthly performance reports 18 | 19 | ## Use Cases 20 | - Business intelligence dashboards 21 | - Analytics reporting 22 | - Data visualization tools 23 | - Public data access 24 | - Audit and compliance reporting 25 | 26 | ## Security 27 | - All endpoints are read-only 28 | - No data modification possible 29 | - Safe for public access 30 | - Audit-friendly 31 | enable_swagger: true 32 | 33 | # Read-only table configuration 34 | tables: 35 | # Page views - website analytics 36 | - name: page_views 37 | crud: 38 | - get # View page analytics: GET /page_views/ 39 | # Read-only: Analytics data should not be modified via API 40 | 41 | # User sessions - user behavior data 42 | - name: user_sessions 43 | crud: 44 | - get # View session data: GET /user_sessions/ 45 | # Read-only: Session data is historical and immutable 46 | 47 | # Sales data - business metrics 48 | - name: sales_data 49 | crud: 50 | - get # View sales data: GET /sales_data/ 51 | # Read-only: Sales data comes from other systems 52 | 53 | # Monthly reports - aggregated data 54 | - name: monthly_reports 55 | crud: 56 | - get # View reports: GET /monthly_reports/ 57 | # Read-only: Reports are generated by batch processes 58 | -------------------------------------------------------------------------------- /examples/01_rest_crud_basic.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String 2 | 3 | from lightapi.core import LightApi 4 | from lightapi.models import Base 5 | from lightapi.rest import RestEndpoint 6 | 7 | 8 | # Define a model that inherits from Base and RestEndpoint 9 | class User(Base, RestEndpoint): 10 | __tablename__ = "users" 11 | __table_args__ = {"extend_existing": True} 12 | 13 | id = Column(Integer, primary_key=True) 14 | name = Column(String(100)) 15 | email = Column(String(100)) 16 | role = Column(String(50)) 17 | 18 | # The default implementation already includes: 19 | # - GET: List all users or get a specific user by ID 20 | # - POST: Create a new user 21 | # - PUT: Update an existing user 22 | # - DELETE: Delete a user 23 | # - OPTIONS: Return allowed methods 24 | 25 | 26 | def _print_usage(): 27 | """Print usage instructions.""" 28 | print("🚀 Basic REST API Started") 29 | print("Server running at http://localhost:8000") 30 | print("API documentation available at http://localhost:8000/docs") 31 | print("\nTry these endpoints:") 32 | print(" curl http://localhost:8000/users/") 33 | print(" curl -X POST http://localhost:8000/users/ -H 'Content-Type: application/json' -d '{\"name\": \"John\", \"email\": \"john@example.com\"}'") 34 | 35 | 36 | if __name__ == "__main__": 37 | # Initialize the API with SQLite database 38 | app = LightApi( 39 | database_url="sqlite:///basic_example.db", 40 | swagger_title="Basic REST API Example", 41 | swagger_version="1.0.0", 42 | swagger_description="Simple REST API demonstrating basic CRUD operations", 43 | ) 44 | 45 | # Register our endpoint 46 | app.register(User) 47 | 48 | _print_usage() 49 | 50 | # Run the server 51 | app.run(host="localhost", port=8000, debug=True) 52 | -------------------------------------------------------------------------------- /docs/advanced/validation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Request Validation 3 | --- 4 | 5 | LightAPI supports request data validation by plugging in a `validator_class` in your endpoint's `Configuration`. Validators inherit from the base `Validator` and define `validate_` methods. 6 | 7 | ## 1. Creating a Validator 8 | 9 | ```python 10 | # app/validators.py 11 | from lightapi.rest import Validator 12 | 13 | class UserValidator(Validator): 14 | def validate_username(self, value: str) -> str: 15 | if not value: 16 | raise ValueError("Username cannot be empty") 17 | return value.strip() 18 | 19 | def validate_email(self, value: str) -> str: 20 | if "@" not in value: 21 | raise ValueError("Invalid email address") 22 | return value.lower() 23 | ``` 24 | 25 | ## 2. Enabling Validation 26 | 27 | Configure your endpoint to use the validator: 28 | 29 | ```python 30 | from lightapi.rest import RestEndpoint 31 | from app.validators import UserValidator 32 | 33 | class UserEndpoint(Base, RestEndpoint): 34 | class Configuration: 35 | validator_class = UserValidator 36 | 37 | async def post(self, request): 38 | # Data is automatically validated before creating the instance 39 | data = request.data 40 | # If validation fails, returns a 400 error with the exception message 41 | return super().post(request) 42 | ``` 43 | 44 | ## 3. Error Handling 45 | 46 | - If a `validate_` method raises `ValueError`, LightAPI catches it, rolls back the transaction, and returns a 400 Bad Request with the error message. 47 | - Unrecognized fields are passed through unchanged. 48 | 49 | ## 4. Custom Validation Patterns 50 | 51 | - You can also override the `validate(self, data: dict)` method directly for full-body validation. 52 | - Combine with filtering and pagination for robust endpoint logic. 53 | -------------------------------------------------------------------------------- /docs/advanced/filtering.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Request Filtering 3 | --- 4 | 5 | LightAPI supports request filtering for list endpoints by plugging in a `filter_class` in your endpoint's `Configuration`. The built-in `ParameterFilter` applies filters based on URL query parameters. 6 | 7 | ## ParameterFilter 8 | 9 | The `ParameterFilter` inspects query parameters (e.g., `?status=completed&category=books`) and applies them to the SQLAlchemy query by matching parameter names to model attributes: 10 | 11 | ```python 12 | from lightapi.rest import RestEndpoint 13 | from lightapi.filters import ParameterFilter 14 | 15 | class TaskEndpoint(Base, RestEndpoint): 16 | class Configuration: 17 | filter_class = ParameterFilter 18 | 19 | async def get(self, request): 20 | # Default GET will apply filters automatically 21 | return super().get(request) 22 | ``` 23 | 24 | With `GET /tasks/?status=completed`, the `ParameterFilter` adds a filter clause equivalent to: 25 | 26 | ```python 27 | query.filter(Task.status == "completed") 28 | ``` 29 | 30 | ## Custom Filters 31 | 32 | For more complex filtering logic, subclass `BaseFilter` and override `filter_queryset`: 33 | 34 | ```python 35 | from lightapi.filters import BaseFilter 36 | 37 | class DateRangeFilter(BaseFilter): 38 | def filter_queryset(self, queryset, request): 39 | start = request.query_params.get("start_date") 40 | end = request.query_params.get("end_date") 41 | if start and end: 42 | query = queryset.filter( 43 | Task.created_at.between(start, end) 44 | ) 45 | return query 46 | return queryset 47 | 48 | class TaskEndpoint(Base, RestEndpoint): 49 | class Configuration: 50 | filter_class = DateRangeFilter 51 | ``` 52 | 53 | Custom filters give you full control over how querysets are restricted based on request data. 54 | -------------------------------------------------------------------------------- /examples/db_multi_database_config.yaml: -------------------------------------------------------------------------------- 1 | # Multi-Database Configuration 2 | # Demonstrates switching between database types using environment variables 3 | 4 | # Database URL determined by environment 5 | database_url: "${DATABASE_URL}" 6 | 7 | swagger_title: "Multi-Database Company API" 8 | swagger_version: "3.0.0" 9 | swagger_description: | 10 | Flexible company management API supporting multiple database backends 11 | 12 | ## Supported Databases 13 | 14 | ### SQLite (Development) 15 | ``` 16 | DATABASE_URL=sqlite:///company.db 17 | ``` 18 | - File-based storage 19 | - Zero configuration 20 | - Perfect for development and testing 21 | 22 | ### PostgreSQL (Production) 23 | ``` 24 | DATABASE_URL=postgresql://user:pass@host:port/db 25 | ``` 26 | - Enterprise-grade features 27 | - Advanced SQL support 28 | - High availability options 29 | 30 | ### MySQL (Alternative Production) 31 | ``` 32 | DATABASE_URL=mysql+pymysql://user:pass@host:port/db 33 | ``` 34 | - High performance 35 | - Wide ecosystem support 36 | - Proven scalability 37 | 38 | ## Environment Variables 39 | - `DATABASE_URL`: Database connection string 40 | - `DB_TYPE`: Database type (sqlite|postgresql|mysql) 41 | - `DB_HOST`: Database host 42 | - `DB_PORT`: Database port 43 | - `DB_NAME`: Database name 44 | - `DB_USER`: Database username 45 | - `DB_PASS`: Database password 46 | enable_swagger: true 47 | 48 | tables: 49 | # Universal table configuration works with all database types 50 | - name: companies 51 | crud: 52 | - get 53 | - post 54 | - put 55 | - patch 56 | - delete 57 | 58 | - name: employees 59 | crud: 60 | - get 61 | - post 62 | - put 63 | - patch 64 | - delete 65 | 66 | - name: projects 67 | crud: 68 | - get 69 | - post 70 | - put 71 | - patch 72 | - delete 73 | -------------------------------------------------------------------------------- /lightapi/filters.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import Query 2 | 3 | 4 | class BaseFilter: 5 | """ 6 | Base class for query filters. 7 | 8 | Provides a common interface for all filtering methods. 9 | By default, returns the queryset unchanged. 10 | """ 11 | 12 | def filter_queryset(self, queryset: Query, request) -> Query: 13 | """ 14 | Filter a database queryset based on the request. 15 | 16 | Args: 17 | queryset: The SQLAlchemy query to filter. 18 | request: The HTTP request containing filter parameters. 19 | 20 | Returns: 21 | Query: The filtered query. 22 | """ 23 | return queryset 24 | 25 | 26 | class ParameterFilter(BaseFilter): 27 | """ 28 | Filter queryset based on request query parameters. 29 | 30 | Automatically filters the queryset using query parameters that 31 | match model field names, performing exact matching. 32 | """ 33 | 34 | def filter_queryset(self, queryset: Query, request) -> Query: 35 | """ 36 | Filter a database queryset based on request query parameters. 37 | 38 | For each query parameter that matches a model field name, 39 | the queryset is filtered to records where that field equals 40 | the parameter value. 41 | 42 | Args: 43 | queryset: The SQLAlchemy query to filter. 44 | request: The HTTP request containing filter parameters. 45 | 46 | Returns: 47 | Query: The filtered query. 48 | """ 49 | query_params = dict(request.query_params) 50 | if not query_params: 51 | return queryset 52 | 53 | entity = queryset.column_descriptions[0]["entity"] 54 | result = None 55 | for param, value in query_params.items(): 56 | if hasattr(entity, param): 57 | result = queryset.filter(getattr(entity, param) == value) 58 | return result if result is not None else queryset 59 | -------------------------------------------------------------------------------- /docs/api-reference/index.md: -------------------------------------------------------------------------------- 1 | # API Reference Overview 2 | 3 | This section provides detailed documentation for all LightAPI modules and components. 4 | 5 | ## Core Modules 6 | 7 | ### Core API 8 | The foundation of LightAPI, providing essential functionality for application setup and configuration. 9 | 10 | ### REST API 11 | Tools and utilities for building RESTful endpoints with built-in support for CRUD operations. 12 | 13 | ### Authentication 14 | Secure authentication implementation with support for JWT and Basic authentication. 15 | 16 | ### Database 17 | Database integration and ORM support for data persistence. 18 | 19 | ### Caching 20 | Redis-based caching system for improved performance. 21 | 22 | ### Filtering 23 | Advanced filtering capabilities for data queries. 24 | 25 | ### Pagination 26 | Built-in pagination support for large datasets. 27 | 28 | ### Swagger Integration 29 | Automatic API documentation generation. 30 | 31 | ### Models 32 | Data model definitions and schema validation. 33 | 34 | ### Exceptions 35 | Error handling and custom exception definitions. 36 | 37 | ## Usage Guidelines 38 | 39 | 1. Always refer to the specific module documentation for detailed usage instructions 40 | 2. Follow the provided examples for common use cases 41 | 3. Implement proper error handling 42 | 4. Use the built-in utilities whenever possible 43 | 5. Follow the best practices outlined in each module's documentation 44 | 45 | ## Getting Started 46 | 47 | 1. Review the [Core API](core.md) documentation for basic setup 48 | 2. Implement [REST API](rest.md) endpoints for your resources 49 | 3. Add [Authentication](auth.md) for secure access 50 | 4. Configure [Database](database.md) integration 51 | 5. Implement [Caching](cache.md) for performance optimization 52 | 53 | ## See Also 54 | 55 | - [Getting Started](../getting-started/introduction.md) - Framework introduction 56 | - [Tutorial](../tutorial/basic-api.md) - Step-by-step guide 57 | - [Advanced Topics](../advanced/authentication.md) - Advanced features 58 | -------------------------------------------------------------------------------- /docs/deployment/security.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Security Considerations 3 | --- 4 | 5 | This guide outlines best practices for securing your LightAPI application in production. 6 | 7 | ## 1. Secure Communication (TLS) 8 | Always serve your API over HTTPS to encrypt data in transit. If using a reverse proxy (e.g., Nginx), configure SSL certificates: 9 | 10 | ```nginx 11 | server { 12 | listen 443 ssl; 13 | server_name api.example.com; 14 | 15 | ssl_certificate /etc/ssl/fullchain.pem; 16 | ssl_certificate_key /etc/ssl/privkey.pem; 17 | 18 | location / { 19 | proxy_pass http://127.0.0.1:8000; 20 | proxy_set_header Host $host; 21 | proxy_set_header X-Real-IP $remote_addr; 22 | } 23 | } 24 | ``` 25 | 26 | ## 2. Cross-Origin Resource Sharing (CORS) 27 | Control which domains can access your API. Use Starlette's `CORSMiddleware`: 28 | 29 | ```python 30 | from starlette.middleware.cors import CORSMiddleware 31 | from lightapi import LightApi 32 | 33 | app = LightApi() 34 | app.app.add_middleware( 35 | CORSMiddleware, 36 | allow_origins=["https://example.com"], 37 | allow_methods=["*"], 38 | allow_headers=["*"], 39 | ) 40 | ``` 41 | 42 | ## 3. Input Validation & Sanitization 43 | Always validate and sanitize inputs using `validator_class` to prevent SQL injection and ensure data integrity. 44 | 45 | ## 4. Authentication & Authorization 46 | - Use strong, randomly generated secrets for JWT (`LIGHTAPI_JWT_SECRET`). 47 | - Protect endpoints with `authentication_class` and implement role checks in your logic. 48 | 49 | ## 5. Rate Limiting 50 | Thwart abuse by implementing rate limiting. Options include: 51 | - Nginx rate limiting 52 | - ASGI middleware (e.g., `slowapi`) 53 | 54 | ## 6. Secret Management 55 | Manage secrets and credentials via environment variables or secret managers (e.g., Vault, AWS Secrets Manager). Do not commit secrets to source control. 56 | 57 | ## 7. Logging & Monitoring 58 | Enable structured logging and monitor metrics. Use tools like Prometheus, Grafana, or ELK stack to track performance and detect anomalies. 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/**/workspace.xml 2 | .idea/**/tasks.xml 3 | .idea/**/usage.statistics.xml 4 | .idea/**/dictionaries 5 | .idea/**/shelf 6 | .idea/**/aws.xml 7 | .idea/**/contentModel.xml 8 | .idea/**/dataSources/ 9 | .idea/**/dataSources.ids 10 | .idea/**/dataSources.local.xml 11 | .idea/**/sqlDataSources.xml 12 | .idea/**/dynamic.xml 13 | .idea/**/uiDesigner.xml 14 | .idea/**/dbnavigator.xml 15 | .idea/**/gradle.xml 16 | .idea/**/libraries 17 | cmake-build-*/ 18 | .idea/**/mongoSettings.xml 19 | *.iws 20 | out/ 21 | .idea_modules/ 22 | atlassian-ide-plugin.xml 23 | .idea/replstate.xml 24 | .idea/sonarlint/ 25 | com_crashlytics_export_strings.xml 26 | crashlytics.properties 27 | crashlytics-build.properties 28 | fabric.properties 29 | .idea/httpRequests 30 | .idea/caches/build_file_checksums.ser 31 | __pycache__/ 32 | *.py[cod] 33 | *$py.class 34 | *.so 35 | .Python 36 | build/ 37 | develop-eggs/ 38 | dist/ 39 | downloads/ 40 | eggs/ 41 | .eggs/ 42 | lib/ 43 | lib64/ 44 | parts/ 45 | sdist/ 46 | var/ 47 | wheels/ 48 | share/python-wheels/ 49 | *.egg-info/ 50 | .installed.cfg 51 | *.egg 52 | MANIFEST 53 | *.manifest 54 | *.spec 55 | pip-log.txt 56 | pip-delete-this-directory.txt 57 | htmlcov/ 58 | .tox/ 59 | .nox/ 60 | .coverage 61 | .coverage.* 62 | .cache 63 | nosetests.xml 64 | coverage.xml 65 | *.cover 66 | *.py,cover 67 | .hypothesis/ 68 | .pytest_cache/ 69 | cover/ 70 | *.mo 71 | *.pot 72 | *.log 73 | local_settings.py 74 | db.sqlite3 75 | db.sqlite3-journal 76 | instance/ 77 | .webassets-cache 78 | .scrapy 79 | docs/_build/ 80 | .pybuilder/ 81 | target/ 82 | .ipynb_checkpoints 83 | profile_default/ 84 | ipython_config.py 85 | .pdm.toml 86 | __pypackages__/ 87 | celerybeat-schedule 88 | celerybeat.pid 89 | *.sage.py 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | .spyderproject 98 | .spyproject 99 | .ropeproject 100 | /site 101 | .mypy_cache/ 102 | .dmypy.json 103 | dmypy.json 104 | .pyre/ 105 | .pytype/ 106 | cython_debug/ 107 | .idea 108 | .idea 109 | alembic 110 | *.db 111 | -------------------------------------------------------------------------------- /docs/advanced/pagination.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Data Pagination 3 | --- 4 | 5 | LightAPI includes a built-in pagination utility via the `Paginator` class. You can plug this into any `RestEndpoint` to limit and offset large querysets. 6 | 7 | ## 1. Enabling Pagination 8 | 9 | Add `pagination_class` to your endpoint's `Configuration`: 10 | 11 | ```python 12 | from lightapi.rest import RestEndpoint 13 | from lightapi.pagination import Paginator 14 | 15 | class ItemEndpoint(Base, RestEndpoint): 16 | class Configuration: 17 | pagination_class = Paginator 18 | 19 | async def get(self, request): 20 | # Default GET will use Paginator to limit results 21 | return super().get(request) 22 | ``` 23 | 24 | ## 2. Configuring Limits and Offsets 25 | 26 | The `Paginator` uses its `limit` and `offset` attributes to control pagination. You can customize these values at runtime by modifying the instance: 27 | 28 | ```python 29 | class CustomPaginator(Paginator): 30 | def get_limit(self) -> int: 31 | # Read limit from query params or fallback to default 32 | return int(self.request.query_params.get('limit', self.limit)) 33 | 34 | def get_offset(self) -> int: 35 | return int(self.request.query_params.get('offset', self.offset)) 36 | ``` 37 | 38 | Then assign your custom paginator: 39 | 40 | ```python 41 | class ItemEndpoint(Base, RestEndpoint): 42 | class Configuration: 43 | pagination_class = CustomPaginator 44 | ``` 45 | 46 | ## 3. Sorting Results 47 | 48 | By default, `Paginator.sort` is `False`. Enable sorting in a subclass to apply ordering: 49 | 50 | ```python 51 | class SortedPaginator(Paginator): 52 | sort = True 53 | def apply_sorting(self, queryset): 54 | # Example: sort by 'created_at' field 55 | return queryset.order_by(self.model.created_at.desc()) 56 | ``` 57 | 58 | Use it in your endpoint: 59 | 60 | ```python 61 | class ItemEndpoint(Base, RestEndpoint): 62 | class Configuration: 63 | pagination_class = SortedPaginator 64 | ``` 65 | 66 | Pagination helps control memory usage and response size when dealing with large datasets. 67 | -------------------------------------------------------------------------------- /tests/test_additional_features.py: -------------------------------------------------------------------------------- 1 | import json 2 | from unittest.mock import MagicMock 3 | 4 | import pytest 5 | import redis 6 | from starlette.applications import Starlette 7 | from starlette.routing import Route 8 | from starlette.testclient import TestClient 9 | 10 | from lightapi.cache import RedisCache 11 | from lightapi.core import Middleware, Response 12 | from lightapi.filters import ParameterFilter 13 | from lightapi.lightapi import LightApi 14 | from lightapi.rest import RestEndpoint 15 | 16 | 17 | class DummyRedis: 18 | def __init__(self, *args, **kwargs): 19 | self.store = {} 20 | self.setex_count = 0 21 | 22 | def get(self, key): 23 | return self.store.get(key) 24 | 25 | def setex(self, key, timeout, value): 26 | self.setex_count += 1 27 | self.store[key] = value 28 | return True 29 | 30 | 31 | def test_response_asgi_call(): 32 | async def endpoint(request): 33 | return Response({"hello": "world"}) 34 | 35 | app = Starlette(routes=[Route("/", endpoint)]) 36 | with TestClient(app) as client: 37 | resp = client.get("/") 38 | assert resp.status_code == 200 39 | assert resp.json() == {"hello": "world"} 40 | 41 | 42 | def test_jwt_auth_missing_secret(monkeypatch): 43 | from lightapi import auth 44 | 45 | monkeypatch.setattr(auth.config, "jwt_secret", None) 46 | with pytest.raises(ValueError): 47 | auth.JWTAuthentication() 48 | 49 | 50 | def test_parameter_filter_ignores_unknown(): 51 | filter_obj = ParameterFilter() 52 | query = MagicMock() 53 | entity = type("E", (), {"name": "n"}) 54 | query.column_descriptions = [{"entity": entity}] 55 | filtered = MagicMock() 56 | query.filter.return_value = filtered 57 | request = type("Req", (), {"query_params": {"name": "a", "unknown": "x"}})() 58 | 59 | result = filter_obj.filter_queryset(query, request) 60 | query.filter.assert_called_once() 61 | assert result == filtered 62 | 63 | 64 | def test_response_decode(): 65 | data = {"foo": "bar"} 66 | resp = Response(data) 67 | assert resp.decode() == json.dumps(data) 68 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock 2 | 3 | from sqlalchemy import Column, Integer, String 4 | 5 | from lightapi.auth import JWTAuthentication 6 | from lightapi.cache import RedisCache 7 | from lightapi.filters import ParameterFilter 8 | from lightapi.pagination import Paginator 9 | from lightapi.rest import RestEndpoint, Validator 10 | 11 | 12 | def create_mock_request(method="GET", data=None, headers=None, query_params=None, path_params=None): 13 | mock_request = MagicMock() 14 | mock_request.method = method 15 | mock_request.data = data or {} 16 | mock_request.headers = headers or {} 17 | mock_request.query_params = query_params or {} 18 | mock_request.path_params = path_params or {} 19 | return mock_request 20 | 21 | 22 | def create_mock_session(): 23 | mock_session = MagicMock() 24 | mock_query = MagicMock() 25 | mock_session.query.return_value = mock_query 26 | mock_filter = MagicMock() 27 | mock_query.filter_by.return_value = mock_filter 28 | mock_first = MagicMock() 29 | mock_filter.first.return_value = mock_first 30 | mock_all = [] 31 | mock_query.all.return_value = mock_all 32 | 33 | return mock_session 34 | 35 | 36 | class TestPaginator(Paginator): 37 | limit = 5 38 | sort = True 39 | 40 | 41 | class TestEndpoint(RestEndpoint): 42 | __tablename__ = "test_models" 43 | 44 | id = Column(Integer, primary_key=True) 45 | name = Column(String) 46 | email = Column(String, unique=True) 47 | 48 | class Configuration: 49 | http_method_names = ["GET", "POST", "PUT", "DELETE"] 50 | validator_class = Validator 51 | pagination_class = TestPaginator 52 | filter_class = ParameterFilter 53 | authentication_class = JWTAuthentication 54 | caching_class = RedisCache 55 | caching_method_names = ["GET"] 56 | 57 | 58 | def setup_endpoint(endpoint_class=TestEndpoint, session=None, request=None): 59 | if session is None: 60 | session = create_mock_session() 61 | if request is None: 62 | request = create_mock_request() 63 | 64 | endpoint = endpoint_class() 65 | endpoint._setup(request, session) 66 | return endpoint 67 | -------------------------------------------------------------------------------- /docs/getting-started/first-steps.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: First Steps 3 | --- 4 | 5 | In this guide, you'll explore LightAPI's core concepts by creating a simple project from scratch. 6 | 7 | ## 1. Project Layout 8 | 9 | A minimal project structure might look like: 10 | 11 | ``` 12 | myapp/ 13 | ├── app/ 14 | │ ├── __init__.py 15 | │ ├── models.py 16 | │ └── main.py 17 | ├── requirements.txt 18 | └── README.md 19 | ``` 20 | 21 | - **app/models.py**: Define your SQLAlchemy models here. 22 | - **app/main.py**: Create and configure the LightAPI application. 23 | 24 | ## 2. Defining Your First Model 25 | 26 | In `app/models.py`, define a simple `User` model: 27 | 28 | ```python 29 | # app/models.py 30 | from sqlalchemy import Column, Integer, String 31 | from lightapi.database import Base 32 | 33 | class User(Base): 34 | id = Column(Integer, primary_key=True, index=True) 35 | username = Column(String, unique=True, index=True) 36 | email = Column(String, unique=True, index=True) 37 | ``` 38 | 39 | This class inherits from `Base`, which includes: 40 | 41 | - SQLAlchemy metadata 42 | - Default `__tablename__` generation (snake_case of the class name) 43 | 44 | ## 3. Creating the Application 45 | 46 | In `app/main.py`, register the model and start the server: 47 | 48 | ```python 49 | # app/main.py 50 | from lightapi import LightApi 51 | from app.models import User 52 | 53 | app = LightApi() 54 | app.register({ 55 | '/users': User 56 | }) 57 | 58 | if __name__ == '__main__': 59 | app.run(host='127.0.0.1', port=8000) 60 | ``` 61 | 62 | - `register` automatically generates CRUD routes (GET, POST, PUT, PATCH, DELETE) for `/users`. 63 | - `run` starts an ASGI server (defaults to Uvicorn under the hood). 64 | 65 | ## 4. Testing Your Endpoints 66 | 67 | Start the app: 68 | 69 | ```bash 70 | python app/main.py 71 | ``` 72 | 73 | Then, in a separate terminal, try: 74 | 75 | ```bash 76 | # Create a new user 77 | curl -X POST http://localhost:8000/users/ \ 78 | -H 'Content-Type: application/json' \ 79 | -d '{"username":"alice","email":"alice@example.com"}' 80 | 81 | # Get list of users 82 | curl http://localhost:8000/users/ 83 | 84 | # Retrieve a user by ID 85 | curl http://localhost:8000/users/1 86 | ``` 87 | 88 | You should see JSON responses corresponding to each action. 89 | -------------------------------------------------------------------------------- /lightapi/pagination.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List 2 | 3 | from sqlalchemy.orm import Query 4 | 5 | 6 | class Paginator: 7 | """ 8 | Base class for pagination. 9 | 10 | Provides methods for limiting, offsetting, and sorting database queries. 11 | Can be subclassed to implement custom pagination behavior. 12 | 13 | Attributes: 14 | limit: Maximum number of records to return. 15 | offset: Number of records to skip. 16 | sort: Whether to apply sorting. 17 | """ 18 | 19 | limit = 10 20 | offset = 0 21 | sort = False 22 | 23 | def paginate(self, queryset: Query) -> List[Any]: 24 | """ 25 | Apply pagination to a database query. 26 | 27 | Limits the number of results, applies offset, and 28 | optionally sorts the queryset. 29 | 30 | Args: 31 | queryset: The SQLAlchemy query to paginate. 32 | 33 | Returns: 34 | List[Any]: The paginated list of results. 35 | """ 36 | request_limit = self.get_limit() 37 | request_offset = self.get_offset() 38 | 39 | if self.sort: 40 | queryset = self.apply_sorting(queryset) 41 | 42 | return queryset.limit(request_limit).offset(request_offset).all() 43 | 44 | def get_limit(self) -> int: 45 | """ 46 | Get the limit for pagination. 47 | 48 | Override this method to implement dynamic limits based on the request. 49 | 50 | Returns: 51 | int: The maximum number of records to return. 52 | """ 53 | return self.limit 54 | 55 | def get_offset(self) -> int: 56 | """ 57 | Get the offset for pagination. 58 | 59 | Override this method to implement dynamic offsets based on the request. 60 | 61 | Returns: 62 | int: The number of records to skip. 63 | """ 64 | return self.offset 65 | 66 | def apply_sorting(self, queryset: Query) -> Query: 67 | """ 68 | Apply sorting to the queryset. 69 | 70 | Override this method to implement custom sorting logic. 71 | 72 | Args: 73 | queryset: The SQLAlchemy query to sort. 74 | 75 | Returns: 76 | Query: The sorted query. 77 | """ 78 | return queryset 79 | -------------------------------------------------------------------------------- /docs/tutorial/requests.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Handling Requests 3 | --- 4 | 5 | LightAPI simplifies request handling by automatically parsing incoming data and making parameters accessible. 6 | 7 | ## JSON Payloads 8 | 9 | For `POST`, `PUT`, and `PATCH` methods, LightAPI reads the request body and attempts to parse it as JSON. Parsed data is available on `request.data`: 10 | 11 | ```python 12 | async def post(self, request): 13 | payload = request.data # Dict from JSON body 14 | # Use payload directly 15 | ``` 16 | 17 | If the body is empty or invalid JSON, `request.data` will be an empty dict. 18 | 19 | ## Path Parameters 20 | 21 | When defining endpoints with path parameters (e.g., `/items/{id}`), you can access them via `request.path_params` or `request.match_info`: 22 | 23 | ```python 24 | async def get(self, request): 25 | item_id = request.path_params.get('id') 26 | # or 27 | item_id = request.match_info['id'] 28 | ``` 29 | 30 | For more robust endpoints, especially in testing scenarios, it's recommended to support parameters from both path and query: 31 | 32 | ```python 33 | def get(self, request): 34 | # First check path parameters 35 | item_id = None 36 | if hasattr(request, 'path_params'): 37 | item_id = request.path_params.get('id') 38 | 39 | # If not found, check query parameters 40 | if not item_id and hasattr(request, 'query_params'): 41 | item_id = request.query_params.get('id') 42 | 43 | # Use a default if still not found 44 | if not item_id: 45 | item_id = 'default' 46 | ``` 47 | 48 | ## Query Parameters 49 | 50 | Query parameters (e.g., `?limit=10&sort=asc`) are available via: 51 | 52 | ```python 53 | params = dict(request.query_params) 54 | limit = params.get('limit') 55 | sort_order = params.get('sort') 56 | ``` 57 | 58 | You can also leverage the built-in `ParameterFilter` (see Advanced → Request Filtering) to automatically apply filters based on query parameters. 59 | 60 | ## Request Headers 61 | 62 | You can inspect headers directly from the `request` object: 63 | 64 | ```python 65 | auth_header = request.headers.get('Authorization') 66 | user_agent = request.headers.get('User-Agent') 67 | ``` 68 | 69 | This allows you to implement custom authentication, content negotiation, or other header-based logic. 70 | -------------------------------------------------------------------------------- /tests/test_cache.py: -------------------------------------------------------------------------------- 1 | import json 2 | from unittest.mock import MagicMock, patch 3 | 4 | import pytest 5 | 6 | from lightapi.cache import BaseCache, RedisCache 7 | 8 | 9 | class TestCache: 10 | def test_base_cache_get(self): 11 | cache = BaseCache() 12 | result = cache.get("test_key") 13 | assert result is None 14 | 15 | def test_base_cache_set(self): 16 | cache = BaseCache() 17 | result = cache.set("test_key", {"data": "value"}, 300) 18 | assert result is True 19 | 20 | @patch("redis.Redis") 21 | def test_redis_cache_init(self, mock_redis): 22 | mock_redis_instance = MagicMock() 23 | mock_redis.return_value = mock_redis_instance 24 | cache = RedisCache() 25 | mock_redis.assert_called_once_with(host="localhost", port=6379, db=0) 26 | assert cache.client == mock_redis_instance 27 | 28 | @patch("redis.Redis") 29 | def test_redis_cache_get_with_data(self, mock_redis): 30 | mock_redis_instance = MagicMock() 31 | mock_redis.return_value = mock_redis_instance 32 | cached_data = json.dumps({"test": "data"}).encode() 33 | mock_redis_instance.get.return_value = cached_data 34 | cache = RedisCache() 35 | result = cache.get("test_key") 36 | mock_redis_instance.get.assert_called_once() 37 | assert "test_key" in str(mock_redis_instance.get.call_args) 38 | assert result == {"test": "data"} 39 | 40 | @patch("redis.Redis") 41 | def test_redis_cache_get_no_data(self, mock_redis): 42 | mock_redis_instance = MagicMock() 43 | mock_redis.return_value = mock_redis_instance 44 | mock_redis_instance.get.return_value = None 45 | cache = RedisCache() 46 | result = cache.get("test_key") 47 | mock_redis_instance.get.assert_called_once() 48 | assert result is None 49 | 50 | @patch("redis.Redis") 51 | def test_redis_cache_set(self, mock_redis): 52 | mock_redis_instance = MagicMock() 53 | mock_redis.return_value = mock_redis_instance 54 | mock_redis_instance.setex.return_value = True 55 | cache = RedisCache() 56 | result = cache.set("test_key", {"test": "data"}, 300) 57 | mock_redis_instance.setex.assert_called_once() 58 | assert result is True 59 | -------------------------------------------------------------------------------- /lightapi/database.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | 4 | from sqlalchemy import Column, Integer, create_engine 5 | from sqlalchemy.orm import as_declarative, declared_attr, sessionmaker 6 | 7 | from .config import config 8 | 9 | engine = create_engine(config.database_url) 10 | SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) 11 | 12 | 13 | @as_declarative() 14 | class Base: 15 | """ 16 | Custom SQLAlchemy base class for all models. 17 | 18 | Provides automatic __tablename__ generation and utility methods 19 | for model instances to make working with SQLAlchemy models easier. 20 | 21 | Attributes: 22 | __table__: SQLAlchemy table metadata. 23 | table: Property that returns the table metadata. 24 | __tablename__: Automatically generated based on class name. 25 | """ 26 | 27 | __table__ = None 28 | 29 | id = Column(Integer, primary_key=True, autoincrement=True) 30 | 31 | @property 32 | def table(self): 33 | """ 34 | Get the table metadata for this model. 35 | 36 | Returns: 37 | The SQLAlchemy Table object for this model. 38 | """ 39 | return self.__table__ 40 | 41 | @declared_attr 42 | def __tablename__(cls): 43 | """ 44 | Generate the table name based on the class name. 45 | 46 | The table name is derived by converting the class name to lowercase. 47 | 48 | Returns: 49 | str: The generated table name. 50 | """ 51 | return cls.__name__.lower() 52 | 53 | @property 54 | def pk(self): 55 | return self.id 56 | 57 | def serialize(self) -> dict: 58 | """ 59 | Convert the model instance into a dictionary representation. 60 | 61 | Each key in the dictionary corresponds to a column name, and the value 62 | is the data stored in that column. Datetime objects are converted to strings. 63 | 64 | Returns: 65 | dict: A dictionary representation of the model instance. 66 | """ 67 | return { 68 | column.name: ( 69 | getattr(self, column.name).isoformat() if isinstance(getattr(self, column.name), datetime) else getattr(self, column.name) 70 | ) 71 | for column in self.table.columns 72 | } 73 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from lightapi.config import config 6 | 7 | # Test configuration 8 | TEST_JWT_SECRET = "test_secret_key_for_testing" 9 | TEST_DATABASE_URL = "sqlite:///:memory:" 10 | 11 | 12 | @pytest.fixture(autouse=True) 13 | def setup_test_env(): 14 | """Set up test environment variables and configuration.""" 15 | # Store original values 16 | original_env = { 17 | "LIGHTAPI_JWT_SECRET": os.environ.get("LIGHTAPI_JWT_SECRET"), 18 | "LIGHTAPI_ENV": os.environ.get("LIGHTAPI_ENV"), 19 | "LIGHTAPI_DATABASE_URL": os.environ.get("LIGHTAPI_DATABASE_URL"), 20 | } 21 | 22 | # Set test values 23 | os.environ["LIGHTAPI_JWT_SECRET"] = TEST_JWT_SECRET 24 | os.environ["LIGHTAPI_ENV"] = "test" 25 | os.environ["LIGHTAPI_DATABASE_URL"] = TEST_DATABASE_URL 26 | 27 | # Update config directly 28 | config.update(jwt_secret=TEST_JWT_SECRET, database_url=TEST_DATABASE_URL) 29 | 30 | yield 31 | 32 | # Restore original values 33 | for key, value in original_env.items(): 34 | if value is None: 35 | os.environ.pop(key, None) 36 | else: 37 | os.environ[key] = value 38 | 39 | 40 | def pytest_configure(config): 41 | """ 42 | Configure pytest settings before test collection begins. 43 | 44 | This function adds configuration to ignore pytest collection warnings 45 | related to test classes that have similar names to actual test fixtures 46 | but aren't intended to be collected, such as model classes in test files. 47 | 48 | Args: 49 | config: The pytest config object. 50 | """ 51 | config.addinivalue_line("filterwarnings", "ignore::pytest.PytestCollectionWarning") 52 | 53 | 54 | def pytest_collect_file(parent, file_path): 55 | """ 56 | Control how pytest collects test files. 57 | 58 | This hook can be used to skip certain files or implement custom 59 | collection logic. In this implementation, we return None for files 60 | that shouldn't be collected as test files, preventing test collection 61 | conflicts with model classes. 62 | 63 | Args: 64 | parent: The parent collector node. 65 | file_path: Path to the file (pathlib.Path). 66 | 67 | Returns: 68 | None: To indicate the file should not be collected. 69 | """ 70 | return None 71 | -------------------------------------------------------------------------------- /docs/tutorial/endpoints.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Creating Endpoints 3 | --- 4 | 5 | LightAPI auto-generates standard CRUD routes when you register SQLAlchemy models, but you can also define custom endpoints by subclassing the `RestEndpoint` class. 6 | 7 | ## Subclassing RestEndpoint 8 | 9 | ```python 10 | # app/endpoints/custom_user.py 11 | from lightapi.rest import RestEndpoint 12 | 13 | class CustomUserEndpoint(Base, RestEndpoint): 14 | tablename = 'users' 15 | # Only allow GET and POST methods 16 | http_method_names = ['GET', 'POST'] 17 | 18 | async def get(self, request): 19 | return {'message': 'Custom GET endpoint'} 20 | 21 | async def post(self, request): 22 | data = await request.json() 23 | return {'received': data} 24 | ``` 25 | 26 | ## Registering Custom Endpoints 27 | 28 | ```python 29 | from lightapi import LightApi 30 | from app.endpoints.custom_user import CustomUserEndpoint 31 | 32 | app = LightApi() 33 | app.register({'/custom-users': CustomUserEndpoint}) 34 | ``` 35 | 36 | ## Registering Custom Endpoints with route_patterns 37 | 38 | When defining a custom endpoint (not a SQLAlchemy model), always specify the intended path(s) using the `route_patterns` attribute: 39 | 40 | ```python 41 | class HelloWorldEndpoint(Base, RestEndpoint): 42 | route_patterns = ["/hello"] 43 | def get(self, request): 44 | return {"message": "Hello, World!"} 45 | 46 | app.register(HelloWorldEndpoint) 47 | ``` 48 | 49 | > See the mega example for a comprehensive demonstration of this pattern. 50 | 51 | ## HTTP Method Configuration 52 | 53 | - `http_method_names`: List of allowed HTTP methods. 54 | - `http_exclude`: List of methods to exclude from the default set. 55 | 56 | ```python 57 | class ReadOnlyEndpoint(Base, RestEndpoint): 58 | tablename = 'items' 59 | http_method_names = ['GET'] 60 | ``` 61 | 62 | ## Accessing Path Parameters 63 | 64 | You can retrieve path parameters from `request.match_info`: 65 | 66 | ```python 67 | async def get(self, request): 68 | item_id = request.match_info['id'] 69 | # Use item_id in your logic 70 | ``` 71 | 72 | ## Custom Route Prefixes 73 | 74 | You can add a common prefix to routes when registering: 75 | 76 | ```python 77 | app.register( 78 | {'/v2/items': Item}, 79 | prefix='/api' 80 | ) 81 | # Endpoints will be mounted at /api/v2/items/... 82 | ``` 83 | -------------------------------------------------------------------------------- /lightapi/config.py: -------------------------------------------------------------------------------- 1 | """Configuration management for LightAPI.""" 2 | import json 3 | import os 4 | from typing import List, Optional, Union 5 | 6 | 7 | class Config: 8 | """Configuration class that handles environment variables and defaults.""" 9 | 10 | def __init__(self): 11 | # Server settings 12 | self.host: str = os.getenv("LIGHTAPI_HOST", "127.0.0.1") 13 | self.port: int = int(os.getenv("LIGHTAPI_PORT", "8000")) 14 | self.debug: bool = self._parse_bool(os.getenv("LIGHTAPI_DEBUG", "False")) 15 | self.reload: bool = self._parse_bool(os.getenv("LIGHTAPI_RELOAD", "False")) 16 | 17 | # Database settings 18 | self.database_url: str = os.getenv("LIGHTAPI_DATABASE_URL", "sqlite:///./app.db") 19 | 20 | # CORS settings 21 | self.cors_origins: List[str] = self._parse_list(os.getenv("LIGHTAPI_CORS_ORIGINS", "[]")) 22 | 23 | # JWT settings 24 | self.jwt_secret: Optional[str] = os.getenv("LIGHTAPI_JWT_SECRET") 25 | 26 | # Swagger settings 27 | self.swagger_title: str = os.getenv("LIGHTAPI_SWAGGER_TITLE", "LightAPI Documentation") 28 | self.swagger_version: str = os.getenv("LIGHTAPI_SWAGGER_VERSION", "1.0.0") 29 | self.swagger_description: str = os.getenv("LIGHTAPI_SWAGGER_DESCRIPTION", "API automatic documentation") 30 | self.enable_swagger: bool = self._parse_bool(os.getenv("LIGHTAPI_ENABLE_SWAGGER", "True")) 31 | 32 | # Cache settings 33 | self.cache_timeout: int = int(os.getenv("LIGHTAPI_CACHE_TIMEOUT", "3600")) # Default 1 hour 34 | 35 | @staticmethod 36 | def _parse_bool(value: str) -> bool: 37 | """Parse string to boolean.""" 38 | return value.lower() in ("true", "1", "yes", "on") 39 | 40 | @staticmethod 41 | def _parse_list(value: str) -> List[str]: 42 | """Parse JSON string to list.""" 43 | try: 44 | result = json.loads(value) 45 | if isinstance(result, list): 46 | return result 47 | return [] 48 | except json.JSONDecodeError: 49 | return [] 50 | 51 | def update(self, **kwargs): 52 | """Update configuration with provided values.""" 53 | for key, value in kwargs.items(): 54 | if hasattr(self, key): 55 | setattr(self, key, value) 56 | 57 | 58 | # Global configuration instance 59 | config = Config() 60 | -------------------------------------------------------------------------------- /docs/examples/basic-crud.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Basic CRUD Examples 3 | --- 4 | 5 | # Basic CRUD Operations 6 | 7 | This example demonstrates basic Create, Read, Update, and Delete (CRUD) operations using a LightAPI-generated endpoint for an `Item` model. 8 | 9 | ## Prerequisites 10 | 11 | Assuming you have an `Item` model registered at `/items`: 12 | 13 | ```python 14 | # app/main.py 15 | from lightapi import LightApi 16 | from sqlalchemy import Column, Integer, String 17 | from lightapi.database import Base 18 | 19 | class Item(Base): 20 | id = Column(Integer, primary_key=True, index=True) 21 | name = Column(String, unique=True) 22 | 23 | app = LightApi() 24 | app.register({'/items': Item}) 25 | app.run() 26 | ``` 27 | 28 | ## 1. Create an Item 29 | 30 | ```bash 31 | curl -X POST http://localhost:8000/items/ \ 32 | -H 'Content-Type: application/json' \ 33 | -d '{"name":"Sample Item"}' 34 | ``` 35 | 36 | _Response (201 Created):_ 37 | ```json 38 | {"id":1,"name":"Sample Item"} 39 | ``` 40 | 41 | ## 2. Read All Items 42 | 43 | ```bash 44 | curl http://localhost:8000/items/ 45 | ``` 46 | 47 | _Response (200 OK):_ 48 | ```json 49 | [{"id":1,"name":"Sample Item"}] 50 | ``` 51 | 52 | ## 3. Read a Single Item by ID 53 | 54 | ```bash 55 | curl http://localhost:8000/items/1 56 | ``` 57 | 58 | _Response (200 OK):_ 59 | ```json 60 | {"id":1,"name":"Sample Item"} 61 | ``` 62 | 63 | ## 4. Update an Item 64 | 65 | ```bash 66 | curl -X PUT http://localhost:8000/items/1 \ 67 | -H 'Content-Type: application/json' \ 68 | -d '{"name":"Updated Item"}' 69 | ``` 70 | 71 | _Response (200 OK):_ 72 | ```json 73 | {"result":{"id":1,"name":"Updated Item"}} 74 | ``` 75 | 76 | ## 5. Delete an Item 77 | 78 | ```bash 79 | curl -X DELETE http://localhost:8000/items/1 80 | ``` 81 | 82 | _Response (204 No Content):_ (empty body) 83 | 84 | --- 85 | 86 | ## Python Client Example 87 | 88 | ```python 89 | import requests 90 | 91 | base = 'http://localhost:8000/items' 92 | 93 | # Create 94 | r = requests.post(base+'/', json={'name':'Hello'}) 95 | print(r.json()) 96 | 97 | # List 98 | r = requests.get(base+'/') 99 | print(r.json()) 100 | 101 | # Update 102 | r = requests.put(base+'/1', json={'name':'New'}) 103 | print(r.json()) 104 | 105 | # Delete 106 | r = requests.delete(base+'/1') 107 | print(r.status_code) 108 | ``` 109 | -------------------------------------------------------------------------------- /examples/01_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | LightAPI Hello World Example 4 | 5 | This is the simplest possible LightAPI example demonstrating: 6 | - Basic API setup 7 | - Single endpoint creation 8 | - Minimal configuration 9 | 10 | Features demonstrated: 11 | - LightApi initialization 12 | - Custom endpoint creation 13 | - Basic HTTP methods 14 | - Swagger documentation 15 | """ 16 | 17 | from lightapi import LightApi, Response, RestEndpoint 18 | 19 | # Constants 20 | DEFAULT_PORT = 8000 21 | 22 | if __name__ == "__main__": 23 | # Define endpoint class in main section 24 | class HelloEndpoint(RestEndpoint): 25 | """Simple hello world endpoint without database.""" 26 | __tablename__ = "hello_endpoint" 27 | 28 | def get(self, request): 29 | """Return a simple hello message.""" 30 | return Response( 31 | body={"message": "Hello, World!", "framework": "LightAPI"}, 32 | status_code=200 33 | ) 34 | 35 | def post(self, request): 36 | """Echo back the request data.""" 37 | try: 38 | data = request.json() 39 | return Response( 40 | body={"echo": data, "message": "Data received successfully"}, 41 | status_code=201 42 | ) 43 | except Exception as e: 44 | return Response( 45 | body={"error": "Invalid JSON", "details": str(e)}, 46 | status_code=400 47 | ) 48 | 49 | def _print_usage(): 50 | """Print usage instructions.""" 51 | print("🚀 LightAPI Hello World Example") 52 | print("=" * 50) 53 | print("Server running at http://localhost:8000") 54 | print("API documentation available at http://localhost:8000/docs") 55 | print() 56 | print("Available endpoints:") 57 | print("• GET /hello_endpoint - Returns hello message") 58 | print("• POST /hello_endpoint - Echoes back request data") 59 | print() 60 | print("Try these example queries:") 61 | print(" curl http://localhost:8000/hello_endpoint") 62 | print(" curl -X POST http://localhost:8000/hello_endpoint -H 'Content-Type: application/json' -d '{\"name\": \"World\"}'") 63 | 64 | # Create and run the application 65 | app = LightApi() 66 | app.register(HelloEndpoint) 67 | 68 | _print_usage() 69 | 70 | # Run the server 71 | app.run(host="localhost", port=DEFAULT_PORT, debug=True) -------------------------------------------------------------------------------- /docs/technical-reference/core-api.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Core API 3 | --- 4 | 5 | ## LightApi 6 | 7 | The `LightApi` class is the central application object that configures routes, middleware, and runs the server. 8 | 9 | ### Initialization 10 | 11 | ```python 12 | def __init__( 13 | self, 14 | database_url: str = "sqlite:///app.db", 15 | swagger_title: str = "LightAPI Documentation", 16 | swagger_version: str = "1.0.0", 17 | swagger_description: str = "API automatic documentation", 18 | enable_swagger: bool = True 19 | ): 20 | ... 21 | ``` 22 | 23 | - `database_url` (str): SQLAlchemy database URL (e.g., `sqlite:///app.db`). 24 | - `swagger_title` (str): Title for the Swagger UI. 25 | - `swagger_version` (str): API version for the OpenAPI spec. 26 | - `swagger_description` (str): Description for the OpenAPI spec. 27 | - `enable_swagger` (bool): Mounts Swagger routes when `True`. 28 | 29 | Upon initialization, `LightApi` sets up the database engine and session via `setup_database`, and registers OpenAPI routes if enabled. 30 | 31 | ### Methods 32 | 33 | #### register(endpoints: Dict[str, Type[RestEndpoint]]) -> None 34 | 35 | Registers endpoint classes and mounts their routes. 36 | 37 | - **Parameters:** 38 | - `endpoints`: Mapping of URL prefixes to `RestEndpoint` subclasses. 39 | 40 | Adds each endpoint's routes to the internal Starlette application and registers OpenAPI metadata when available. 41 | 42 | Raises `TypeError` if a handler is not a subclass of `RestEndpoint`. 43 | 44 | #### add_middleware(middleware_classes: List[Type[Middleware]]) -> None 45 | 46 | Adds application-wide middleware. 47 | 48 | - **Parameters:** 49 | - `middleware_classes`: List of `Middleware` subclasses to apply globally. 50 | 51 | #### run(host: str = "0.0.0.0", port: int = 8000, debug: bool = False) -> None 52 | 53 | Starts the server. This is the only supported way to start the application. Do not use external libraries to start the server directly. 54 | 55 | - **Parameters:** 56 | - `host`: Host address to bind (default: `"0.0.0.0"`). 57 | - `port`: Port number (default: `8000`). 58 | - `debug`: Toggle Starlette debug mode (default: `False`). 59 | 60 | ### Example 61 | 62 | ```python 63 | from lightapi import LightApi 64 | from app.endpoints import UserEndpoint 65 | from app.middleware import AuthMiddleware, TimingMiddleware 66 | 67 | app = LightApi(database_url="postgresql+asyncpg://user:pass@db/db") 68 | app.add_middleware([AuthMiddleware, TimingMiddleware]) 69 | app.register({"/users": UserEndpoint}) 70 | app.run(host="0.0.0.0", port=8000, debug=True) 71 | ``` 72 | -------------------------------------------------------------------------------- /docs/technical-reference/handlers.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Request Handlers 3 | --- 4 | 5 | This document describes the built-in request handler classes used by LightAPI to process HTTP requests for SQLAlchemy models. 6 | 7 | ## AbstractHandler 8 | 9 | Base class for all model handlers (`lightapi.handlers.AbstractHandler`). Implements: 10 | 11 | - `__call__(request, *args, **kwargs) -> web.Response` – Entry point, manages session lifecycle. 12 | - `async def handle(self, db: Session, request: web.Request) -> web.Response` – Override in subclasses to implement logic. 13 | - Utility methods: 14 | - `get_request_json(request)` – Parse JSON body. 15 | - `get_item_by_id(db, item_id)` – Fetch a record by primary key. 16 | - `add_and_commit_item(db, item)` – Add and commit a new record. 17 | - `delete_and_commit_item(db, item)` – Delete and commit removal. 18 | - `json_response(item, status=200)` – Return a JSONResponse. 19 | - `json_error_response(error_message, status=404)` – Return error JSON. 20 | 21 | ## CreateHandler 22 | 23 | Handles `POST //` to create a new record. 24 | ```python 25 | class CreateHandler(AbstractHandler): 26 | async def handle(self, db, request): 27 | data = await self.get_request_json(request) 28 | item = self.model(**data) 29 | item = self.add_and_commit_item(db, item) 30 | return self.json_response(item, status=201) 31 | ``` 32 | 33 | ## ReadHandler 34 | 35 | Handles `GET //` and `GET //{id}` to list or retrieve records. 36 | ```python 37 | class ReadHandler(AbstractHandler): 38 | async def handle(self, db, request): 39 | if 'id' in request.match_info: 40 | item_id = int(request.match_info['id']) 41 | item = self.get_item_by_id(db, item_id) 42 | return self.json_response(item) if item else self.json_error_response('Not found') 43 | items = db.query(self.model).all() 44 | return self.json_response([i.serialize() for i in items]) 45 | ``` 46 | 47 | ## UpdateHandler 48 | 49 | Handles `PUT //{id}` to fully replace a record. 50 | 51 | ## PatchHandler 52 | 53 | Handles `PATCH //{id}` to partially update fields. 54 | 55 | ## DeleteHandler 56 | 57 | Handles `DELETE //{id}` to remove a record. 58 | 59 | ## RetrieveAllHandler 60 | 61 | Internal alias for listing all records (`HEAD` on list route). 62 | 63 | ## OptionsHandler 64 | 65 | Handles `OPTIONS //` to return allowed HTTP methods. 66 | 67 | ## HeadHandler 68 | 69 | Handles `HEAD //` to return headers for list endpoint without body. 70 | -------------------------------------------------------------------------------- /docs/tutorial/database.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Database Integration 3 | --- 4 | 5 | LightAPI integrates seamlessly with SQLAlchemy's async support. In this tutorial, you'll configure your database connection, define models, create tables, and use async sessions in your endpoints. 6 | 7 | ## 1. Configure the Database URL 8 | 9 | When creating your `LightApi` instance, pass the `database_url` parameter: 10 | 11 | ```python 12 | # main.py 13 | from lightapi import LightApi 14 | 15 | app = LightApi( 16 | database_url="sqlite+aiosqlite:///./app.db" 17 | ) 18 | ``` 19 | 20 | Supported URL schemes include: 21 | 22 | - `sqlite+aiosqlite:///` 23 | - `postgresql+asyncpg://user:pass@host/dbname` 24 | - `mysql+aiomysql://user:pass@host/dbname` 25 | 26 | ## 2. Define Models and Create Tables 27 | 28 | LightAPI uses a shared `Base` metadata. After defining your SQLAlchemy models, you can create tables using the built-in helper: 29 | 30 | ```python 31 | # app/models.py 32 | from sqlalchemy import Column, Integer, String 33 | from lightapi.database import Base 34 | 35 | class Task(Base): 36 | id = Column(Integer, primary_key=True, index=True) 37 | title = Column(String, nullable=False) 38 | completed = Column(Boolean, default=False) 39 | ``` 40 | 41 | To create tables at startup, use an event handler: 42 | 43 | ```python 44 | # app/main.py 45 | from lightapi import LightApi 46 | from lightapi.database import Base, engine 47 | from app.models import Task 48 | 49 | app = LightApi(database_url="sqlite+aiosqlite:///./app.db") 50 | 51 | @app.on_event("startup") 52 | async def create_tables(): 53 | async with engine.begin() as conn: 54 | await conn.run_sync(Base.metadata.create_all) 55 | 56 | app.register({"/tasks": Task}) 57 | ``` 58 | 59 | ## 3. Using Async Sessions in Custom Endpoints 60 | 61 | If you need direct access to the session, inject it into your custom endpoint: 62 | 63 | ```python 64 | # app/endpoints/custom_task.py 65 | from lightapi.rest import RestEndpoint 66 | 67 | class CustomTaskEndpoint(Base, RestEndpoint): 68 | tablename = "tasks" 69 | 70 | async def get(self, request): 71 | # `self.session` is an async SQLAlchemy session 72 | tasks = await self.session.execute( 73 | select(Task).order_by(Task.id) 74 | ) 75 | return [t._asdict() for t in tasks.scalars().all()] 76 | ``` 77 | 78 | Register: 79 | ```python 80 | app.register({"/custom-tasks": CustomTaskEndpoint}) 81 | ``` 82 | 83 | ## 4. Alembic Migrations (Optional) 84 | 85 | LightAPI doesn't include migrations out of the box, but you can configure Alembic using the same `database_url`. Initialize Alembic in your project and point `alembic.ini` to `env.py` that imports `Base.metadata`: 86 | 87 | ```ini 88 | # alembic.ini 89 | sqlalchemy.url = sqlite+aiosqlite:///./app.db 90 | ``` 91 | -------------------------------------------------------------------------------- /.github/workflows/test-dev.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs tests on the development branch 2 | # It runs tests but does not publish to PyPI 3 | 4 | name: Development Tests 5 | 6 | on: 7 | push: 8 | branches: [ "development" ] 9 | pull_request: 10 | branches: [ "development", "master" ] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | python-version: ["3.8", "3.9", "3.10", "3.11"] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v4 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | pip install -e .[test,dev] 34 | 35 | - name: Run linting (Python 3.11 only) 36 | if: matrix.python-version == '3.11' 37 | run: | 38 | # Install linting tools 39 | pip install black isort flake8 mypy 40 | 41 | # Run code formatting checks (warn only in development) 42 | echo "🔍 Checking code formatting..." 43 | black --check --diff . || echo "⚠️ Code formatting issues found, but not failing in development" 44 | 45 | # Run import sorting checks (warn only in development) 46 | echo "🔍 Checking import sorting..." 47 | isort --check-only --diff . || echo "⚠️ Import sorting issues found, but not failing in development" 48 | 49 | # Run style checks (errors only) 50 | echo "🔍 Checking critical style issues..." 51 | flake8 lightapi/ --count --select=E9,F63,F7,F82 --show-source --statistics 52 | 53 | # Run additional style checks (warnings only) 54 | echo "🔍 Checking style guidelines..." 55 | flake8 lightapi/ --count --exit-zero --max-complexity=10 --max-line-length=88 --statistics 56 | 57 | - name: Run tests 58 | run: | 59 | pytest tests/ -v --tb=short 60 | 61 | - name: Test package build 62 | if: matrix.python-version == '3.11' 63 | run: | 64 | pip install build 65 | python -m build 66 | 67 | - name: Check package 68 | if: matrix.python-version == '3.11' 69 | run: | 70 | pip install twine 71 | twine check dist/* 72 | 73 | # Job to check if all tests passed 74 | test-summary: 75 | needs: test 76 | runs-on: ubuntu-latest 77 | if: always() 78 | steps: 79 | - name: Check test results 80 | run: | 81 | if [ "${{ needs.test.result }}" = "success" ]; then 82 | echo "✅ All tests passed! Ready for merge to master." 83 | else 84 | echo "❌ Tests failed. Please fix issues before merging." 85 | exit 1 86 | fi -------------------------------------------------------------------------------- /docs/tutorial/responses.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Working with Responses 3 | --- 4 | 5 | LightAPI makes it easy to return HTTP responses from your endpoints with flexible JSON and custom response types. 6 | 7 | ## 1. Default JSON Responses 8 | 9 | By default, any Python `dict`, list, or sequence returned from a handler is automatically converted into a JSON response with status code 200: 10 | 11 | ```python 12 | async def get(self, request): 13 | return {"message": "Hello, World!"} 14 | ``` 15 | 16 | For methods that return `(body, status_code)` tuples, LightAPI sets both the JSON body and the HTTP status: 17 | 18 | ```python 19 | async def post(self, request): 20 | data = request.data 21 | # ... create object ... 22 | return {"result": created_obj}, 201 23 | ``` 24 | 25 | ## 2. Using the `Response` Class 26 | 27 | You can also use the imported `Response` class for more control over headers, media type, and status: 28 | 29 | ```python 30 | from lightapi import Response 31 | 32 | async def delete(self, request): 33 | # ... delete logic ... 34 | return Response({"detail": "Deleted"}, status_code=204, headers={"X-Deleted": "true"}) 35 | ``` 36 | 37 | ## 3. Custom Headers 38 | 39 | When using `Response`, you can include custom headers directly: 40 | 41 | ```python 42 | return Response( 43 | {"message": "Created"}, 44 | status_code=201, 45 | headers={"Location": f"/items/{item.id}"} 46 | ) 47 | ``` 48 | 49 | ## 4. Error Responses 50 | 51 | LightAPI's `Response` and default handlers can generate error JSON: 52 | 53 | ```python 54 | # Return a 404 Not Found 55 | return {"error": "Item not found"}, 404 56 | 57 | # Return a 400 Bad Request with detailed message 58 | return Response({"detail": "Invalid input"}, status_code=400) 59 | ``` 60 | 61 | Unallowed HTTP methods automatically return a 405 Method Not Allowed with a JSON body indicating the error. 62 | 63 | ## 5. Advanced Response Types 64 | 65 | Since LightAPI is built on Starlette, you can import and return any Starlette response directly for specialized use cases, such as: 66 | 67 | - `PlainTextResponse` for text data 68 | - `FileResponse` for serving files 69 | - `StreamingResponse` for streaming content 70 | 71 | ```python 72 | from starlette.responses import FileResponse 73 | 74 | async def get_file(self, request): 75 | return FileResponse("/path/to/file.zip") 76 | ``` 77 | 78 | ## 6. Working with Responses in Tests 79 | 80 | When testing endpoints that return `Response` objects, you can access the original content via the `body` property: 81 | 82 | ```python 83 | # In your test 84 | response = endpoint.get(request) 85 | assert response.body['message'] == 'Success' # Access original Python dict 86 | ``` 87 | 88 | The `Response.body` property returns: 89 | - The original Python object (dict, list, etc.) when accessed in tests 90 | - Attempts to decode JSON data from bytes when necessary 91 | - Falls back to the raw body when decoding fails 92 | 93 | This makes it easier to write assertions in tests without having to manually decode JSON. 94 | -------------------------------------------------------------------------------- /tests/test_pagination.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock 2 | 3 | import pytest 4 | 5 | from lightapi.pagination import Paginator 6 | 7 | 8 | class CustomPaginator(Paginator): 9 | limit = 20 10 | offset = 5 11 | sort = True 12 | 13 | def apply_sorting(self, queryset): 14 | return queryset.order_by("id") 15 | 16 | 17 | class TestPaginator: 18 | def test_default_paginator(self): 19 | paginator = Paginator() 20 | assert paginator.limit == 10 21 | assert paginator.offset == 0 22 | assert paginator.sort is False 23 | 24 | def test_custom_paginator(self): 25 | paginator = CustomPaginator() 26 | assert paginator.limit == 20 27 | assert paginator.offset == 5 28 | assert paginator.sort is True 29 | 30 | def test_get_limit(self): 31 | paginator = Paginator() 32 | assert paginator.get_limit() == 10 33 | 34 | custom_paginator = CustomPaginator() 35 | assert custom_paginator.get_limit() == 20 36 | 37 | def test_get_offset(self): 38 | paginator = Paginator() 39 | assert paginator.get_offset() == 0 40 | 41 | custom_paginator = CustomPaginator() 42 | assert custom_paginator.get_offset() == 5 43 | 44 | def test_paginate(self): 45 | paginator = Paginator() 46 | 47 | # Create a mock queryset 48 | mock_queryset = MagicMock() 49 | mock_limited = MagicMock() 50 | mock_queryset.limit.return_value = mock_limited 51 | mock_offset = MagicMock() 52 | mock_limited.offset.return_value = mock_offset 53 | mock_results = [{"id": 1}, {"id": 2}] 54 | mock_offset.all.return_value = mock_results 55 | 56 | # Test pagination 57 | results = paginator.paginate(mock_queryset) 58 | 59 | # Verify correct methods were called 60 | mock_queryset.limit.assert_called_once_with(10) 61 | mock_limited.offset.assert_called_once_with(0) 62 | mock_offset.all.assert_called_once() 63 | 64 | # Verify results 65 | assert results == mock_results 66 | 67 | def test_paginate_with_sorting(self): 68 | paginator = CustomPaginator() 69 | 70 | # Create a mock queryset 71 | mock_queryset = MagicMock() 72 | mock_sorted = MagicMock() 73 | mock_queryset.order_by.return_value = mock_sorted 74 | mock_limited = MagicMock() 75 | mock_sorted.limit.return_value = mock_limited 76 | mock_offset = MagicMock() 77 | mock_limited.offset.return_value = mock_offset 78 | mock_results = [{"id": 1}, {"id": 2}] 79 | mock_offset.all.return_value = mock_results 80 | 81 | # Test pagination with sorting 82 | results = paginator.paginate(mock_queryset) 83 | 84 | # Verify correct methods were called 85 | mock_queryset.order_by.assert_called_once_with("id") 86 | mock_sorted.limit.assert_called_once_with(20) 87 | mock_limited.offset.assert_called_once_with(5) 88 | mock_offset.all.assert_called_once() 89 | 90 | # Verify results 91 | assert results == mock_results 92 | -------------------------------------------------------------------------------- /tests/test_auth.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) 5 | import time 6 | from unittest.mock import MagicMock 7 | 8 | import jwt 9 | import pytest 10 | from conftest import TEST_JWT_SECRET 11 | 12 | from lightapi.auth import JWTAuthentication 13 | 14 | 15 | class TestJWTAuthentication: 16 | def test_authenticate_valid_token(self): 17 | auth = JWTAuthentication() 18 | auth.secret_key = TEST_JWT_SECRET 19 | 20 | # Create a valid token 21 | payload = {"user_id": 1, "exp": time.time() + 3600} 22 | token = jwt.encode(payload, TEST_JWT_SECRET, algorithm=auth.algorithm) 23 | 24 | # Create mock request with token and state attribute 25 | mock_request = MagicMock() 26 | mock_request.headers = {"Authorization": f"Bearer {token}"} 27 | mock_request.state = MagicMock() 28 | 29 | result = auth.authenticate(mock_request) 30 | 31 | assert result is True 32 | assert hasattr(mock_request.state, "user") 33 | assert mock_request.state.user["user_id"] == 1 34 | 35 | def test_authenticate_invalid_token(self): 36 | auth = JWTAuthentication() 37 | auth.secret_key = TEST_JWT_SECRET 38 | 39 | # Create an invalid token 40 | invalid_token = "invalid.token.string" 41 | 42 | # Create mock request with invalid token 43 | mock_request = MagicMock() 44 | mock_request.headers = {"Authorization": f"Bearer {invalid_token}"} 45 | 46 | result = auth.authenticate(mock_request) 47 | 48 | assert result is False 49 | 50 | def test_authenticate_expired_token(self): 51 | auth = JWTAuthentication() 52 | auth.secret_key = TEST_JWT_SECRET 53 | 54 | # Create an expired token 55 | payload = {"user_id": 1, "exp": time.time() - 3600} # 1 hour in the past 56 | token = jwt.encode(payload, TEST_JWT_SECRET, algorithm=auth.algorithm) 57 | 58 | # Create mock request with expired token 59 | mock_request = MagicMock() 60 | mock_request.headers = {"Authorization": f"Bearer {token}"} 61 | 62 | result = auth.authenticate(mock_request) 63 | 64 | assert result is False 65 | 66 | def test_authenticate_no_token(self): 67 | auth = JWTAuthentication() 68 | auth.secret_key = TEST_JWT_SECRET 69 | 70 | # Create mock request without token 71 | mock_request = MagicMock() 72 | mock_request.headers = {} 73 | 74 | result = auth.authenticate(mock_request) 75 | 76 | assert result is False 77 | 78 | def test_generate_token(self): 79 | auth = JWTAuthentication() 80 | auth.secret_key = TEST_JWT_SECRET 81 | 82 | user_data = {"user_id": 1, "username": "testuser"} 83 | token = auth.generate_token(user_data) 84 | 85 | # Decode the token and verify its contents 86 | decoded = jwt.decode(token, TEST_JWT_SECRET, algorithms=[auth.algorithm]) 87 | 88 | assert decoded["user_id"] == 1 89 | assert decoded["username"] == "testuser" 90 | assert "exp" in decoded 91 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "hatchling", 4 | ] 5 | build-backend = "hatchling.build" 6 | 7 | [project] 8 | name = "lightapi" 9 | version = "0.1.11" 10 | description = "A lightweight framework for building API endpoints using Python's native libraries." 11 | readme = "README.md" 12 | requires-python = ">=3.8.1" 13 | authors = [ 14 | { name = "iklobato", email = "iklobato1@gmail.com" }, 15 | ] 16 | keywords = [ 17 | "api", 18 | "rest", 19 | "restful", 20 | "endpoint", 21 | "lightweight", 22 | "framework", 23 | ] 24 | classifiers = [ 25 | "Programming Language :: Python :: 3", 26 | "Programming Language :: Python :: 3.8", 27 | "Programming Language :: Python :: 3.9", 28 | "Programming Language :: Python :: 3.10", 29 | "Programming Language :: Python :: 3.11", 30 | "License :: OSI Approved :: MIT License", 31 | "Operating System :: OS Independent", 32 | "Topic :: Internet :: WWW/HTTP", 33 | "Topic :: Internet :: WWW/HTTP :: HTTP Servers", 34 | "Topic :: Software Development :: Libraries :: Application Frameworks", 35 | ] 36 | dependencies = [ 37 | "SQLAlchemy>=2.0.30,<3.0.0", 38 | "aiohttp>=3.9.5,<4.0.0", 39 | "PyJWT>=2.8.0,<3.0.0", 40 | "starlette>=0.37.0,<1.0.0", 41 | "uvicorn>=0.30.0,<1.0.0", 42 | "redis>=5.0.0,<6.0.0", 43 | "PyYAML>=5.1", 44 | ] 45 | 46 | [project.license] 47 | text = "MIT" 48 | 49 | [project.optional-dependencies] 50 | dev = [ 51 | "pytest>=7.3.1,<8.0.0", 52 | "black>=23.3.0,<24.0.0", 53 | "isort>=5.12.0,<6.0.0", 54 | "mypy>=1.3.0,<2.0.0", 55 | "flake8>=6.0.0,<7.0.0", 56 | ] 57 | test = [ 58 | "pytest>=7.3.1,<8.0.0", 59 | "PyJWT>=2.8.0,<3.0.0", 60 | "starlette>=0.37.0,<1.0.0", 61 | "uvicorn>=0.30.0,<1.0.0", 62 | "redis>=5.0.0,<6.0.0", 63 | "httpx>=0.27.0,<1.0.0", 64 | ] 65 | docs = [ 66 | "mkdocs-material", 67 | "mkdocstrings[python]", 68 | "mkdocs-glightbox", 69 | "mkdocs-awesome-pages-plugin", 70 | "mkdocs-git-committers-plugin-2", 71 | "mkdocs-git-revision-date-localized-plugin", 72 | "mkdocs-git-authors-plugin", 73 | ] 74 | 75 | [project.urls] 76 | Repository = "https://github.com/henriqueblobato/LightApi" 77 | Issues = "https://github.com/henriqueblobato/LightApi/issues" 78 | Homepage = "https://github.com/henriqueblobato/LightApi" 79 | 80 | [tool.pytest.ini_options] 81 | testpaths = [ 82 | "tests", 83 | ] 84 | python_files = "test_*.py" 85 | filterwarnings = [ 86 | "ignore::pytest.PytestCollectionWarning", 87 | ] 88 | 89 | [tool.black] 90 | line-length = 88 91 | target-version = [ 92 | "py38", 93 | "py39", 94 | "py310", 95 | "py311", 96 | ] 97 | include = "\\.pyi?$" 98 | 99 | [tool.isort] 100 | profile = "black" 101 | line_length = 88 102 | 103 | [tool.mypy] 104 | python_version = "3.8" 105 | warn_return_any = true 106 | warn_unused_configs = true 107 | disallow_untyped_defs = true 108 | disallow_incomplete_defs = true 109 | 110 | [[tool.mypy.overrides]] 111 | module = [ 112 | "aiohttp.*", 113 | "SQLAlchemy.*", 114 | ] 115 | ignore_missing_imports = true 116 | -------------------------------------------------------------------------------- /tests/test_middleware.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock 2 | 3 | import pytest 4 | 5 | from lightapi.core import Middleware, Response 6 | 7 | 8 | class LoggingMiddleware(Middleware): 9 | def process(self, request, response): 10 | self.logged_request = request 11 | return response 12 | 13 | 14 | class HeaderModifyingMiddleware(Middleware): 15 | def process(self, request, response): 16 | if response: 17 | response.headers["X-Test-Header"] = "test-value" 18 | return response 19 | 20 | 21 | class ResponseModifyingMiddleware(Middleware): 22 | def process(self, request, response): 23 | if response: 24 | return Response({"modified": "response"}, 200) 25 | return response 26 | 27 | 28 | class RequestBlockingMiddleware(Middleware): 29 | def process(self, request, response): 30 | return Response({"error": "blocked"}, 403) 31 | 32 | 33 | class TestMiddleware: 34 | def test_base_middleware(self): 35 | middleware = Middleware() 36 | mock_request = MagicMock() 37 | mock_response = MagicMock() 38 | 39 | result = middleware.process(mock_request, mock_response) 40 | 41 | assert result == mock_response 42 | 43 | def test_logging_middleware(self): 44 | middleware = LoggingMiddleware() 45 | mock_request = MagicMock() 46 | mock_response = MagicMock() 47 | 48 | result = middleware.process(mock_request, mock_response) 49 | 50 | assert middleware.logged_request == mock_request 51 | assert result == mock_response 52 | 53 | def test_header_modifying_middleware(self): 54 | middleware = HeaderModifyingMiddleware() 55 | mock_request = MagicMock() 56 | mock_response = MagicMock() 57 | mock_response.headers = {} 58 | 59 | result = middleware.process(mock_request, mock_response) 60 | 61 | assert result == mock_response 62 | assert result.headers["X-Test-Header"] == "test-value" 63 | 64 | def test_response_modifying_middleware(self): 65 | middleware = ResponseModifyingMiddleware() 66 | mock_request = MagicMock() 67 | mock_response = MagicMock() 68 | 69 | result = middleware.process(mock_request, mock_response) 70 | 71 | assert result != mock_response 72 | assert isinstance(result, Response) 73 | assert result.status_code == 200 74 | assert "modified" in str(result.body) or "modified" in result.body 75 | 76 | def test_request_blocking_middleware(self): 77 | middleware = RequestBlockingMiddleware() 78 | mock_request = MagicMock() 79 | 80 | result = middleware.process(mock_request, None) 81 | 82 | assert isinstance(result, Response) 83 | assert result.status_code == 403 84 | assert "error" in str(result.body) or "error" in result.body 85 | 86 | def test_middleware_with_no_response(self): 87 | middleware = HeaderModifyingMiddleware() 88 | mock_request = MagicMock() 89 | 90 | result = middleware.process(mock_request, None) 91 | 92 | assert result is None 93 | 94 | 95 | # Merged TestLoggingMiddleware, TestCORSMiddleware, TestRateLimitMiddleware from test_middleware_example.py into this file. 96 | # Moved TestHelloWorldEndpoint to the end of this file as endpoint-specific middleware tests. 97 | -------------------------------------------------------------------------------- /lightapi/cache.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import json 3 | from typing import Any, Dict, Optional 4 | 5 | import redis 6 | 7 | 8 | class BaseCache: 9 | """ 10 | Base class for cache implementations. 11 | 12 | Provides a common interface for all caching methods. 13 | By default, acts as a no-op cache (doesn't actually cache anything). 14 | """ 15 | 16 | def get(self, key: str) -> Optional[Dict[str, Any]]: 17 | """ 18 | Retrieve data from the cache. 19 | 20 | Args: 21 | key: The cache key. 22 | 23 | Returns: 24 | The cached data, or None if not found. 25 | """ 26 | return None 27 | 28 | def set(self, key: str, value: Dict[str, Any], timeout: int = 300) -> bool: 29 | """ 30 | Store data in the cache. 31 | 32 | Args: 33 | key: The cache key. 34 | value: The data to cache. 35 | timeout: The cache timeout in seconds. 36 | 37 | Returns: 38 | bool: True if the data was cached successfully, False otherwise. 39 | """ 40 | return True 41 | 42 | 43 | class RedisCache(BaseCache): 44 | """ 45 | Redis-based cache implementation. 46 | 47 | Uses Redis for distributed caching with timeout support. 48 | Serializes data as JSON for storage. 49 | """ 50 | 51 | def __init__(self, host: str = "localhost", port: int = 6379, db: int = 0): 52 | """ 53 | Initialize a new Redis cache. 54 | 55 | Args: 56 | host: Redis server hostname. 57 | port: Redis server port. 58 | db: Redis database number. 59 | """ 60 | self.client = redis.Redis(host=host, port=port, db=db) 61 | 62 | def get(self, key: str) -> Optional[Dict[str, Any]]: 63 | """ 64 | Retrieve data from the Redis cache. 65 | 66 | Args: 67 | key: The cache key. 68 | 69 | Returns: 70 | The cached data, or None if not found or if deserialization fails. 71 | """ 72 | cached_data = self.client.get(key) 73 | if cached_data: 74 | try: 75 | return json.loads(cached_data) 76 | except json.JSONDecodeError: 77 | return None 78 | return None 79 | 80 | def set(self, key: str, value: Dict[str, Any], timeout: int = 300) -> bool: 81 | """ 82 | Store data in the Redis cache. 83 | 84 | Args: 85 | key: The cache key. 86 | value: The data to cache. 87 | timeout: The cache timeout in seconds. 88 | 89 | Returns: 90 | bool: True if the data was cached successfully, False otherwise. 91 | """ 92 | try: 93 | serialized_data = json.dumps(value) 94 | return self.client.setex(key, timeout, serialized_data) 95 | except (json.JSONDecodeError, redis.RedisError): 96 | return False 97 | 98 | def _get_cache_key(self, key: str) -> str: 99 | """ 100 | Legacy support method for cache key generation. 101 | 102 | Args: 103 | key: The original cache key. 104 | 105 | Returns: 106 | str: The formatted cache key. 107 | """ 108 | return key 109 | -------------------------------------------------------------------------------- /docs/examples/custom-application.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Custom Application Example 3 | --- 4 | 5 | This example demonstrates building a fully custom LightAPI application by composing the core classes: 6 | 7 | ```python 8 | import time 9 | from lightapi import LightApi, Middleware 10 | from lightapi.auth import JWTAuthentication 11 | from lightapi.cache import RedisCache 12 | from lightapi.filters import ParameterFilter 13 | from lightapi.pagination import Paginator 14 | from lightapi.rest import RestEndpoint, Validator, Response 15 | 16 | # 1. Custom Middleware for Logging and Timing 17 | class LoggingMiddleware(Middleware): 18 | def process(self, request, response): 19 | # Pre-request: log method and URL 20 | if response is None: 21 | request.start_time = time.time() 22 | print(f"[Request] {request.method} {request.url}") 23 | return None 24 | # Post-response: log status and elapsed time 25 | elapsed = time.time() - request.start_time 26 | print(f"[Response] {response.status_code} completed in {elapsed:.3f}s") 27 | return response 28 | 29 | # 2. Custom Validator to enforce field rules 30 | class ItemValidator(Validator): 31 | def validate_name(self, value: str) -> str: 32 | if not value or len(value) < 3: 33 | raise ValueError("Item name must be at least 3 characters") 34 | return value.strip() 35 | 36 | # 3. Define a custom RestEndpoint using all pluggable features 37 | class ItemEndpoint(Base, RestEndpoint): 38 | tablename = 'items' 39 | 40 | class Configuration: 41 | authentication_class = JWTAuthentication 42 | caching_class = RedisCache 43 | caching_method_names = ['get'] 44 | filter_class = ParameterFilter 45 | pagination_class = Paginator 46 | validator_class = ItemValidator 47 | 48 | # Override POST to wrap default behavior in a Response 49 | async def post(self, request): 50 | body, status = await super().post(request) 51 | return Response({ 'created': body['result'] }, status_code=status) 52 | 53 | # 4. Assemble the application 54 | app = LightApi( 55 | database_url='sqlite+aiosqlite:///./app.db', 56 | enable_swagger=True, 57 | swagger_title='Custom App API', 58 | swagger_version='1.0.0', 59 | swagger_description='Demo of custom LightAPI application' 60 | ) 61 | 62 | # 5. Register middleware and endpoints 63 | app.add_middleware([LoggingMiddleware]) 64 | app.register({ '/items': ItemEndpoint }) 65 | 66 | # 6. Run the server 67 | if __name__ == '__main__': 68 | app.run(host='0.0.0.0', port=8000, debug=True, reload=True) 69 | ``` 70 | 71 | ### Key integrations 72 | 73 | - **JWTAuthentication**: Secures all `/items` requests. Issue tokens via `JWTAuthentication.generate_token({...})`. 74 | - **RedisCache**: Caches GET responses to reduce database load. 75 | - **ParameterFilter + Paginator**: Enables query-param filtering and pagination automatically. 76 | - **ItemValidator**: Validates the `name` field on POST/PUT operations. 77 | - **LoggingMiddleware**: Logs each request and response timing. 78 | - **Response**: Builds response with custom status and payload. 79 | - **LightApi**: Configured to serve Swagger UI and OpenAPI schema at `/api/docs` and `/openapi.json`. 80 | 81 | With this setup, your `/items` endpoint is authenticated, cached, filterable, paginated, validated, logged, and documented—all with minimal boilerplate. -------------------------------------------------------------------------------- /docs/technical-reference/endpoints.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Endpoint Classes 3 | --- 4 | 5 | ## RestEndpoint 6 | 7 | `RestEndpoint` is the base class for building resource endpoints in LightAPI. It provides default implementations for common HTTP methods (GET, POST, PUT, PATCH, DELETE, OPTIONS) and supports pluggable features: 8 | 9 | ### Configuration 10 | 11 | Each `RestEndpoint` subclass can define an inner `Configuration` class with the following attributes: 12 | 13 | - `http_method_names` (List[str]): Allowed HTTP methods (default: `['GET','POST','PUT','DELETE','PATCH','OPTIONS']`). 14 | - `validator_class` (Type[Validator], optional): Class for validating request data. 15 | - `filter_class` (Type[BaseFilter], optional): Class for filtering querysets based on request. 16 | - `authentication_class` (Type[BaseAuthentication], optional): Class for authenticating requests. 17 | - `caching_class` (Type[BaseCache], optional): Class for caching responses. 18 | - `caching_method_names` (List[str], optional): Which methods to cache (e.g., `['get']`). 19 | - `pagination_class` (Type[Paginator], optional): Class for paginating querysets. 20 | 21 | ### Lifecycle 22 | 23 | When a request arrives, `RestEndpoint` handlers: 24 | 25 | 1. Call `_setup(request, session)` to attach: 26 | - `self.request`: the incoming request raised by Starlette. 27 | - `self.session`: the SQLAlchemy session for database operations. 28 | 2. Invoke `_setup_auth()`, `_setup_cache()`, `_setup_filter()`, `_setup_validator()`, `_setup_pagination()` to initialize any configured components. 29 | 3. Dispatch to the HTTP method handler (e.g., `get`, `post`). 30 | 31 | If `authentication_class` is set and `authenticate(request)` returns `False`, the request is short-circuited with a 401 response. 32 | 33 | ### Default Methods 34 | 35 | - `async def get(self, request) -> Tuple[dict, int]`: 36 | - Lists all records or retrieves one by `id` query parameter. 37 | - Applies filtering and pagination when configured. 38 | - Returns `{'results': [...]}, 200`. 39 | 40 | - `async def post(self, request) -> Tuple[dict, int]`: 41 | - Creates a new record from `request.data`. 42 | - Applies validation when `validator_class` is set. 43 | - Returns `{'result': {...}}, 201` or `{'error': '...'}, 400` on failure. 44 | 45 | - `async def put(self, request) -> Tuple[dict, int]`: 46 | - Replaces an existing record by `id` path parameter. 47 | - Returns `{'result': {...}}, 200` or `{'error': '...'}, 404/400`. 48 | 49 | - `async def patch(self, request) -> Tuple[dict, int]`: 50 | - Partially updates fields on an existing record. 51 | 52 | - `async def delete(self, request) -> Tuple[dict, int]`: 53 | - Deletes a record by `id`. 54 | - Returns `{'result': 'Object deleted'}, 204`. 55 | 56 | - `def options(self, request) -> Tuple[dict, int]`: 57 | - Returns allowed methods from `Configuration.http_method_names`, status 200. 58 | 59 | Each handler returns either a `(body, status_code)` tuple, a `Response` instance, or a Python object which is serialized to JSON with status 200. 60 | 61 | ### Example 62 | 63 | ```python 64 | from lightapi.rest import RestEndpoint 65 | from lightapi.auth import JWTAuthentication 66 | 67 | class UserEndpoint(Base, RestEndpoint): 68 | class Configuration: 69 | http_method_names = ['GET', 'POST'] 70 | authentication_class = JWTAuthentication 71 | pagination_class = CustomPaginator 72 | 73 | # Override GET to add custom logic 74 | async def get(self, request): 75 | base_response, status = await super().get(request) 76 | return {'users': base_response['results']}, status 77 | ``` 78 | -------------------------------------------------------------------------------- /examples/10_blog_post.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text 4 | from sqlalchemy.orm import relationship 5 | 6 | from lightapi import Base, LightApi 7 | from lightapi.database import Base 8 | from lightapi.rest import RestEndpoint 9 | 10 | print(f"DEBUG: LightApi loaded from {LightApi.__module__}") 11 | 12 | 13 | class BlogPost(Base): 14 | __tablename__ = "posts" 15 | 16 | id = Column(Integer, primary_key=True) 17 | title = Column(String(200), nullable=False) 18 | content = Column(Text, nullable=False) 19 | created_at = Column(DateTime, default=datetime.utcnow, nullable=False) 20 | 21 | comments = relationship("Comment", back_populates="post", cascade="all, delete-orphan") 22 | 23 | def _serialize_post(self, post, include_comments=True): 24 | """Serialize post with optional comments.""" 25 | result = {c.name: getattr(post, c.name) for c in post.__table__.columns} 26 | if result.get('created_at'): 27 | result['created_at'] = result['created_at'].isoformat() 28 | 29 | if include_comments: 30 | result['comments'] = [self._serialize_comment(c) for c in post.comments] 31 | else: 32 | result['comment_count'] = len(post.comments) 33 | return result 34 | 35 | def _serialize_comment(self, comment): 36 | """Serialize comment to dict.""" 37 | result = {c.name: getattr(comment, c.name) for c in comment.__table__.columns} 38 | if result.get('created_at'): 39 | result['created_at'] = result['created_at'].isoformat() 40 | return result 41 | 42 | 43 | class Comment(Base): 44 | __tablename__ = "comments" 45 | 46 | id = Column(Integer, primary_key=True) 47 | content = Column(String(1000), nullable=False) 48 | author = Column(String(100), nullable=False) 49 | created_at = Column(DateTime, default=datetime.utcnow, nullable=False) 50 | post_id = Column(Integer, ForeignKey("posts.id"), nullable=False) 51 | 52 | post = relationship("BlogPost", back_populates="comments") 53 | 54 | 55 | class Endpoint(Base, RestEndpoint): 56 | __tablename__ = "asdasd" 57 | 58 | def get(self, post_id: int): 59 | return {"status": "ok"}, 200 60 | 61 | def post(self, data: dict): 62 | return {"status": "ok"}, 200 63 | 64 | 65 | def _create_sample_posts(session): 66 | """Create sample blog posts.""" 67 | posts = [ 68 | BlogPost(title="Getting Started with LightAPI", content="This is a comprehensive guide to using LightAPI..."), 69 | BlogPost(title="Advanced Features", content="Learn about advanced features like caching and pagination..."), 70 | BlogPost(title="Best Practices", content="Follow these best practices for building robust APIs...") 71 | ] 72 | session.add_all(posts) 73 | return posts 74 | 75 | 76 | def _print_usage(): 77 | """Print usage instructions.""" 78 | print("🚀 Blog Post API Started") 79 | print("Server running at http://localhost:8000") 80 | print("API documentation at http://localhost:8000/docs") 81 | print("\nTry these endpoints:") 82 | print(" curl http://localhost:8000/posts/") 83 | print(" curl http://localhost:8000/comments/") 84 | 85 | 86 | if __name__ == "__main__": 87 | app = LightApi( 88 | enable_swagger=True, 89 | swagger_title="Blog Post API", 90 | swagger_version="1.0.0", 91 | swagger_description="API documentation for the Blog Post application", 92 | ) 93 | app.register(BlogPost) 94 | app.register(Comment) 95 | app.register(Endpoint) 96 | 97 | _print_usage() 98 | app.run(host="0.0.0.0", port=8000) 99 | -------------------------------------------------------------------------------- /docs/api-reference/database.md: -------------------------------------------------------------------------------- 1 | # Database Reference 2 | 3 | The Database module provides integration with SQLAlchemy and other ORMs for data persistence in LightAPI. 4 | 5 | ## Database Configuration 6 | 7 | ### Basic Setup 8 | 9 | ```python 10 | from lightapi.database import Database 11 | 12 | db = Database('sqlite:///app.db') 13 | ``` 14 | 15 | ### Connection Options 16 | 17 | ```python 18 | db = Database( 19 | 'postgresql://user:pass@localhost/dbname', 20 | pool_size=5, 21 | max_overflow=10 22 | ) 23 | ``` 24 | 25 | ## Model Definition 26 | 27 | ### Basic Model 28 | 29 | ```python 30 | from lightapi.database import Model 31 | from lightapi.models import Column, String, Integer 32 | 33 | class User(Model): 34 | __tablename__ = 'users' 35 | 36 | id = Column(Integer, primary_key=True) 37 | name = Column(String(50)) 38 | email = Column(String(120), unique=True) 39 | ``` 40 | 41 | ### Relationships 42 | 43 | ```python 44 | class Post(Model): 45 | __tablename__ = 'posts' 46 | 47 | id = Column(Integer, primary_key=True) 48 | title = Column(String(100)) 49 | user_id = Column(Integer, ForeignKey('users.id')) 50 | user = relationship('User', backref='posts') 51 | ``` 52 | 53 | ## Query Operations 54 | 55 | ### Basic Queries 56 | 57 | ```python 58 | # Create 59 | user = User(name='John', email='john@example.com') 60 | db.session.add(user) 61 | db.session.commit() 62 | 63 | # Read 64 | user = User.query.filter_by(name='John').first() 65 | 66 | # Update 67 | user.email = 'new@example.com' 68 | db.session.commit() 69 | 70 | # Delete 71 | db.session.delete(user) 72 | db.session.commit() 73 | ``` 74 | 75 | ### Advanced Queries 76 | 77 | ```python 78 | # Join queries 79 | users = User.query.join(Post).filter(Post.title.like('%python%')).all() 80 | 81 | # Aggregate functions 82 | from sqlalchemy import func 83 | post_count = db.session.query(func.count(Post.id)).scalar() 84 | ``` 85 | 86 | ## Migration Support 87 | 88 | ### Creating Migrations 89 | 90 | ```python 91 | from lightapi.database import create_migration 92 | 93 | create_migration('add_user_table') 94 | ``` 95 | 96 | ### Running Migrations 97 | 98 | ```python 99 | from lightapi.database import migrate 100 | 101 | migrate() 102 | ``` 103 | 104 | ## Examples 105 | 106 | ### Complete Database Setup 107 | 108 | ```python 109 | from lightapi import LightAPI 110 | from lightapi.database import Database, Model 111 | from lightapi.models import Column, String, Integer, relationship 112 | 113 | # Initialize app and database 114 | app = LightAPI() 115 | db = Database('sqlite:///app.db') 116 | 117 | # Define models 118 | class User(Model): 119 | __tablename__ = 'users' 120 | 121 | id = Column(Integer, primary_key=True) 122 | name = Column(String(50)) 123 | email = Column(String(120), unique=True) 124 | posts = relationship('Post', backref='author') 125 | 126 | class Post(Model): 127 | __tablename__ = 'posts' 128 | 129 | id = Column(Integer, primary_key=True) 130 | title = Column(String(100)) 131 | content = Column(String) 132 | user_id = Column(Integer, ForeignKey('users.id')) 133 | 134 | # Create tables 135 | db.create_all() 136 | ``` 137 | 138 | ## Best Practices 139 | 140 | 1. Use migrations for database schema changes 141 | 2. Implement proper indexing for better performance 142 | 3. Use relationships appropriately 143 | 4. Handle database errors properly 144 | 5. Use connection pooling in production 145 | 146 | ## See Also 147 | 148 | - [Models](models.md) - Model definitions and validation 149 | - [REST API](rest.md) - REST endpoint implementation 150 | - [Core API](core.md) - Core framework functionality -------------------------------------------------------------------------------- /examples/03_validation_custom_fields.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String 2 | 3 | from lightapi.core import LightApi, Response 4 | from lightapi.models import Base 5 | from lightapi.rest import RestEndpoint, Validator 6 | 7 | 8 | # Define a custom validator with field-specific validation methods 9 | class ProductValidator(Validator): 10 | def validate_name(self, value): 11 | if not value or len(value) < 3: 12 | raise ValueError("Product name must be at least 3 characters") 13 | return value.strip() 14 | 15 | def validate_price(self, value): 16 | try: 17 | price = float(value) 18 | if price <= 0: 19 | raise ValueError("Price must be greater than zero") 20 | return price 21 | except (TypeError, ValueError) as e: 22 | # If it's our own ValueError, re-raise it 23 | if isinstance(e, ValueError) and "must be greater than zero" in str(e): 24 | raise e 25 | # Otherwise, raise the generic message 26 | raise ValueError("Price must be a valid number") 27 | 28 | def validate_sku(self, value): 29 | if not value or not isinstance(value, str) or len(value) != 8: 30 | raise ValueError("SKU must be an 8-character string") 31 | return value.upper() 32 | 33 | 34 | # Define a model that uses the validator 35 | class Product(Base, RestEndpoint): 36 | __tablename__ = "products" 37 | __table_args__ = {"extend_existing": True} 38 | 39 | id = Column(Integer, primary_key=True) 40 | name = Column(String(100)) 41 | price = Column(Integer) # Stored as cents 42 | sku = Column(String(8), unique=True) 43 | 44 | class Configuration: 45 | validator_class = ProductValidator 46 | 47 | # Override POST to handle validation errors gracefully 48 | def post(self, request): 49 | try: 50 | data = getattr(request, "data", {}) 51 | 52 | # The validator will raise exceptions if validation fails 53 | validated_data = self.validator.validate(data) 54 | 55 | # Convert price to cents for storage 56 | if "price" in validated_data: 57 | validated_data["price"] = int(validated_data["price"] * 100) 58 | 59 | instance = self.__class__(**validated_data) 60 | self.session.add(instance) 61 | self.session.commit() 62 | 63 | # Return the created instance 64 | return { 65 | "id": instance.id, 66 | "name": instance.name, 67 | "price": instance.price / 100, # Convert back to dollars 68 | "sku": instance.sku, 69 | }, 201 70 | 71 | except ValueError as e: 72 | # Return validation errors with 400 status 73 | return Response({"error": str(e)}, status_code=400) 74 | except Exception as e: 75 | self.session.rollback() 76 | return Response({"error": str(e)}, status_code=500) 77 | 78 | 79 | if __name__ == "__main__": 80 | app = LightApi( 81 | database_url="sqlite:///validation_example.db", 82 | swagger_title="Validation Example", 83 | swagger_version="1.0.0", 84 | swagger_description="Example showing data validation with LightAPI", 85 | ) 86 | 87 | app.register(Product) 88 | 89 | print("Server running at http://localhost:8000") 90 | print("API documentation available at http://localhost:8000/docs") 91 | print("Try creating products with:") 92 | print( 93 | 'curl -X POST http://localhost:8000/products -H \'Content-Type: application/json\' -d \'{"name": "Widget", "price": 19.99, "sku": "WDG12345"}\'' 94 | ) 95 | 96 | app.run(host="localhost", port=8000, debug=True) 97 | -------------------------------------------------------------------------------- /examples/validation_custom_fields_03.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String 2 | 3 | from lightapi.core import LightApi, Response 4 | from lightapi.models import Base 5 | from lightapi.rest import RestEndpoint, Validator 6 | 7 | 8 | # Define a custom validator with field-specific validation methods 9 | class ProductValidator(Validator): 10 | def validate_name(self, value): 11 | if not value or len(value) < 3: 12 | raise ValueError("Product name must be at least 3 characters") 13 | return value.strip() 14 | 15 | def validate_price(self, value): 16 | try: 17 | price = float(value) 18 | if price <= 0: 19 | raise ValueError("Price must be greater than zero") 20 | return price 21 | except (TypeError, ValueError) as e: 22 | # If it's our own ValueError, re-raise it 23 | if isinstance(e, ValueError) and "must be greater than zero" in str(e): 24 | raise e 25 | # Otherwise, raise the generic message 26 | raise ValueError("Price must be a valid number") 27 | 28 | def validate_sku(self, value): 29 | if not value or not isinstance(value, str) or len(value) != 8: 30 | raise ValueError("SKU must be an 8-character string") 31 | return value.upper() 32 | 33 | 34 | # Define a model that uses the validator 35 | class Product(Base, RestEndpoint): 36 | __tablename__ = "products" 37 | __table_args__ = {"extend_existing": True} 38 | 39 | id = Column(Integer, primary_key=True) 40 | name = Column(String(100)) 41 | price = Column(Integer) # Stored as cents 42 | sku = Column(String(8), unique=True) 43 | 44 | class Configuration: 45 | validator_class = ProductValidator 46 | 47 | # Override POST to handle validation errors gracefully 48 | def post(self, request): 49 | try: 50 | data = getattr(request, "data", {}) 51 | 52 | # The validator will raise exceptions if validation fails 53 | validated_data = self.validator.validate(data) 54 | 55 | # Convert price to cents for storage 56 | if "price" in validated_data: 57 | validated_data["price"] = int(validated_data["price"] * 100) 58 | 59 | instance = self.__class__(**validated_data) 60 | self.session.add(instance) 61 | self.session.commit() 62 | 63 | # Return the created instance 64 | return { 65 | "id": instance.id, 66 | "name": instance.name, 67 | "price": instance.price / 100, # Convert back to dollars 68 | "sku": instance.sku, 69 | }, 201 70 | 71 | except ValueError as e: 72 | # Return validation errors with 400 status 73 | return Response({"error": str(e)}, status_code=400) 74 | except Exception as e: 75 | self.session.rollback() 76 | return Response({"error": str(e)}, status_code=500) 77 | 78 | 79 | if __name__ == "__main__": 80 | app = LightApi( 81 | database_url="sqlite:///validation_example.db", 82 | swagger_title="Validation Example", 83 | swagger_version="1.0.0", 84 | swagger_description="Example showing data validation with LightAPI", 85 | ) 86 | 87 | app.register(Product) 88 | 89 | print("Server running at http://localhost:8000") 90 | print("API documentation available at http://localhost:8000/docs") 91 | print("Try creating products with:") 92 | print( 93 | 'curl -X POST http://localhost:8000/products -H \'Content-Type: application/json\' -d \'{"name": "Widget", "price": 19.99, "sku": "WDG12345"}\'' 94 | ) 95 | 96 | app.run(host="localhost", port=8000, debug=True) 97 | -------------------------------------------------------------------------------- /examples/09_yaml_basic_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Basic YAML Configuration Example 4 | 5 | This example demonstrates the simplest way to create a REST API using YAML configuration. 6 | Perfect for getting started with LightAPI's YAML system. 7 | 8 | Features demonstrated: 9 | - Basic YAML structure 10 | - Simple database connection 11 | - Full CRUD operations 12 | - Swagger documentation 13 | """ 14 | 15 | import os 16 | import tempfile 17 | import yaml 18 | from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey 19 | from sqlalchemy import create_engine 20 | from sqlalchemy.sql import func 21 | from lightapi import LightApi 22 | from lightapi.models import Base 23 | 24 | # Constants 25 | DEFAULT_PORT = 8000 26 | 27 | if __name__ == "__main__": 28 | # Define SQLAlchemy models in main section 29 | class User(Base): 30 | __tablename__ = "users" 31 | id = Column(Integer, primary_key=True) 32 | name = Column(String(100), nullable=False) 33 | email = Column(String(100), unique=True, nullable=False) 34 | created_at = Column(DateTime, default=func.now()) 35 | 36 | class Post(Base): 37 | __tablename__ = "posts" 38 | id = Column(Integer, primary_key=True) 39 | title = Column(String(200), nullable=False) 40 | content = Column(Text) 41 | user_id = Column(Integer, ForeignKey("users.id")) 42 | created_at = Column(DateTime, default=func.now()) 43 | 44 | def create_basic_database(): 45 | """Create a simple database using SQLAlchemy ORM.""" 46 | # Create temporary database 47 | db_file = tempfile.NamedTemporaryFile(suffix='.db', delete=False) 48 | db_path = db_file.name 49 | db_file.close() 50 | 51 | # Create engine and tables using ORM 52 | engine = create_engine(f"sqlite:///{db_path}") 53 | Base.metadata.create_all(engine) 54 | 55 | return db_path 56 | 57 | def create_yaml_config(db_path): 58 | """Create basic YAML configuration file.""" 59 | config = { 60 | 'database_url': f'sqlite:///{db_path}', 61 | 'tables': [ 62 | {'name': 'users', 'methods': ['GET', 'POST', 'PUT', 'DELETE']}, 63 | {'name': 'posts', 'methods': ['GET', 'POST', 'PUT', 'DELETE']} 64 | ] 65 | } 66 | 67 | config_path = os.path.join(os.path.dirname(__file__), 'basic_config.yaml') 68 | with open(config_path, 'w') as f: 69 | yaml.dump(config, f, default_flow_style=False) 70 | 71 | return config_path 72 | 73 | def _print_usage(): 74 | """Print usage instructions.""" 75 | print("🚀 Basic YAML Configuration Example") 76 | print("=" * 50) 77 | print("This example demonstrates:") 78 | print("• Basic YAML structure") 79 | print("• Simple database connection") 80 | print("• Full CRUD operations") 81 | print("• Swagger documentation") 82 | print() 83 | print("Server running at http://localhost:8000") 84 | print("API documentation available at http://localhost:8000/docs") 85 | print() 86 | print("Available endpoints:") 87 | print("• GET/POST/PUT/DELETE /users") 88 | print("• GET/POST/PUT/DELETE /posts") 89 | print() 90 | print("Try these example queries:") 91 | print(" curl http://localhost:8000/users") 92 | print(" curl http://localhost:8000/posts") 93 | 94 | # Create database and configuration 95 | db_path = create_basic_database() 96 | config_path = create_yaml_config(db_path) 97 | 98 | # Create LightAPI instance from YAML configuration 99 | app = LightApi.from_config(config_path) 100 | 101 | _print_usage() 102 | 103 | # Run the server 104 | app.run(host="localhost", port=DEFAULT_PORT, debug=True) -------------------------------------------------------------------------------- /tests/test_swagger.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock, patch 2 | 3 | import pytest 4 | from sqlalchemy import Column, Integer, String 5 | 6 | from lightapi.rest import RestEndpoint 7 | from lightapi.swagger import SwaggerGenerator, openapi_json_route, swagger_ui_route 8 | 9 | 10 | class TestEndpoint(RestEndpoint): 11 | __tablename__ = "test_models" 12 | 13 | id = Column(Integer, primary_key=True) 14 | name = Column(String) 15 | email = Column(String, unique=True) 16 | 17 | class Configuration: 18 | http_method_names = ["GET", "POST"] 19 | 20 | def get(self, request): 21 | return {"data": "ok"}, 200 22 | 23 | def post(self, request): 24 | return {"data": "created"}, 201 25 | 26 | 27 | class TestSwaggerGenerator: 28 | def test_init(self): 29 | generator = SwaggerGenerator(title="Test API", version="1.0.0", description="Test description") 30 | 31 | assert generator.title == "Test API" 32 | assert generator.version == "1.0.0" 33 | assert generator.description == "Test description" 34 | assert generator.paths == {} 35 | assert "schemas" in generator.components 36 | assert "securitySchemes" in generator.components 37 | 38 | def test_register_endpoint(self): 39 | generator = SwaggerGenerator() 40 | generator.register_endpoint("/test", TestEndpoint) 41 | 42 | # Check paths 43 | assert "/test" in generator.paths 44 | assert "get" in generator.paths["/test"] 45 | assert "post" in generator.paths["/test"] 46 | 47 | # Check schemas 48 | assert "TestEndpoint" in generator.components["schemas"] 49 | assert generator.components["schemas"]["TestEndpoint"]["type"] == "object" 50 | 51 | def test_generate_openapi_spec(self): 52 | generator = SwaggerGenerator(title="Test API", version="1.0.0", description="Test description") 53 | generator.register_endpoint("/test", TestEndpoint) 54 | 55 | spec = generator.generate_openapi_spec() 56 | 57 | assert spec["openapi"] == "3.0.0" 58 | assert spec["info"]["title"] == "Test API" 59 | assert spec["info"]["version"] == "1.0.0" 60 | assert spec["info"]["description"] == "Test description" 61 | assert "/test" in spec["paths"] 62 | assert "components" in spec 63 | 64 | def test_get_swagger_ui(self): 65 | generator = SwaggerGenerator() 66 | response = generator.get_swagger_ui() 67 | 68 | assert response.status_code == 200 69 | assert response.media_type == "text/html" 70 | assert "swagger-ui" in response.body.decode() 71 | 72 | def test_get_openapi_json(self): 73 | generator = SwaggerGenerator() 74 | generator.register_endpoint("/test", TestEndpoint) 75 | 76 | response = generator.get_openapi_json() 77 | 78 | assert response.status_code == 200 79 | assert response.media_type == "application/json" 80 | 81 | 82 | class TestSwaggerRoutes: 83 | def test_swagger_ui_route(self): 84 | mock_request = MagicMock() 85 | mock_generator = MagicMock() 86 | mock_response = MagicMock() 87 | 88 | mock_generator.get_swagger_ui.return_value = mock_response 89 | mock_request.app.state.swagger_generator = mock_generator 90 | 91 | response = swagger_ui_route(mock_request) 92 | 93 | mock_generator.get_swagger_ui.assert_called_once() 94 | assert response == mock_response 95 | 96 | def test_openapi_json_route(self): 97 | mock_request = MagicMock() 98 | mock_generator = MagicMock() 99 | mock_response = MagicMock() 100 | 101 | mock_generator.get_openapi_json.return_value = mock_response 102 | mock_request.app.state.swagger_generator = mock_generator 103 | 104 | response = openapi_json_route(mock_request) 105 | 106 | mock_generator.get_openapi_json.assert_called_once() 107 | assert response == mock_response 108 | -------------------------------------------------------------------------------- /docs/api-reference/cache.md: -------------------------------------------------------------------------------- 1 | # Caching Reference 2 | 3 | The Caching module provides Redis-based caching capabilities for improved performance in LightAPI applications. 4 | 5 | ## Cache Configuration 6 | 7 | ### Basic Setup 8 | 9 | ```python 10 | from lightapi.cache import Cache 11 | 12 | cache = Cache('redis://localhost:6379/0') 13 | ``` 14 | 15 | ### Advanced Configuration 16 | 17 | ```python 18 | cache = Cache( 19 | 'redis://localhost:6379/0', 20 | prefix='myapp:', 21 | default_timeout=3600, 22 | serializer='json' 23 | ) 24 | ``` 25 | 26 | ## Basic Operations 27 | 28 | ### Setting Values 29 | 30 | ```python 31 | # Basic set 32 | cache.set('key', 'value') 33 | 34 | # Set with timeout 35 | cache.set('key', 'value', timeout=300) # 5 minutes 36 | 37 | # Set multiple values 38 | cache.set_many({ 39 | 'key1': 'value1', 40 | 'key2': 'value2' 41 | }) 42 | ``` 43 | 44 | ### Getting Values 45 | 46 | ```python 47 | # Get single value 48 | value = cache.get('key') 49 | 50 | # Get with default 51 | value = cache.get('key', default='default_value') 52 | 53 | # Get multiple values 54 | values = cache.get_many(['key1', 'key2']) 55 | ``` 56 | 57 | ### Deleting Values 58 | 59 | ```python 60 | # Delete single key 61 | cache.delete('key') 62 | 63 | # Delete multiple keys 64 | cache.delete_many(['key1', 'key2']) 65 | 66 | # Clear all keys 67 | cache.clear() 68 | ``` 69 | 70 | ## Decorators 71 | 72 | ### Function Caching 73 | 74 | ```python 75 | from lightapi.cache import cached 76 | 77 | @cached(timeout=300) 78 | def expensive_operation(): 79 | # ... perform expensive operation ... 80 | return result 81 | ``` 82 | 83 | ### Method Caching 84 | 85 | ```python 86 | class UserService: 87 | @cached(timeout=300) 88 | def get_user_data(self, user_id): 89 | # ... fetch user data ... 90 | return data 91 | ``` 92 | 93 | ## Advanced Features 94 | 95 | ### Pattern-based Operations 96 | 97 | ```python 98 | # Delete all keys matching pattern 99 | cache.delete_pattern('user:*') 100 | 101 | # Get all keys matching pattern 102 | keys = cache.keys('user:*') 103 | ``` 104 | 105 | ### Cache Tags 106 | 107 | ```python 108 | # Set with tags 109 | cache.set('user:1', data, tags=['users']) 110 | 111 | # Invalidate by tag 112 | cache.invalidate_tags(['users']) 113 | ``` 114 | 115 | ## Examples 116 | 117 | ### Complete Caching Setup 118 | 119 | ```python 120 | from lightapi import LightAPI 121 | from lightapi.cache import Cache, cached 122 | 123 | # Initialize app and cache 124 | app = LightAPI() 125 | cache = Cache('redis://localhost:6379/0') 126 | 127 | # Cache endpoint response 128 | @app.route('/users') 129 | @cached(timeout=300) 130 | def get_users(): 131 | users = User.query.all() 132 | return {'users': [user.to_dict() for user in users]} 133 | 134 | # Cache with dynamic key 135 | @app.route('/user/') 136 | @cached(key_prefix='user:{id}') 137 | def get_user(id): 138 | user = User.query.get(id) 139 | return user.to_dict() 140 | 141 | # Manual cache management 142 | @app.route('/update-user/', methods=['POST']) 143 | def update_user(id): 144 | user = User.query.get(id) 145 | user.update(request.json) 146 | db.session.commit() 147 | 148 | # Invalidate cache 149 | cache.delete(f'user:{id}') 150 | cache.invalidate_tags(['users']) 151 | 152 | return {'message': 'User updated'} 153 | ``` 154 | 155 | ## Best Practices 156 | 157 | 1. Use appropriate timeout values 158 | 2. Implement cache invalidation strategy 159 | 3. Use cache tags for related data 160 | 4. Monitor cache memory usage 161 | 5. Handle cache failures gracefully 162 | 163 | ## See Also 164 | 165 | - [Core API](core.md) - Core framework functionality 166 | - [REST API](rest.md) - REST endpoint implementation 167 | - [Database](database.md) - Database integration 168 | 169 | > **Note:** Only GET, POST, PUT, PATCH, DELETE HTTP verbs are supported. OPTIONS and HEAD are not available. Required fields must be NOT NULL in the schema. Constraint violations (NOT NULL, UNIQUE, FK) return 409. -------------------------------------------------------------------------------- /lightapi/auth.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import datetime, timedelta 3 | from typing import Any, Dict, Optional 4 | 5 | import jwt 6 | from starlette.responses import JSONResponse 7 | 8 | from .config import config 9 | 10 | 11 | class BaseAuthentication: 12 | """ 13 | Base class for authentication. 14 | 15 | Provides a common interface for all authentication methods. 16 | By default, allows all requests. 17 | """ 18 | 19 | def authenticate(self, request): 20 | """ 21 | Authenticate a request. 22 | 23 | Args: 24 | request: The HTTP request to authenticate. 25 | 26 | Returns: 27 | bool: True if authentication succeeds, False otherwise. 28 | """ 29 | return True 30 | 31 | def get_auth_error_response(self, request): 32 | """ 33 | Get the response to return when authentication fails. 34 | 35 | Args: 36 | request: The HTTP request object. 37 | 38 | Returns: 39 | Response object for authentication error. 40 | """ 41 | return JSONResponse({"error": "not allowed"}, status_code=403) 42 | 43 | 44 | class JWTAuthentication(BaseAuthentication): 45 | """ 46 | JWT (JSON Web Token) based authentication. 47 | 48 | Authenticates requests using JWT tokens from the Authorization header. 49 | Validates token signatures and expiration times. 50 | Automatically skips authentication for OPTIONS requests (CORS preflight). 51 | 52 | Attributes: 53 | secret_key: Secret key for signing tokens. 54 | algorithm: JWT algorithm to use. 55 | expiration: Token expiration time in seconds. 56 | """ 57 | 58 | def __init__(self): 59 | if not config.jwt_secret: 60 | raise ValueError("JWT secret key not configured. Set LIGHTAPI_JWT_SECRET environment variable.") 61 | self.secret_key = config.jwt_secret 62 | self.algorithm = "HS256" 63 | self.expiration = 3600 # 1 hour default 64 | 65 | def authenticate(self, request): 66 | """ 67 | Authenticate a request using JWT token. 68 | Automatically allows OPTIONS requests for CORS preflight. 69 | 70 | Args: 71 | request: The HTTP request object. 72 | 73 | Returns: 74 | bool: True if authentication succeeds, False otherwise. 75 | """ 76 | # Skip authentication for OPTIONS requests (CORS preflight) 77 | if request.method == "OPTIONS": 78 | return True 79 | 80 | auth_header = request.headers.get("Authorization") 81 | if not auth_header or not auth_header.startswith("Bearer "): 82 | return False 83 | 84 | token = auth_header.split(" ")[1] 85 | try: 86 | payload = self.decode_token(token) 87 | request.state.user = payload 88 | return True 89 | except jwt.InvalidTokenError: 90 | return False 91 | 92 | def generate_token(self, payload: Dict, expiration: Optional[int] = None) -> str: 93 | """ 94 | Generate a JWT token. 95 | 96 | Args: 97 | payload: The data to encode in the token. 98 | expiration: Token expiration time in seconds. 99 | 100 | Returns: 101 | str: The encoded JWT token. 102 | """ 103 | exp_seconds = expiration or self.expiration 104 | token_data = { 105 | **payload, 106 | "exp": datetime.utcnow() + timedelta(seconds=exp_seconds), 107 | } 108 | return jwt.encode(token_data, self.secret_key, algorithm=self.algorithm) 109 | 110 | def decode_token(self, token: str) -> Dict: 111 | """ 112 | Decode and verify a JWT token. 113 | 114 | Args: 115 | token: The JWT token to decode. 116 | 117 | Returns: 118 | dict: The decoded token payload. 119 | 120 | Raises: 121 | jwt.InvalidTokenError: If the token is invalid or expired. 122 | """ 123 | return jwt.decode(token, self.secret_key, algorithms=[self.algorithm]) 124 | -------------------------------------------------------------------------------- /examples/01_general_usage.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String 2 | 3 | from lightapi.auth import JWTAuthentication 4 | from lightapi.cache import RedisCache 5 | from lightapi.core import LightApi, Middleware 6 | from lightapi.models import Base 7 | from lightapi.filters import ParameterFilter 8 | from lightapi.pagination import Paginator 9 | from lightapi.rest import Response, RestEndpoint, Validator 10 | 11 | 12 | class CustomEndpointValidator(Validator): 13 | def validate_name(self, value): 14 | return value 15 | 16 | def validate_email(self, value): 17 | return value 18 | 19 | def validate_website(self, value): 20 | return value 21 | 22 | 23 | class Company(Base, RestEndpoint): 24 | __table_args__ = {"extend_existing": True} 25 | """Company entity for demonstration purposes. 26 | 27 | This endpoint allows management of company information. 28 | """ 29 | 30 | __tablename__ = "companies" 31 | 32 | id = Column(Integer, primary_key=True) 33 | name = Column(String) 34 | email = Column(String, unique=True) 35 | website = Column(String) 36 | 37 | class Configuration: 38 | validator_class = CustomEndpointValidator 39 | filter_class = ParameterFilter 40 | 41 | async def post(self, request): 42 | """Create a new company. 43 | 44 | Accepts company data and creates a new record. 45 | """ 46 | return Response( 47 | {"data": "ok", "request_data": await request.get_data()}, 48 | status_code=200, 49 | content_type="application/json", 50 | ) 51 | 52 | def get(self, request): 53 | """Retrieve company information. 54 | 55 | Returns a list of companies or a specific company if ID is provided. 56 | """ 57 | return {"data": "ok"}, 200 58 | 59 | def headers(self, request): 60 | request.headers["X-New-Header"] = "my new header value" 61 | return request 62 | 63 | 64 | class CustomPaginator(Paginator): 65 | limit = 100 66 | sort = True 67 | 68 | 69 | class CustomEndpoint(Base, RestEndpoint): 70 | __tablename__ = "custom_endpoints" 71 | __table_args__ = {"extend_existing": True} 72 | 73 | id = Column(Integer, primary_key=True) 74 | 75 | class Configuration: 76 | http_method_names = ["GET", "POST"] 77 | authentication_class = JWTAuthentication 78 | caching_class = RedisCache 79 | caching_method_names = ["GET"] 80 | pagination_class = CustomPaginator 81 | 82 | async def post(self, request): 83 | return {"data": "ok"}, 200 84 | 85 | def get(self, request): 86 | return {"data": "ok"}, 200 87 | 88 | 89 | class MyCustomMiddleware(Middleware): 90 | def process(self, request, response): 91 | if "Authorization" not in request.headers: 92 | return Response({"error": "not allowed"}, status_code=403) 93 | return response 94 | 95 | 96 | class CORSMiddleware(Middleware): 97 | def process(self, request, response): 98 | if response is None: 99 | return None 100 | 101 | if hasattr(response, "headers"): 102 | response.headers["Access-Control-Allow-Origin"] = "*" 103 | response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS" 104 | response.headers["Access-Control-Allow-Headers"] = "Authorization, Content-Type" 105 | 106 | if request.method == "OPTIONS": 107 | return Response(status_code=200) 108 | return response 109 | 110 | 111 | if __name__ == "__main__": 112 | app = LightApi( 113 | database_url="sqlite:///example.db", 114 | swagger_title="LightAPI Example", 115 | swagger_version="1.0.0", 116 | swagger_description="Example API for demonstrating LightAPI capabilities", 117 | ) 118 | app.register(Company) 119 | app.register(CustomEndpoint) 120 | # app.add_middleware([MyCustomMiddleware, CORSMiddleware]) 121 | 122 | print("Server running at http://0.0.0.0:8000") 123 | print("API documentation available at http://0.0.0.0:8000/docs") 124 | 125 | app.run(host="0.0.0.0", port=8000, debug=True) 126 | -------------------------------------------------------------------------------- /examples/07_middleware_cors_auth.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, String 2 | 3 | from lightapi.auth import JWTAuthentication 4 | from lightapi.cache import RedisCache 5 | from lightapi.core import AuthenticationMiddleware, CORSMiddleware, Middleware, Response 6 | from lightapi.models import Base 7 | from lightapi.filters import ParameterFilter 8 | from lightapi.lightapi import LightApi 9 | from lightapi.pagination import Paginator 10 | from lightapi.rest import RestEndpoint, Validator 11 | 12 | 13 | class CustomEndpointValidator(Validator): 14 | def validate_name(self, value): 15 | return value 16 | 17 | def validate_email(self, value): 18 | return value 19 | 20 | def validate_website(self, value): 21 | return value 22 | 23 | 24 | class Company(Base, RestEndpoint): 25 | __table_args__ = {"extend_existing": True} 26 | name = Column(String) 27 | email = Column(String, unique=True) 28 | website = Column(String) 29 | 30 | class Configuration: 31 | http_method_names = ["GET", "POST", "OPTIONS"] 32 | validator_class = CustomEndpointValidator 33 | filter_class = ParameterFilter 34 | 35 | async def post(self, request): 36 | from starlette.responses import JSONResponse 37 | 38 | return JSONResponse({"status": "ok", "data": await request.get_data()}, status_code=200) 39 | 40 | def get(self, request): 41 | return {"data": "ok"}, 200 42 | 43 | def headers(self, request): 44 | # Headers in starlette are typically immutable during request processing 45 | # This method demonstrates header handling but shouldn't modify request headers 46 | # Instead, headers should be modified in the response 47 | return request 48 | 49 | 50 | class CustomPaginator(Paginator): 51 | limit = 100 52 | sort = True 53 | 54 | 55 | class CustomEndpoint(Base, RestEndpoint): 56 | __tablename__ = "customendpoint" 57 | __table_args__ = {"extend_existing": True} 58 | class Configuration: 59 | # Remove the http_method_names restriction to get full CRUD automatically 60 | # http_method_names = ['GET', 'POST', 'OPTIONS'] # This was limiting the methods! 61 | # OR specify all CRUD methods explicitly: 62 | http_method_names = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"] 63 | authentication_class = JWTAuthentication 64 | caching_class = RedisCache 65 | caching_method_names = ["GET"] 66 | pagination_class = CustomPaginator 67 | 68 | def get(self, request): 69 | """Retrieve resource(s).""" 70 | return {"data": "ok", "message": "GET request successful"}, 200 71 | 72 | async def post(self, request): 73 | """Create a new resource.""" 74 | return { 75 | "data": "ok", 76 | "message": "POST request successful", 77 | "body": await request.get_data(), 78 | }, 200 79 | 80 | async def put(self, request): 81 | """Update an existing resource (full update).""" 82 | return { 83 | "data": "updated", 84 | "message": "PUT request successful", 85 | "body": await request.get_data(), 86 | }, 200 87 | 88 | async def patch(self, request): 89 | """Partially update an existing resource.""" 90 | return { 91 | "data": "patched", 92 | "message": "PATCH request successful", 93 | "body": await request.get_data(), 94 | }, 200 95 | 96 | def delete(self, request): 97 | """Delete a resource.""" 98 | return {"data": "deleted", "message": "DELETE request successful"}, 200 99 | 100 | async def options(self, request): 101 | """Return allowed HTTP methods.""" 102 | return { 103 | "allowed_methods": ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], 104 | "message": "OPTIONS request successful", 105 | }, 200 106 | 107 | 108 | def create_app(): 109 | app = LightApi() 110 | app.register(Company) 111 | app.register(CustomEndpoint) 112 | # Use built-in middleware classes 113 | app.add_middleware([CORSMiddleware, AuthenticationMiddleware]) 114 | return app 115 | 116 | 117 | if __name__ == "__main__": 118 | create_app().run() 119 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: LightAPI 2 | site_url: https://iklobato.github.io/LightAPI/ 3 | site_description: LightApi is a lightweight API framework designed for rapid development of RESTful APIs in Python. 4 | 5 | repo_url: https://github.com/iklobato/LightAPI 6 | repo_name: LightAPI 7 | edit_uri: https://github.com/iklobato/LightAPI/edit/main/docs 8 | 9 | theme: 10 | name: material 11 | #custom_dir: overrides 12 | language: en 13 | icon: 14 | repo: fontawesome/brands/github 15 | edit: material/pencil 16 | view: material/eye 17 | 18 | palette: 19 | 20 | # Palette toggle for light mode 21 | - media: "(prefers-color-scheme: light)" 22 | primary: green 23 | accent: green 24 | scheme: default 25 | toggle: 26 | icon: material/brightness-7 27 | name: Switch to dark mode 28 | 29 | # Palette toggle for dark mode 30 | - media: "(prefers-color-scheme: dark)" 31 | primary: green 32 | accent: green 33 | scheme: slate 34 | toggle: 35 | icon: material/brightness-4 36 | name: Switch to system preference 37 | features: 38 | - content.action.edit 39 | - navigation.tabs 40 | - content.code.copy 41 | - content.code.annotate 42 | # - navigation.indexes 43 | - navigation.footer 44 | - navigation.instant 45 | - navigation.instant.progress 46 | - navigation.instant.preview 47 | - navigation.tracking 48 | 49 | 50 | plugins: 51 | - git-authors 52 | - git-revision-date-localized: 53 | enable_creation_date: true 54 | # - git-committers: 55 | # repository: iklobato/LightAPI 56 | # branch: main 57 | - search: 58 | lang: it 59 | - awesome-pages 60 | #- blog: 61 | # blog_toc: true 62 | # pagination_per_page: 5 63 | - tags 64 | - glightbox: 65 | touchNavigation: true 66 | loop: false 67 | effect: zoom 68 | slide_effect: slide 69 | width: 100% 70 | height: auto 71 | zoomable: true 72 | draggable: true 73 | skip_classes: 74 | - custom-skip-class-name 75 | auto_caption: false 76 | caption_position: bottom 77 | background: white 78 | shadow: true 79 | manual: false 80 | - mkdocstrings: 81 | handlers: 82 | python: 83 | inventories: 84 | - https://docs.python-requests.org/en/master/objects.inv 85 | options: 86 | annotations_path: brief 87 | separate_signature: true 88 | show_signature_annotations: true 89 | signature_crossrefs: true 90 | show_signature: true 91 | show_docstring_attributes: true 92 | show_docstring_functions: true 93 | show_labels: true 94 | group_by_category: true 95 | show_category_heading: true 96 | members_order: source 97 | show_if_no_docstring: true 98 | show_symbol_type_toc: true 99 | show_symbol_type_heading: true 100 | show_root_heading: true 101 | show_source: true 102 | docstring_style: google 103 | allow_inspection: true 104 | paths: [src] 105 | 106 | markdown_extensions: 107 | - def_list 108 | - pymdownx.tasklist: 109 | custom_checkbox: true 110 | - pymdownx.arithmatex: 111 | generic: true 112 | - pymdownx.tabbed: 113 | alternate_style: true 114 | - pymdownx.emoji: 115 | emoji_index: !!python/name:material.extensions.emoji.twemoji 116 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 117 | - pymdownx.highlight: 118 | anchor_linenums: true 119 | line_spans: __span 120 | pygments_lang_class: true 121 | - pymdownx.superfences: 122 | custom_fences: 123 | - name: mermaid 124 | class: mermaid 125 | format: !!python/name:pymdownx.superfences.fence_code_format 126 | - pymdownx.inlinehilite 127 | - pymdownx.snippets 128 | - pymdownx.superfences 129 | - admonition 130 | - pymdownx.details 131 | - attr_list 132 | - md_in_html 133 | - footnotes 134 | - def_list 135 | 136 | validation: 137 | omitted_files: warn 138 | absolute_links: warn # Or 'relative_to_docs' - new in MkDocs 1.6 139 | unrecognized_links: warn 140 | 141 | extra: 142 | social: 143 | - icon: fontawesome/brands/github 144 | link: https://github.com/iklobato/LightAPI 145 | name: LightAPI 146 | - icon: /fontawesome/regular/envelope 147 | name: send me an email 148 | link: mailto:iklobato1@gmail.com -------------------------------------------------------------------------------- /docs/api-reference/pagination.md: -------------------------------------------------------------------------------- 1 | # Pagination Reference 2 | 3 | The Pagination module provides tools for implementing paginated responses in LightAPI endpoints. 4 | 5 | ## Basic Pagination 6 | 7 | ### Enabling Pagination 8 | 9 | ```python 10 | from lightapi.rest import RESTEndpoint 11 | 12 | class UserEndpoint(RESTEndpoint): 13 | paginate = True 14 | items_per_page = 20 15 | ``` 16 | 17 | ### Pagination Parameters 18 | 19 | ```python 20 | # Default pagination parameters 21 | pagination_params = { 22 | 'page': 1, # Current page number 23 | 'per_page': 20, # Items per page 24 | 'max_per_page': 100 # Maximum items per page 25 | } 26 | ``` 27 | 28 | ## Advanced Pagination 29 | 30 | ### Custom Pagination 31 | 32 | ```python 33 | from lightapi.pagination import Paginator 34 | 35 | class CustomPaginator(Paginator): 36 | def get_pagination_data(self, total_items): 37 | return { 38 | 'total': total_items, 39 | 'pages': self.get_total_pages(total_items), 40 | 'current_page': self.page, 41 | 'has_next': self.has_next(total_items), 42 | 'has_prev': self.has_prev() 43 | } 44 | ``` 45 | 46 | ### Cursor-based Pagination 47 | 48 | ```python 49 | from lightapi.pagination import CursorPaginator 50 | 51 | class UserEndpoint(RESTEndpoint): 52 | paginator_class = CursorPaginator 53 | cursor_field = 'created_at' 54 | ``` 55 | 56 | ## Response Format 57 | 58 | ### Default Format 59 | 60 | ```python 61 | { 62 | "items": [...], 63 | "pagination": { 64 | "total": 100, 65 | "pages": 5, 66 | "current_page": 1, 67 | "per_page": 20, 68 | "has_next": true, 69 | "has_prev": false 70 | } 71 | } 72 | ``` 73 | 74 | ### Custom Format 75 | 76 | ```python 77 | class UserEndpoint(RESTEndpoint): 78 | def format_paginated_response(self, items, pagination_data): 79 | return { 80 | 'users': items, 81 | 'meta': { 82 | 'total_users': pagination_data['total'], 83 | 'page': pagination_data['current_page'], 84 | 'total_pages': pagination_data['pages'] 85 | } 86 | } 87 | ``` 88 | 89 | ## Examples 90 | 91 | ### Basic Pagination Example 92 | 93 | ```python 94 | from lightapi import LightAPI 95 | from lightapi.rest import RESTEndpoint 96 | from lightapi.pagination import Paginator 97 | 98 | app = LightAPI() 99 | 100 | class UserEndpoint(RESTEndpoint): 101 | route = '/users' 102 | model = User 103 | paginate = True 104 | items_per_page = 20 105 | 106 | def get(self, request): 107 | query = self.model.query 108 | paginated_query = self.paginate_query(query) 109 | return self.format_paginated_response( 110 | paginated_query.items, 111 | paginated_query.pagination_data 112 | ) 113 | ``` 114 | 115 | ### Advanced Pagination Example 116 | 117 | ```python 118 | class UserEndpoint(RESTEndpoint): 119 | route = '/users' 120 | model = User 121 | paginator_class = CursorPaginator 122 | cursor_field = 'created_at' 123 | items_per_page = 20 124 | 125 | def get(self, request): 126 | query = self.model.query.order_by(self.model.created_at.desc()) 127 | 128 | # Get cursor from request 129 | cursor = request.args.get('cursor') 130 | 131 | # Apply cursor-based pagination 132 | if cursor: 133 | query = query.filter(self.model.created_at < cursor) 134 | 135 | # Get paginated results 136 | paginated = self.paginate_query(query) 137 | 138 | # Format response 139 | return { 140 | 'users': [user.to_dict() for user in paginated.items], 141 | 'next_cursor': paginated.next_cursor, 142 | 'has_more': paginated.has_more 143 | } 144 | ``` 145 | 146 | ## URL Parameters 147 | 148 | ### Basic Pagination 149 | 150 | ``` 151 | GET /users?page=2&per_page=20 152 | ``` 153 | 154 | ### Cursor Pagination 155 | 156 | ``` 157 | GET /users?cursor=2023-01-01T12:00:00Z&per_page=20 158 | ``` 159 | 160 | ## Best Practices 161 | 162 | 1. Set reasonable default and maximum page sizes 163 | 2. Use cursor-based pagination for large datasets 164 | 3. Include proper metadata in responses 165 | 4. Handle invalid pagination parameters 166 | 5. Document pagination parameters and response format 167 | 168 | ## See Also 169 | 170 | - [REST API](rest.md) - REST endpoint implementation 171 | - [Filtering](filters.md) - Query filtering 172 | - [Database](database.md) - Database integration -------------------------------------------------------------------------------- /docs/api-reference/exceptions.md: -------------------------------------------------------------------------------- 1 | # Exceptions Reference 2 | 3 | The Exceptions module provides a comprehensive error handling system for LightAPI applications. 4 | 5 | ## Built-in Exceptions 6 | 7 | ### HTTP Exceptions 8 | 9 | ```python 10 | from lightapi.exceptions import ( 11 | HTTPException, 12 | NotFound, 13 | BadRequest, 14 | Unauthorized, 15 | Forbidden, 16 | MethodNotAllowed, 17 | Conflict, 18 | InternalServerError 19 | ) 20 | 21 | # Usage 22 | raise NotFound('User not found') 23 | raise BadRequest('Invalid input') 24 | ``` 25 | 26 | ### Validation Exceptions 27 | 28 | ```python 29 | from lightapi.exceptions import ( 30 | ValidationError, 31 | InvalidField, 32 | RequiredField, 33 | InvalidType 34 | ) 35 | 36 | # Usage 37 | raise ValidationError('Invalid data format') 38 | raise InvalidField('email', 'Invalid email format') 39 | ``` 40 | 41 | ### Database Exceptions 42 | 43 | ```python 44 | from lightapi.exceptions import ( 45 | DatabaseError, 46 | IntegrityError, 47 | ConnectionError, 48 | QueryError 49 | ) 50 | 51 | # Usage 52 | raise DatabaseError('Database connection failed') 53 | raise IntegrityError('Duplicate entry') 54 | ``` 55 | 56 | ## Custom Exceptions 57 | 58 | ### Creating Custom Exceptions 59 | 60 | ```python 61 | from lightapi.exceptions import HTTPException 62 | 63 | class CustomError(HTTPException): 64 | status_code = 400 65 | error_code = 'CUSTOM_ERROR' 66 | 67 | def __init__(self, message='Custom error occurred'): 68 | super().__init__(message) 69 | ``` 70 | 71 | ### Exception Handlers 72 | 73 | ```python 74 | from lightapi import LightAPI 75 | from lightapi.exceptions import HTTPException 76 | 77 | app = LightAPI() 78 | 79 | @app.error_handler(CustomError) 80 | def handle_custom_error(error): 81 | return { 82 | 'error': error.error_code, 83 | 'message': str(error) 84 | }, error.status_code 85 | ``` 86 | 87 | ## Error Response Format 88 | 89 | ### Default Format 90 | 91 | ```python 92 | { 93 | "error": "NOT_FOUND", 94 | "message": "User not found", 95 | "status_code": 404, 96 | "details": { 97 | "resource": "User", 98 | "id": "123" 99 | } 100 | } 101 | ``` 102 | 103 | ### Custom Format 104 | 105 | ```python 106 | @app.error_handler(HTTPException) 107 | def format_error(error): 108 | return { 109 | 'status': 'error', 110 | 'code': error.error_code, 111 | 'description': str(error), 112 | 'timestamp': datetime.now().isoformat() 113 | }, error.status_code 114 | ``` 115 | 116 | ## Examples 117 | 118 | ### Complete Error Handling Setup 119 | 120 | ```python 121 | from lightapi import LightAPI 122 | from lightapi.exceptions import ( 123 | HTTPException, 124 | NotFound, 125 | ValidationError, 126 | DatabaseError 127 | ) 128 | from datetime import datetime 129 | 130 | app = LightAPI() 131 | 132 | # Custom exception 133 | class BusinessLogicError(HTTPException): 134 | status_code = 400 135 | error_code = 'BUSINESS_LOGIC_ERROR' 136 | 137 | # Global error handler 138 | @app.error_handler(HTTPException) 139 | def handle_http_error(error): 140 | return { 141 | 'status': 'error', 142 | 'code': error.error_code, 143 | 'message': str(error), 144 | 'timestamp': datetime.now().isoformat() 145 | }, error.status_code 146 | 147 | # Specific error handlers 148 | @app.error_handler(ValidationError) 149 | def handle_validation_error(error): 150 | return { 151 | 'status': 'error', 152 | 'code': 'VALIDATION_ERROR', 153 | 'fields': error.fields, 154 | 'message': str(error) 155 | }, 400 156 | 157 | @app.error_handler(DatabaseError) 158 | def handle_database_error(error): 159 | return { 160 | 'status': 'error', 161 | 'code': 'DATABASE_ERROR', 162 | 'message': 'An internal error occurred' 163 | }, 500 164 | 165 | # Usage in endpoints 166 | @app.route('/users/') 167 | def get_user(request, id): 168 | user = User.query.get(id) 169 | if not user: 170 | raise NotFound(f'User {id} not found') 171 | return user.dict() 172 | 173 | @app.route('/users', methods=['POST']) 174 | def create_user(request): 175 | try: 176 | user = User(**request.json) 177 | user.save() 178 | except ValidationError as e: 179 | raise BadRequest(str(e)) 180 | except IntegrityError: 181 | raise Conflict('User already exists') 182 | return user.dict(), 201 183 | ``` 184 | 185 | ## Best Practices 186 | 187 | 1. Use appropriate exception types 188 | 2. Implement custom exceptions for business logic 189 | 3. Handle all exceptions appropriately 190 | 4. Provide meaningful error messages 191 | 5. Follow security best practices in error responses 192 | 193 | ## See Also 194 | 195 | - [Core API](core.md) - Core framework functionality 196 | - [REST API](rest.md) - REST endpoint implementation 197 | - [Validation](validation.md) - Request validation -------------------------------------------------------------------------------- /examples/02_authentication_jwt.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | 4 | import jwt 5 | from sqlalchemy import Column, Integer, String 6 | 7 | from lightapi.auth import JWTAuthentication 8 | from lightapi.config import config 9 | from lightapi.core import LightApi, Middleware, Response 10 | from lightapi.models import Base 11 | from lightapi.rest import RestEndpoint 12 | 13 | 14 | # Custom authentication class 15 | class CustomJWTAuth(JWTAuthentication): 16 | def __init__(self): 17 | super().__init__() 18 | self.secret_key = config.jwt_secret 19 | 20 | def authenticate(self, request): 21 | # Use the parent class implementation 22 | return super().authenticate(request) 23 | 24 | 25 | # Login endpoint to get a token 26 | class AuthEndpoint(Base, RestEndpoint): 27 | __abstract__ = True # Not a database model 28 | 29 | def post(self, request): 30 | data = getattr(request, "data", {}) 31 | username = data.get("username") 32 | password = data.get("password") 33 | 34 | # Simple authentication (replace with database lookup in real apps) 35 | if username == "admin" and password == "password": 36 | # Create a JWT token 37 | payload = { 38 | "sub": "user_1", 39 | "username": username, 40 | "role": "admin", 41 | "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1), 42 | } 43 | token = jwt.encode(payload, config.jwt_secret, algorithm="HS256") 44 | 45 | return {"token": token}, 200 46 | else: 47 | return Response({"error": "Invalid credentials"}, status_code=401) 48 | 49 | 50 | # Protected resource that requires authentication 51 | class SecretResource(Base, RestEndpoint): 52 | __abstract__ = True # Not a database model 53 | 54 | class Configuration: 55 | authentication_class = CustomJWTAuth 56 | 57 | def get(self, request): 58 | try: 59 | # Access the user info stored during authentication 60 | username = request.state.user.get("username") 61 | role = request.state.user.get("role") 62 | 63 | return { 64 | "message": f"Hello, {username}! You have {role} access.", 65 | "secret_data": "This is protected information", 66 | }, 200 67 | except Exception as e: 68 | import traceback 69 | 70 | print(f"Error in SecretResource.get: {e}") 71 | print(traceback.format_exc()) 72 | return {"error": str(e)}, 500 73 | 74 | 75 | # Public endpoint that doesn't require authentication 76 | class PublicResource(Base, RestEndpoint): 77 | __abstract__ = True # Not a database model 78 | 79 | def get(self, request): 80 | try: 81 | return {"message": "This is public information"}, 200 82 | except Exception as e: 83 | import traceback 84 | 85 | print(f"Error in PublicResource.get: {e}") 86 | print(traceback.format_exc()) 87 | return {"error": str(e)}, 500 88 | 89 | 90 | # User profile endpoint that requires authentication 91 | class UserProfile(Base, RestEndpoint): 92 | __tablename__ = "user_profiles" 93 | 94 | id = Column(Integer, primary_key=True) 95 | user_id = Column(String(50)) 96 | full_name = Column(String(100)) 97 | email = Column(String(100)) 98 | 99 | class Configuration: 100 | authentication_class = CustomJWTAuth 101 | 102 | # Override GET to return only the current user's profile 103 | def get(self, request): 104 | user_id = request.state.user.get("sub") 105 | profile = self.session.query(self.__class__).filter_by(user_id=user_id).first() 106 | 107 | if profile: 108 | return { 109 | "id": profile.id, 110 | "user_id": profile.user_id, 111 | "full_name": profile.full_name, 112 | "email": profile.email, 113 | }, 200 114 | else: 115 | return Response({"error": "Profile not found"}, status_code=404) 116 | 117 | 118 | if __name__ == "__main__": 119 | app = LightApi( 120 | database_url="sqlite:///auth_example.db", 121 | swagger_title="Authentication Example", 122 | swagger_version="1.0.0", 123 | swagger_description="Example showing JWT authentication with LightAPI", 124 | ) 125 | 126 | app.register(AuthEndpoint) 127 | app.register(PublicResource) 128 | app.register(SecretResource) 129 | app.register(UserProfile) 130 | 131 | print("Server running at http://localhost:8000") 132 | print("API documentation available at http://localhost:8000/docs") 133 | print("\nTo get a token:") 134 | print( 135 | 'curl -X POST http://localhost:8000/auth/login -H \'Content-Type: application/json\' -d \'{"username": "admin", "password": "password"}\'' 136 | ) 137 | print("\nTo access protected resource:") 138 | print("curl -X GET http://localhost:8000/secret -H 'Authorization: Bearer YOUR_TOKEN'") 139 | 140 | app.run(host="localhost", port=8000, debug=True) 141 | -------------------------------------------------------------------------------- /tests/test_rest.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import MagicMock, patch 2 | 3 | import pytest 4 | from sqlalchemy import Column, Integer, String 5 | from starlette.requests import Request 6 | 7 | from lightapi.core import Response 8 | from lightapi.rest import RestEndpoint, Validator 9 | 10 | 11 | class TestValidator(Validator): 12 | """ 13 | Test validator implementation for testing validation logic. 14 | 15 | This validator implements name validation to demonstrate 16 | custom validation logic within the REST framework. 17 | """ 18 | 19 | def validate_name(self, value): 20 | """ 21 | Validate a name field value. 22 | 23 | Args: 24 | value: The name value to validate. 25 | 26 | Returns: 27 | str: The validated and transformed name (uppercase). 28 | 29 | Raises: 30 | ValueError: If the name is less than 3 characters. 31 | """ 32 | if len(value) < 3: 33 | raise ValueError("Name too short") 34 | return value.upper() 35 | 36 | 37 | class TestModel(RestEndpoint): 38 | """ 39 | Test endpoint model for testing the RestEndpoint functionality. 40 | 41 | Defines a simple model with basic fields and configuration 42 | for use in the REST endpoint tests. 43 | """ 44 | 45 | __tablename__ = "test_models" 46 | 47 | id = Column(Integer, primary_key=True) 48 | name = Column(String) 49 | email = Column(String) 50 | 51 | class Configuration: 52 | """Configuration for the test model endpoint.""" 53 | 54 | http_method_names = ["GET", "POST", "PUT", "DELETE"] 55 | validator_class = TestValidator 56 | 57 | def post(self, request): 58 | """ 59 | Handle POST requests for the test model. 60 | 61 | This is a simplified implementation specifically for testing, 62 | which validates the data but doesn't create a database record. 63 | 64 | Args: 65 | request: The HTTP request object. 66 | 67 | Returns: 68 | tuple: A tuple containing the response data and status code. 69 | 70 | Raises: 71 | Exception: If validation fails. 72 | """ 73 | try: 74 | data = getattr(request, "data", {}) 75 | 76 | if hasattr(self, "validator"): 77 | validated_data = self.validator.validate(data) 78 | data = validated_data 79 | 80 | return {"result": data}, 201 81 | except Exception as e: 82 | return {"error": str(e)}, 400 83 | 84 | 85 | class TestRestEndpoint: 86 | """ 87 | Test suite for the RestEndpoint class functionality. 88 | 89 | Tests various aspects of the RestEndpoint implementation, 90 | including model definition, configuration, setup, and HTTP methods. 91 | """ 92 | 93 | def test_model_definition(self): 94 | """Test that the model definition is correctly set up.""" 95 | assert TestModel.__tablename__ == "test_models" 96 | assert hasattr(TestModel, "id") 97 | assert hasattr(TestModel, "name") 98 | assert hasattr(TestModel, "email") 99 | 100 | def test_configuration(self): 101 | """Test that the model configuration is correctly set up.""" 102 | assert TestModel.Configuration.http_method_names == [ 103 | "GET", 104 | "POST", 105 | "PUT", 106 | "DELETE", 107 | ] 108 | assert TestModel.Configuration.validator_class == TestValidator 109 | 110 | def test_setup(self): 111 | """Test that the endpoint setup correctly initializes components.""" 112 | endpoint = TestModel() 113 | mock_request = MagicMock() 114 | mock_session = MagicMock() 115 | 116 | endpoint._setup(mock_request, mock_session) 117 | 118 | assert endpoint.request == mock_request 119 | assert endpoint.session == mock_session 120 | assert hasattr(endpoint, "validator") 121 | 122 | def test_get_method(self): 123 | """Test that the GET method returns the expected response.""" 124 | endpoint = TestModel() 125 | mock_request = MagicMock() 126 | mock_session = MagicMock() 127 | mock_query = MagicMock() 128 | mock_session.query.return_value = mock_query 129 | mock_query.all.return_value = [] 130 | 131 | endpoint._setup(mock_request, mock_session) 132 | response, status_code = endpoint.get(mock_request) 133 | 134 | assert status_code == 200 135 | assert "results" in response 136 | assert isinstance(response["results"], list) 137 | 138 | def test_post_method(self): 139 | """Test that the POST method correctly validates and returns data.""" 140 | endpoint = TestModel() 141 | mock_request = MagicMock() 142 | mock_request.data = {"name": "Test", "email": "test@example.com"} 143 | mock_session = MagicMock() 144 | 145 | endpoint._setup(mock_request, mock_session) 146 | 147 | response, status_code = endpoint.post(mock_request) 148 | 149 | assert status_code == 201 150 | assert "result" in response 151 | assert response["result"]["name"] == "TEST" 152 | 153 | 154 | # All generic CRUD endpoint tests from deleted files are now parameterized here as TestEndpoints. 155 | -------------------------------------------------------------------------------- /lightapi/models.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from datetime import datetime 3 | 4 | import sqlalchemy 5 | import sqlalchemy.orm 6 | from sqlalchemy import Boolean, Column, DateTime, Integer, String 7 | 8 | from lightapi.database import Base 9 | 10 | 11 | def setup_database(database_url: str = "sqlite:///app.db"): 12 | """ 13 | Set up the database connection and create tables. 14 | 15 | Initializes SQLAlchemy with the provided database URL, 16 | creates the database tables, and returns the engine 17 | and session factory. 18 | 19 | Args: 20 | database_url: The SQLAlchemy database URL. 21 | 22 | Returns: 23 | tuple: A tuple containing (engine, Session). 24 | """ 25 | engine = sqlalchemy.create_engine(database_url) 26 | 27 | try: 28 | Base.metadata.create_all(engine) 29 | except Exception: 30 | pass 31 | 32 | Session = sqlalchemy.orm.sessionmaker(bind=engine) 33 | return engine, Session 34 | 35 | 36 | 37 | 38 | class Person(Base): 39 | """ 40 | Person model representing a user or individual. 41 | 42 | Attributes: 43 | id: Primary key. 44 | name: Person's name. 45 | email: Person's email address (unique). 46 | email_verified: Whether the email has been verified. 47 | """ 48 | 49 | __tablename__ = "person" 50 | 51 | id = Column(Integer, primary_key=True, autoincrement=True) 52 | name = Column(String) 53 | email = Column(String, unique=True) 54 | email_verified = Column(Boolean, default=False) 55 | 56 | def as_dict(self): 57 | """ 58 | Convert the model instance to a dictionary. 59 | 60 | Returns: 61 | dict: Dictionary representation of the person. 62 | """ 63 | return { 64 | "id": self.id, 65 | "name": self.name, 66 | "email": self.email, 67 | "email_verified": self.email_verified, 68 | } 69 | 70 | def serialize(self): 71 | result = {} 72 | for col in self.__table__.columns: 73 | val = getattr(self, col.name) 74 | if isinstance(val, bytes): 75 | result[col.name] = base64.b64encode(val).decode() 76 | elif isinstance(val, (datetime, datetime.date)): 77 | result[col.name] = val.isoformat() 78 | else: 79 | result[col.name] = val 80 | return result 81 | 82 | 83 | class Company(Base): 84 | """ 85 | Company model representing a business organization. 86 | 87 | Attributes: 88 | id: Primary key. 89 | name: Company name. 90 | email: Company email address (unique). 91 | website: Company website URL. 92 | """ 93 | 94 | __tablename__ = "company" 95 | 96 | id = Column(Integer, primary_key=True, autoincrement=True) 97 | name = Column(String) 98 | email = Column(String, unique=True) 99 | website = Column(String) 100 | 101 | def as_dict(self): 102 | """ 103 | Convert the model instance to a dictionary. 104 | 105 | Returns: 106 | dict: Dictionary representation of the company. 107 | """ 108 | return { 109 | "id": self.id, 110 | "name": self.name, 111 | "email": self.email, 112 | "website": self.website, 113 | } 114 | 115 | def serialize(self): 116 | result = {} 117 | for col in self.__table__.columns: 118 | val = getattr(self, col.name) 119 | if isinstance(val, bytes): 120 | result[col.name] = base64.b64encode(val).decode() 121 | elif isinstance(val, (datetime, datetime.date)): 122 | result[col.name] = val.isoformat() 123 | else: 124 | result[col.name] = val 125 | return result 126 | 127 | 128 | class Post(Base): 129 | """ 130 | Post model representing a blog post. 131 | 132 | Attributes: 133 | id: Primary key. 134 | title: Title of the post. 135 | content: Content of the post. 136 | created_at: Timestamp when the post was created. 137 | """ 138 | 139 | __tablename__ = "post" 140 | 141 | id = Column(Integer, primary_key=True, autoincrement=True) 142 | title = Column(String, nullable=False) 143 | content = Column(String, nullable=False) 144 | created_at = Column(DateTime, default=datetime.utcnow, nullable=False) 145 | 146 | def as_dict(self): 147 | """ 148 | Convert the model instance to a dictionary. 149 | 150 | Returns: 151 | dict: Dictionary representation of the post. 152 | """ 153 | return { 154 | "id": self.id, 155 | "title": self.title, 156 | "content": self.content, 157 | "created_at": self.created_at.isoformat(), 158 | } 159 | 160 | def serialize(self): 161 | result = {} 162 | for col in self.__table__.columns: 163 | val = getattr(self, col.name) 164 | if isinstance(val, bytes): 165 | result[col.name] = base64.b64encode(val).decode() 166 | elif isinstance(val, (datetime, datetime.date)): 167 | result[col.name] = val.isoformat() 168 | else: 169 | result[col.name] = val 170 | return result 171 | -------------------------------------------------------------------------------- /docs/api-reference/models.md: -------------------------------------------------------------------------------- 1 | # Models Reference 2 | 3 | The Models module provides tools for defining data models, schema validation, and serialization in LightAPI. 4 | 5 | ## Model Definition 6 | 7 | ### Basic Model 8 | 9 | ```python 10 | from lightapi.models import Model, Field 11 | 12 | class User(Model): 13 | name: str = Field(min_length=2, max_length=50) 14 | email: str = Field(format='email') 15 | age: int = Field(ge=0, optional=True) 16 | status: str = Field(choices=['active', 'inactive']) 17 | ``` 18 | 19 | ### Field Types 20 | 21 | ```python 22 | from lightapi.models import ( 23 | StringField, 24 | IntegerField, 25 | FloatField, 26 | BooleanField, 27 | DateTimeField, 28 | ListField, 29 | DictField 30 | ) 31 | 32 | class Product(Model): 33 | name: str = StringField(min_length=1) 34 | price: float = FloatField(gt=0) 35 | in_stock: bool = BooleanField(default=True) 36 | created_at: datetime = DateTimeField(auto_now_add=True) 37 | tags: List[str] = ListField(StringField()) 38 | metadata: Dict = DictField(default={}) 39 | ``` 40 | 41 | ## Validation 42 | 43 | ### Basic Validation 44 | 45 | ```python 46 | # Validate at instantiation 47 | user = User( 48 | name='John', 49 | email='john@example.com', 50 | age=30, 51 | status='active' 52 | ) 53 | 54 | # Validate manually 55 | user.validate() 56 | ``` 57 | 58 | ### Custom Validators 59 | 60 | ```python 61 | from lightapi.models import validator 62 | 63 | class User(Model): 64 | username: str = Field() 65 | password: str = Field() 66 | 67 | @validator('password') 68 | def validate_password(cls, value): 69 | if len(value) < 8: 70 | raise ValueError('Password must be at least 8 characters') 71 | if not any(c.isupper() for c in value): 72 | raise ValueError('Password must contain uppercase letter') 73 | return value 74 | ``` 75 | 76 | ## Serialization 77 | 78 | ### Basic Serialization 79 | 80 | ```python 81 | # To dictionary 82 | data = user.dict() 83 | 84 | # To JSON 85 | json_data = user.json() 86 | 87 | # From dictionary 88 | user = User.from_dict(data) 89 | 90 | # From JSON 91 | user = User.from_json(json_string) 92 | ``` 93 | 94 | ### Custom Serialization 95 | 96 | ```python 97 | class User(Model): 98 | name: str 99 | email: str 100 | password: str 101 | 102 | def dict(self, exclude=None): 103 | data = super().dict(exclude={'password'}) 104 | return data 105 | ``` 106 | 107 | ## Relationships 108 | 109 | ### Model References 110 | 111 | ```python 112 | class Post(Model): 113 | title: str 114 | content: str 115 | author: User = Field(reference=True) 116 | ``` 117 | 118 | ### Nested Models 119 | 120 | ```python 121 | class Address(Model): 122 | street: str 123 | city: str 124 | country: str 125 | 126 | class User(Model): 127 | name: str 128 | email: str 129 | address: Address 130 | ``` 131 | 132 | ## Examples 133 | 134 | ### Complete Model Example 135 | 136 | ```python 137 | from lightapi.models import Model, Field, validator 138 | from typing import List, Optional 139 | from datetime import datetime 140 | 141 | class User(Model): 142 | id: int = Field(primary_key=True) 143 | username: str = Field(min_length=3, max_length=50) 144 | email: str = Field(format='email') 145 | password: str = Field(min_length=8) 146 | status: str = Field(choices=['active', 'inactive'], default='active') 147 | created_at: datetime = Field(auto_now_add=True) 148 | last_login: Optional[datetime] = Field(null=True) 149 | roles: List[str] = Field(default=['user']) 150 | 151 | @validator('username') 152 | def validate_username(cls, value): 153 | if not value.isalnum(): 154 | raise ValueError('Username must be alphanumeric') 155 | return value 156 | 157 | @validator('password') 158 | def validate_password(cls, value): 159 | if not any(c.isupper() for c in value): 160 | raise ValueError('Password must contain uppercase letter') 161 | if not any(c.isdigit() for c in value): 162 | raise ValueError('Password must contain a number') 163 | return value 164 | 165 | def dict(self, exclude=None): 166 | # Exclude password from serialization 167 | data = super().dict(exclude={'password'}) 168 | return data 169 | 170 | # Usage example 171 | try: 172 | user = User( 173 | username='john_doe', 174 | email='john@example.com', 175 | password='SecurePass123', 176 | roles=['user', 'admin'] 177 | ) 178 | user.validate() 179 | print(user.dict()) 180 | except ValueError as e: 181 | print(f'Validation error: {e}') 182 | ``` 183 | 184 | ## Best Practices 185 | 186 | 1. Define clear validation rules 187 | 2. Use appropriate field types 188 | 3. Implement custom validation when needed 189 | 4. Handle sensitive data appropriately 190 | 5. Use type hints for better IDE support 191 | 192 | ## See Also 193 | 194 | - [Database](database.md) - Database integration 195 | - [REST API](rest.md) - REST endpoint implementation 196 | - [Validation](validation.md) - Request validation 197 | 198 | > **Note:** Only GET, POST, PUT, PATCH, DELETE HTTP verbs are supported. OPTIONS and HEAD are not available. Required fields must be NOT NULL in the schema. Constraint violations (NOT NULL, UNIQUE, FK) return 409. -------------------------------------------------------------------------------- /examples/10_comprehensive_ideal_usage.py: -------------------------------------------------------------------------------- 1 | """ 2 | LightAPI User Goal Example 3 | 4 | This example demonstrates how to use LightAPI as envisioned by the user: 5 | - Custom validators 6 | - JWT authentication 7 | - Custom middleware 8 | - CORS support 9 | - Multiple endpoints with different configurations 10 | - Request/response handling 11 | - Pagination and caching (commented out due to current limitations) 12 | 13 | Run with: LIGHTAPI_JWT_SECRET="test-secret-key-123" uv run python examples/user_goal_example.py 14 | """ 15 | 16 | import os 17 | 18 | from lightapi import Base, LightApi, Middleware, RestEndpoint 19 | from lightapi.auth import JWTAuthentication 20 | from lightapi.cache import RedisCache 21 | from lightapi.core import Response 22 | from lightapi.filters import ParameterFilter 23 | from lightapi.pagination import Paginator 24 | 25 | # Set JWT secret for testing 26 | os.environ["LIGHTAPI_JWT_SECRET"] = "test-secret-key-123" 27 | 28 | 29 | class CustomEndpointValidator: 30 | """Custom validator for endpoint data validation""" 31 | 32 | def validate_name(self, value): 33 | return value 34 | 35 | def validate_email(self, value): 36 | return value 37 | 38 | def validate_website(self, value): 39 | return value 40 | 41 | 42 | class Company(Base, RestEndpoint): 43 | __table_args__ = {"extend_existing": True} 44 | """Company endpoint - no authentication required""" 45 | 46 | class Configuration: 47 | http_method_names = ["GET", "POST"] 48 | validator_class = CustomEndpointValidator 49 | filter_class = ParameterFilter 50 | 51 | async def post(self, request): 52 | """Handle POST requests with custom Response object""" 53 | return Response( 54 | {"data": "ok", "request_data": await request.get_data()}, 55 | status_code=200, 56 | content_type="application/json", 57 | ) 58 | 59 | def get(self, request): 60 | """Handle GET requests with tuple response""" 61 | return {"data": "ok"}, 200 62 | 63 | 64 | class CustomPaginator(Paginator): 65 | """Custom pagination configuration""" 66 | 67 | limit = 100 68 | sort = True 69 | 70 | 71 | class CustomEndpoint(Base, RestEndpoint): 72 | """Custom endpoint with JWT authentication""" 73 | __table_args__ = {"extend_existing": True} 74 | 75 | class Configuration: 76 | http_method_names = ["GET", "POST"] 77 | authentication_class = JWTAuthentication 78 | # Note: Caching and pagination are commented out due to current serialization issues 79 | # These features work individually but cause conflicts when combined 80 | # caching_class = RedisCache 81 | # caching_method_names = ['GET'] 82 | # pagination_class = CustomPaginator 83 | 84 | def _serialize(self, obj): 85 | """Serialize model to dict.""" 86 | return {c.name: getattr(obj, c.name) for c in obj.__table__.columns} 87 | 88 | def post(self, request): 89 | """Handle authenticated POST requests""" 90 | return {"data": "ok", "message": "POST successful"}, 200 91 | 92 | def get(self, request): 93 | """Handle authenticated GET requests""" 94 | return {"data": "ok", "message": "GET successful"}, 200 95 | 96 | 97 | class MyCustomMiddleware(Middleware): 98 | """Custom middleware for additional authentication checks""" 99 | 100 | def process(self, request, response): 101 | if response is None: # Pre-processing 102 | if "Authorization" not in request.headers: 103 | return Response({"error": "not allowed"}, status_code=403) 104 | return None 105 | return response 106 | 107 | 108 | class CustomCORSMiddleware(Middleware): 109 | """Custom CORS middleware (renamed to avoid conflicts with Starlette's CORSMiddleware)""" 110 | 111 | def process(self, request, response): 112 | if response is None: # Pre-processing 113 | if request.method == "OPTIONS": 114 | return Response( 115 | {}, 116 | status_code=200, 117 | headers={ 118 | "Access-Control-Allow-Origin": "*", 119 | "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", 120 | "Access-Control-Allow-Headers": "Authorization, Content-Type", 121 | }, 122 | ) 123 | return None 124 | 125 | # Post-processing - add CORS headers 126 | # Note: Direct header modification can cause serialization issues 127 | # For production use, consider using LightApi's built-in CORS support 128 | return response 129 | 130 | 131 | # Create the API instance 132 | app = LightApi() 133 | 134 | # Register endpoints 135 | app.register(CustomEndpoint) 136 | app.register(Company) 137 | 138 | # Add middleware 139 | # Note: Custom auth middleware is commented out to avoid blocking all requests 140 | # In production, you would configure this more selectively 141 | # app.add_middleware([MyCustomMiddleware, CustomCORSMiddleware]) 142 | 143 | def _print_usage(): 144 | """Print usage instructions.""" 145 | print("🚀 Starting LightAPI Comprehensive Example") 146 | print("📋 Available endpoints:") 147 | print(" • /company - No authentication required") 148 | print(" • /custom - JWT authentication required") 149 | print("🔑 Generate JWT token with:") 150 | print( 151 | " python -c \"import jwt; import datetime; print(jwt.encode({'user_id': 1, 'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)}, 'test-secret-key-123', algorithm='HS256'))\"" 152 | ) 153 | print("📚 API Documentation: http://127.0.0.1:8000/api/docs") 154 | print("=" * 80) 155 | 156 | 157 | if __name__ == "__main__": 158 | _print_usage() 159 | app.run(host="127.0.0.1", port=8000) 160 | -------------------------------------------------------------------------------- /docs/api-reference/swagger.md: -------------------------------------------------------------------------------- 1 | # Swagger Integration Reference 2 | 3 | The Swagger module provides automatic OpenAPI/Swagger documentation generation for LightAPI endpoints. 4 | 5 | ## Basic Setup 6 | 7 | ### Enabling Swagger 8 | 9 | ```python 10 | from lightapi import LightAPI 11 | from lightapi.swagger import SwaggerUI 12 | 13 | app = LightAPI() 14 | swagger = SwaggerUI(app) 15 | ``` 16 | 17 | ### Configuration Options 18 | 19 | ```python 20 | swagger = SwaggerUI( 21 | app, 22 | title='My API', 23 | version='1.0.0', 24 | description='API documentation', 25 | base_url='/api', 26 | swagger_url='/docs' 27 | ) 28 | ``` 29 | 30 | ## API Documentation 31 | 32 | ### Endpoint Documentation 33 | 34 | ```python 35 | from lightapi.rest import RESTEndpoint 36 | from lightapi.swagger import swagger_doc 37 | 38 | @swagger_doc( 39 | summary='Get user information', 40 | description='Retrieve user details by ID', 41 | responses={ 42 | 200: {'description': 'User found'}, 43 | 404: {'description': 'User not found'} 44 | } 45 | ) 46 | class UserEndpoint(RESTEndpoint): 47 | route = '/users/{user_id}' 48 | ``` 49 | 50 | ### Request Parameters 51 | 52 | ```python 53 | @swagger_doc( 54 | parameters=[ 55 | { 56 | 'name': 'user_id', 57 | 'in': 'path', 58 | 'required': True, 59 | 'schema': {'type': 'integer'} 60 | }, 61 | { 62 | 'name': 'include_posts', 63 | 'in': 'query', 64 | 'schema': {'type': 'boolean'} 65 | } 66 | ] 67 | ) 68 | def get(self, request, user_id): 69 | pass 70 | ``` 71 | 72 | ### Request Body 73 | 74 | ```python 75 | @swagger_doc( 76 | request_body={ 77 | 'content': { 78 | 'application/json': { 79 | 'schema': { 80 | 'type': 'object', 81 | 'properties': { 82 | 'name': {'type': 'string'}, 83 | 'email': {'type': 'string'} 84 | }, 85 | 'required': ['name', 'email'] 86 | } 87 | } 88 | } 89 | } 90 | ) 91 | def post(self, request): 92 | pass 93 | ``` 94 | 95 | ## Advanced Features 96 | 97 | ### Security Schemes 98 | 99 | ```python 100 | swagger = SwaggerUI( 101 | app, 102 | security_schemes={ 103 | 'bearerAuth': { 104 | 'type': 'http', 105 | 'scheme': 'bearer', 106 | 'bearerFormat': 'JWT' 107 | } 108 | } 109 | ) 110 | 111 | @swagger_doc(security=[{'bearerAuth': []}]) 112 | class ProtectedEndpoint(RESTEndpoint): 113 | pass 114 | ``` 115 | 116 | ### Tags and Categories 117 | 118 | ```python 119 | @swagger_doc( 120 | tags=['users'], 121 | summary='User management endpoints' 122 | ) 123 | class UserEndpoint(RESTEndpoint): 124 | pass 125 | ``` 126 | 127 | ## Examples 128 | 129 | ### Complete Swagger Setup 130 | 131 | ```python 132 | from lightapi import LightAPI 133 | from lightapi.rest import RESTEndpoint 134 | from lightapi.swagger import SwaggerUI, swagger_doc 135 | 136 | # Initialize app and Swagger 137 | app = LightAPI() 138 | swagger = SwaggerUI( 139 | app, 140 | title='User Management API', 141 | version='1.0.0', 142 | description='API for managing users and posts', 143 | security_schemes={ 144 | 'bearerAuth': { 145 | 'type': 'http', 146 | 'scheme': 'bearer', 147 | 'bearerFormat': 'JWT' 148 | } 149 | } 150 | ) 151 | 152 | # Document endpoints 153 | @swagger_doc( 154 | tags=['users'], 155 | summary='User operations', 156 | security=[{'bearerAuth': []}] 157 | ) 158 | class UserEndpoint(RESTEndpoint): 159 | route = '/users/{user_id}' 160 | 161 | @swagger_doc( 162 | summary='Get user details', 163 | parameters=[ 164 | { 165 | 'name': 'user_id', 166 | 'in': 'path', 167 | 'required': True, 168 | 'schema': {'type': 'integer'} 169 | } 170 | ], 171 | responses={ 172 | 200: { 173 | 'description': 'User found', 174 | 'content': { 175 | 'application/json': { 176 | 'schema': { 177 | 'type': 'object', 178 | 'properties': { 179 | 'id': {'type': 'integer'}, 180 | 'name': {'type': 'string'}, 181 | 'email': {'type': 'string'} 182 | } 183 | } 184 | } 185 | } 186 | }, 187 | 404: {'description': 'User not found'} 188 | } 189 | ) 190 | def get(self, request, user_id): 191 | pass 192 | 193 | @swagger_doc( 194 | summary='Update user', 195 | request_body={ 196 | 'content': { 197 | 'application/json': { 198 | 'schema': { 199 | 'type': 'object', 200 | 'properties': { 201 | 'name': {'type': 'string'}, 202 | 'email': {'type': 'string'} 203 | } 204 | } 205 | } 206 | } 207 | } 208 | ) 209 | def put(self, request, user_id): 210 | pass 211 | ``` 212 | 213 | ## Best Practices 214 | 215 | 1. Document all endpoints thoroughly 216 | 2. Include response schemas 217 | 3. Document error responses 218 | 4. Use appropriate tags for organization 219 | 5. Keep documentation up to date 220 | 221 | ## See Also 222 | 223 | - [REST API](rest.md) - REST endpoint implementation 224 | - [Authentication](auth.md) - Authentication setup 225 | - [Models](models.md) - Data model definitions -------------------------------------------------------------------------------- /examples/09_yaml_advanced_permissions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Advanced YAML Configuration - Role-Based Permissions Example 4 | 5 | This example demonstrates advanced YAML configuration with different permission 6 | levels for different tables, simulating a real-world application with role-based access. 7 | 8 | Features demonstrated: 9 | - Different CRUD operations per table 10 | - Read-only tables 11 | - Limited operations (create/update only) 12 | - Administrative vs user permissions 13 | - Complex database schema 14 | """ 15 | 16 | import os 17 | import tempfile 18 | import yaml 19 | from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, ForeignKey, DECIMAL 20 | from sqlalchemy import create_engine 21 | from sqlalchemy.sql import func 22 | from lightapi import LightApi 23 | from lightapi.models import Base 24 | 25 | # Constants 26 | DEFAULT_PORT = 8000 27 | 28 | if __name__ == "__main__": 29 | # Define SQLAlchemy models in main section 30 | class User(Base): 31 | __tablename__ = "users" 32 | id = Column(Integer, primary_key=True) 33 | username = Column(String(50), unique=True, nullable=False) 34 | email = Column(String(100), unique=True, nullable=False) 35 | role = Column(String(20), default="user") 36 | is_active = Column(Boolean, default=True) 37 | created_at = Column(DateTime, default=func.now()) 38 | 39 | class Product(Base): 40 | __tablename__ = "products" 41 | id = Column(Integer, primary_key=True) 42 | name = Column(String(200), nullable=False) 43 | description = Column(Text) 44 | price = Column(DECIMAL(10, 2), nullable=False) 45 | category_id = Column(Integer, ForeignKey("categories.id")) 46 | is_active = Column(Boolean, default=True) 47 | created_at = Column(DateTime, default=func.now()) 48 | 49 | class Category(Base): 50 | __tablename__ = "categories" 51 | id = Column(Integer, primary_key=True) 52 | name = Column(String(100), unique=True, nullable=False) 53 | description = Column(Text) 54 | is_active = Column(Boolean, default=True) 55 | 56 | class Order(Base): 57 | __tablename__ = "orders" 58 | id = Column(Integer, primary_key=True) 59 | user_id = Column(Integer, ForeignKey("users.id"), nullable=False) 60 | total_amount = Column(DECIMAL(10, 2), nullable=False) 61 | status = Column(String(20), default="pending") 62 | created_at = Column(DateTime, default=func.now()) 63 | 64 | class OrderItem(Base): 65 | __tablename__ = "order_items" 66 | id = Column(Integer, primary_key=True) 67 | order_id = Column(Integer, ForeignKey("orders.id"), nullable=False) 68 | product_id = Column(Integer, ForeignKey("products.id"), nullable=False) 69 | quantity = Column(Integer, nullable=False) 70 | unit_price = Column(DECIMAL(10, 2), nullable=False) 71 | 72 | class AuditLog(Base): 73 | __tablename__ = "audit_logs" 74 | id = Column(Integer, primary_key=True) 75 | user_id = Column(Integer, ForeignKey("users.id")) 76 | action = Column(String(50), nullable=False) 77 | table_name = Column(String(50), nullable=False) 78 | record_id = Column(Integer) 79 | details = Column(Text) 80 | created_at = Column(DateTime, default=func.now()) 81 | 82 | def create_advanced_database(): 83 | """Create a complex database using SQLAlchemy ORM.""" 84 | # Create temporary database 85 | db_file = tempfile.NamedTemporaryFile(suffix='.db', delete=False) 86 | db_path = db_file.name 87 | db_file.close() 88 | 89 | # Create engine and tables using ORM 90 | engine = create_engine(f"sqlite:///{db_path}") 91 | Base.metadata.create_all(engine) 92 | 93 | return db_path 94 | 95 | def create_yaml_config(db_path): 96 | """Create advanced YAML configuration file with permissions.""" 97 | config = { 98 | 'database_url': f'sqlite:///{db_path}', 99 | 'tables': [ 100 | {'name': 'users', 'methods': ['GET', 'POST', 'PUT', 'DELETE']}, 101 | {'name': 'products', 'methods': ['GET', 'POST', 'PUT']}, 102 | {'name': 'categories', 'methods': ['GET']}, 103 | {'name': 'orders', 'methods': ['GET', 'POST']}, 104 | {'name': 'order_items', 'methods': ['GET', 'POST']}, 105 | {'name': 'audit_logs', 'methods': ['GET']} 106 | ] 107 | } 108 | 109 | config_path = os.path.join(os.path.dirname(__file__), 'advanced_permissions_config.yaml') 110 | with open(config_path, 'w') as f: 111 | yaml.dump(config, f, default_flow_style=False) 112 | 113 | return config_path 114 | 115 | def _print_usage(): 116 | """Print usage instructions.""" 117 | print("🚀 Advanced YAML Configuration - Role-Based Permissions") 118 | print("=" * 60) 119 | print("This example demonstrates:") 120 | print("• Different CRUD operations per table") 121 | print("• Read-only tables") 122 | print("• Limited operations (create/update only)") 123 | print("• Administrative vs user permissions") 124 | print("• Complex database schema") 125 | print() 126 | print("Server running at http://localhost:8000") 127 | print("API documentation available at http://localhost:8000/docs") 128 | print() 129 | print("Available endpoints with permissions:") 130 | print("• GET/POST/PUT/DELETE /users (admin only)") 131 | print("• GET/POST/PUT /products (no delete)") 132 | print("• GET /categories (read-only)") 133 | print("• GET/POST /orders (user)") 134 | print("• GET/POST /order_items (user)") 135 | print("• GET /audit_logs (admin read-only)") 136 | print() 137 | print("Try these example queries:") 138 | print(" curl http://localhost:8000/categories") 139 | print(" curl http://localhost:8000/products") 140 | 141 | # Create database and configuration 142 | db_path = create_advanced_database() 143 | config_path = create_yaml_config(db_path) 144 | 145 | # Create LightAPI instance from YAML configuration 146 | app = LightApi.from_config(config_path) 147 | 148 | _print_usage() 149 | 150 | # Run the server 151 | app.run(host="localhost", port=DEFAULT_PORT, debug=True) --------------------------------------------------------------------------------