From f59a310e383d66caddeb4687a3e183259c1ac026 Mon Sep 17 00:00:00 2001 From: Ezeudoh Tochukwu Date: Sun, 5 Jan 2025 14:32:01 +0100 Subject: [PATCH 1/4] Added more context to authentication docs --- .../authentication/api-key-authentication.md | 235 ++++++++++++++ .../authentication/auth-handler-strategy.md | 2 +- docs/security/authentication/index.md | 70 +++++ .../authentication/jwt-authentication.md | 288 ++++++++++++++++++ ellar/core/router_builders/controller.py | 2 - .../carapp/apps/car/views/car/list.html | 1 + .../carapp/apps/car/views/cat/list.html | 1 + 7 files changed, 596 insertions(+), 3 deletions(-) create mode 100644 docs/security/authentication/api-key-authentication.md create mode 100644 docs/security/authentication/index.md create mode 100644 docs/security/authentication/jwt-authentication.md diff --git a/docs/security/authentication/api-key-authentication.md b/docs/security/authentication/api-key-authentication.md new file mode 100644 index 00000000..89533b47 --- /dev/null +++ b/docs/security/authentication/api-key-authentication.md @@ -0,0 +1,235 @@ +# API Key Authentication in Ellar + +This guide demonstrates how to implement API Key authentication in Ellar using both header-based and query parameter-based approaches. + +## Overview + +API Key authentication is a simple yet effective way to secure your APIs. Ellar provides built-in support for three types of API Key authentication: + +1. Header-based API Key +2. Query Parameter-based API Key +3. Cookie-based API Key + +## Implementation Methods + +### 1. Using Authentication Handler + +```python +# auth/api_key_scheme.py +from ellar.auth import UserIdentity +from ellar.auth.handlers import ( + HeaderAPIKeyAuthenticationHandler, + QueryAPIKeyAuthenticationHandler, + CookieAPIKeyAuthenticationHandler +) +from ellar.common import IHostContext +from ellar.di import injectable + + +@injectable +class HeaderAPIKeyAuth(HeaderAPIKeyAuthenticationHandler): + api_key = "your-secret-api-key" # In production, use secure storage + api_key_name = "X-API-Key" + + async def authentication_handler( + self, + context: IHostContext, + api_key: str, + ) -> UserIdentity | None: + if api_key == self.api_key: + return UserIdentity(auth_type="api_key", api_key=api_key) + return None + + +@injectable +class QueryAPIKeyAuth(QueryAPIKeyAuthenticationHandler): + api_key = "your-secret-api-key" + api_key_name = "api_key" + + async def authentication_handler( + self, + context: IHostContext, + api_key: str, + ) -> UserIdentity | None: + if api_key == self.api_key: + return UserIdentity(auth_type="api_key", api_key=api_key) + return None +``` + +Register the authentication handlers: + +```python +# server.py +from ellar.app import AppFactory, use_authentication_schemes + +application = AppFactory.create_from_app_module( + lazyLoad('project_name.root_module:ApplicationModule'), + config_module="project_name.config:DevelopmentConfig" +) +use_authentication_schemes([HeaderAPIKeyAuth, QueryAPIKeyAuth]) +``` + +### 2. Using Guard Strategy + +```python +# auth/guards.py +from ellar.auth import UserIdentity +from ellar.auth.guards import ( + GuardAPIKeyHeader, + GuardAPIKeyQuery +) +from ellar.di import injectable + + +@injectable +class HeaderAPIKeyGuard(GuardAPIKeyHeader): + api_key = "your-secret-api-key" + api_key_name = "X-API-Key" + + async def authentication_handler( + self, + context: IExecutionContext, + api_key: str, + ) -> UserIdentity | None: + if api_key == self.api_key: + return UserIdentity(auth_type="api_key", api_key=api_key) + self.raise_exception() + + +@injectable +class QueryAPIKeyGuard(GuardAPIKeyQuery): + api_key = "your-secret-api-key" + api_key_name = "api_key" + + async def authentication_handler( + self, + context: IExecutionContext, + api_key: str, + ) -> UserIdentity | None: + if api_key == self.api_key: + return UserIdentity(auth_type="api_key", api_key=api_key) + self.raise_exception() +``` + +## Controller Implementation + +```python +from ellar.common import Controller, get +from ellar.auth import AuthenticationRequired + + +@Controller('/api') +@AuthenticationRequired(['HeaderAPIKeyAuth', 'QueryAPIKeyAuth']) +class APIController: + @get('/data') + async def get_data(self): + return {"message": "Protected data"} +``` + +## Testing the Implementation + +```bash +# Using Header-based API Key +curl http://localhost:8000/api/data \ + -H "X-API-Key: your-secret-api-key" + +# Using Query Parameter-based API Key +curl "http://localhost:8000/api/data?api_key=your-secret-api-key" +``` + +## Security Best Practices + +1. **API Key Storage**: + - Never hardcode API keys in source code + - Use environment variables or secure key management systems + - Rotate API keys periodically + +2. **Transport Security**: + - Always use HTTPS in production + - Consider implementing rate limiting + - Log and monitor API key usage + +3. **Key Management**: + - Implement API key rotation + - Maintain an audit trail of API key usage + - Implement key revocation mechanisms + +## Advanced Implementation + +### API Key with Scopes + +```python +from typing import List +from ellar.auth import UserIdentity +from ellar.auth.handlers import HeaderAPIKeyAuthenticationHandler + + +@injectable +class ScopedAPIKeyAuth(HeaderAPIKeyAuthenticationHandler): + api_keys = { + "key1": ["read"], + "key2": ["read", "write"], + } + api_key_name = "X-API-Key" + + async def authentication_handler( + self, + context: IHostContext, + api_key: str, + ) -> UserIdentity | None: + if api_key in self.api_keys: + return UserIdentity( + auth_type="api_key", + api_key=api_key, + scopes=self.api_keys[api_key] + ) + return None +``` + +## Manual OpenAPI Integration + +Ellar automatically generates OpenAPI documentation when you use and class in `ellar.auth.handlers` and `ellar.auth.guards`. But if you want to manually add it, you can do so by using the `OpenAPIDocumentBuilder` class. + +```python +from ellar.openapi import ApiTags, OpenAPIDocumentBuilder + +@Controller +@ApiTags(name='API', security=[{"ApiKeyAuth": []}]) +class APIController: + pass + +# In your OpenAPI configuration +document_builder = OpenAPIDocumentBuilder() +document_builder.add_security_scheme( + "ApiKeyAuth", + { + "type": "apiKey", + "in": "header", + "name": "X-API-Key" + } +) +``` + +## Custom Error Handling + +```python +from ellar.common import IExecutionContext +from ellar.common.responses import JSONResponse +from ellar.core.exceptions import as_exception_handler + + +@as_exception_handler(401) +def invalid_api_key_exception_handler(ctx: IExecutionContext, exc: Exception) -> JSONResponse: + return JSONResponse( + status_code=401, + content={ + "message": "Invalid API key", + "error": "unauthorized" + } + ) +``` +See [Custom Error Handling](../overview/exception_handling.md) for more information. + +## Complete Examples + +For a complete working example of API Key authentication, visit the [Ellar examples repository](https://github.com/python-ellar/ellar/tree/main/examples). diff --git a/docs/security/authentication/auth-handler-strategy.md b/docs/security/authentication/auth-handler-strategy.md index 52eb203d..90527ec9 100644 --- a/docs/security/authentication/auth-handler-strategy.md +++ b/docs/security/authentication/auth-handler-strategy.md @@ -12,7 +12,7 @@ Just like AuthGuard, we need to create its equivalent. But first we need to crea of your application for us to define a `JWTAuthentication` handler. -```python title='prject_name/auth_scheme.py' linenums='1' +```python title='project_name/auth_scheme.py' linenums='1' import typing as t from ellar.common.serializer.guard import ( HTTPAuthorizationCredentials, diff --git a/docs/security/authentication/index.md b/docs/security/authentication/index.md new file mode 100644 index 00000000..077786e4 --- /dev/null +++ b/docs/security/authentication/index.md @@ -0,0 +1,70 @@ +# Authentication in Ellar + +Authentication is an essential part of most applications. It verifies the identity of users interacting with your application. Ellar provides flexible and powerful authentication mechanisms to suit different application needs. + +## Overview + +Ellar offers two main approaches to authentication: + +1. **Guard Strategy** - Authentication performed at the route handler level +2. **Authentication Handler Strategy** - Authentication performed at the middleware layer + +### When to Use Each Strategy + +- **Guard Strategy**: + - When you need fine-grained control over authentication at the route level + - When authentication logic varies between different routes + - When you want to combine multiple authentication methods + +- **Authentication Handler Strategy**: + - When you want consistent authentication across your application + - When authentication should happen early in the request pipeline + - When you need to integrate with existing authentication middleware + +## Available Authentication Methods + +Ellar supports various authentication methods out of the box: + +- JWT Authentication +- HTTP Basic Authentication +- HTTP Bearer Authentication +- API Key Authentication (Header, Query, Cookie) +- Custom Authentication Schemes + +## Getting Started + +Choose the authentication strategy that best fits your needs: + +- [Guard-based Authentication](./guard-strategy.md) - Learn how to implement authentication using Guards +- [Authentication Handler Strategy](./auth-handler-strategy.md) - Implement authentication at the middleware level +- [JWT Authentication Example](./jwt-authentication.md) - Complete example of JWT authentication +- [API Key Authentication](./api-key-authentication.md) - Implement API key-based authentication + +## Best Practices + +1. **Security First**: + - Never store passwords in plain text + - Use secure token generation and validation + - Implement proper error handling + +2. **Performance**: + - Choose the appropriate authentication layer + - Implement caching where appropriate + - Consider token expiration strategies + +3. **User Experience**: + - Implement proper token refresh mechanisms + - Provide clear error messages + - Consider rate limiting + +4. **Code Organization**: + - Separate authentication logic from business logic + - Use dependency injection for services + - Follow Ellar's module structure + +## Next Steps + +Start with the authentication strategy that best matches your requirements: + +- For route-level authentication control, begin with [Guard Strategy](./guard-strategy.md) +- For application-wide authentication, check out [Authentication Handler Strategy](./auth-handler-strategy.md) diff --git a/docs/security/authentication/jwt-authentication.md b/docs/security/authentication/jwt-authentication.md new file mode 100644 index 00000000..f3d04ecf --- /dev/null +++ b/docs/security/authentication/jwt-authentication.md @@ -0,0 +1,288 @@ +# JWT Authentication in Ellar + +This guide demonstrates how to implement JWT (JSON Web Token) authentication in an Ellar application using both Guard and Authentication Handler strategies. + +## Prerequisites + +- Ellar application setup +- `ellar-jwt` package installed: + ```bash + pip install ellar-jwt + ``` + +## Basic Setup + +First, let's set up the basic structure with user management: + +```python +# user/services.py +from ellar.common import Serializer +from ellar.common.serializer import SerializerFilter +from ellar.di import injectable +from ellar.core.security.hashers import make_password + + +class UserModel(Serializer): + _filter = SerializerFilter(exclude={'password'}) + + user_id: int + username: str + password: str + + +@injectable() +class UsersService: + # For demonstration. In production, use a proper database + users = [ + { + 'user_id': 1, + 'username': 'john', + 'password': make_password('password'), + } + ] + + async def get_user_by_username(self, username: str) -> UserModel | None: + filtered_list = filter(lambda item: item["username"] == username, self.users) + found_user = next(filtered_list, None) + if found_user: + return UserModel(**found_user) +``` + +## Method 1: Using Authentication Handler + +The Authentication Handler approach processes authentication at the middleware layer: + +```python +# auth/auth_scheme.py +import typing as t +from ellar.auth import UserIdentity +from ellar.auth.handlers import HttpBearerAuthenticationHandler +from ellar.common import IHostContext +from ellar.common.serializer.guard import HTTPAuthorizationCredentials +from ellar.di import injectable +from ellar_jwt import JWTService + + +@injectable +class JWTAuthentication(HttpBearerAuthenticationHandler): + def __init__(self, jwt_service: JWTService) -> None: + self.jwt_service = jwt_service + + async def authentication_handler( + self, + context: IHostContext, + credentials: HTTPAuthorizationCredentials, + ) -> t.Optional[t.Any]: + try: + data = await self.jwt_service.decode_async(credentials.credentials) + return UserIdentity(auth_type=self.scheme, **data) + except Exception: + return None +``` + +Register the authentication handler in your application: + +```python +# server.py +from ellar.app import AppFactory, use_authentication_schemes + +application = AppFactory.create_from_app_module( + lazyLoad('project_name.root_module:ApplicationModule'), + config_module="project_name.config:DevelopmentConfig" +) +use_authentication_schemes(JWTAuthentication) +``` + +## Method 2: Using Guard Strategy + +The Guard strategy processes authentication at the route handler level: + +```python +# auth/guards.py +from ellar.auth import UserIdentity +from ellar.auth.guards import GuardHttpBearerAuth +from ellar.di import injectable +from ellar_jwt import JWTService + + +@injectable +class JWTGuard(GuardHttpBearerAuth): + def __init__(self, jwt_service: JWTService) -> None: + self.jwt_service = jwt_service + + async def authentication_handler( + self, + context: IExecutionContext, + credentials: HTTPAuthorizationCredentials, + ) -> t.Optional[t.Any]: + try: + data = await self.jwt_service.decode_async(credentials.credentials) + return UserIdentity(auth_type=self.scheme, **data) + except Exception: + self.raise_exception() +``` + +## Authentication Service + +Implement the authentication service: + +```python +# auth/services.py +from datetime import timedelta +from ellar.di import injectable +from ellar.common import exceptions +from ellar_jwt import JWTService +from ..user.services import UsersService + + +@injectable() +class AuthService: + def __init__(self, users_service: UsersService, jwt_service: JWTService) -> None: + self.users_service = users_service + self.jwt_service = jwt_service + + async def sign_in(self, username: str, password: str) -> dict: + user = await self.users_service.get_user_by_username(username) + if not user or not check_password(password, user.password): + raise exceptions.AuthenticationFailed() + + return { + "access_token": await self.jwt_service.sign_async( + dict(user.serialize(), sub=user.user_id) + ), + "refresh_token": await self.jwt_service.sign_async( + dict(sub=user.username), + lifetime=timedelta(days=30) + ) + } + + async def refresh_token(self, refresh_token: str) -> dict: + try: + payload = await self.jwt_service.decode_async(refresh_token) + user = await self.users_service.get_user_by_username(payload['sub']) + if not user: + raise exceptions.AuthenticationFailed() + + return { + "access_token": await self.jwt_service.sign_async( + dict(user.serialize(), sub=user.user_id) + ) + } + except Exception: + raise exceptions.AuthenticationFailed() +``` + +## Controller Implementation + +```python +# auth/controllers.py +from ellar.common import Controller, post, Body, get +from ellar.auth import SkipAuth, AuthenticationRequired +from ellar.openapi import ApiTags +from .services import AuthService + + +@AuthenticationRequired('JWTAuthentication') +@Controller +@ApiTags(name='Authentication') +class AuthController(ControllerBase): + def __init__(self, auth_service: AuthService) -> None: + self.auth_service = auth_service + + @post("/login") + @SkipAuth() + async def sign_in(self, username: Body[str], password: Body[str]): + return await self.auth_service.sign_in(username=username, password=password) + + @get("/profile") + async def get_profile(self): + return self.context.user + + @post("/refresh") + @SkipAuth() + async def refresh_token(self, payload: str = Body(embed=True)): + return await self.auth_service.refresh_token(payload) +``` + +## Module Configuration + +Configure the authentication module: + +```python +# auth/module.py +from datetime import timedelta +from ellar.common import Module +from ellar.core import ModuleBase +from ellar_jwt import JWTModule +from .controllers import AuthController +from .services import AuthService + + +@Module( + modules=[ + JWTModule.setup( + signing_secret_key="your-secret-key", + lifetime=timedelta(minutes=5) + ), + ], + controllers=[AuthController], + providers=[AuthService], +) +class AuthModule(ModuleBase): + pass +``` + +## Testing the Implementation + +```bash +# Login +curl -X POST http://localhost:8000/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username": "john", "password": "password"}' + +# Access protected route +curl http://localhost:8000/auth/profile \ + -H "Authorization: Bearer " + +# Refresh token +curl -X POST http://localhost:8000/auth/refresh \ + -H "Content-Type: application/json" \ + -d '{"payload": ""}' +``` + +## Security Considerations + +1. **Secret Key**: + - Use a strong, environment-specific secret key + - Never commit secrets to version control + +2. **Token Lifetime**: + - Set appropriate expiration times + - Implement token refresh mechanism + - Consider using sliding sessions + +3. **Error Handling**: + - Provide secure, non-revealing error messages + - Implement proper logging + - Handle token expiration gracefully + +## Best Practices + +1. **Token Storage**: + - Store tokens securely (e.g., HttpOnly cookies for web apps) + - Implement proper token revocation + - Consider token rotation strategies + +2. **Refresh Token Handling**: + - Use longer expiration for refresh tokens + - Implement proper refresh token rotation + - Consider implementing a token blacklist + +3. **API Security**: + - Use HTTPS in production + - Implement rate limiting + - Consider implementing CSRF protection + +## Complete Example + +Find a complete working example in the [Ellar GitHub repository](https://github.com/python-ellar/ellar/tree/main/examples/04-auth-with-handlers). diff --git a/ellar/core/router_builders/controller.py b/ellar/core/router_builders/controller.py index 08e646da..ea5b3da3 100644 --- a/ellar/core/router_builders/controller.py +++ b/ellar/core/router_builders/controller.py @@ -33,8 +33,6 @@ def process_controller_routes(controller: t.Type[ControllerBase]) -> t.List[Base controller, tag=constants.OPERATION_ENDPOINT_KEY ): parameters = item.__dict__.get(constants.ROUTE_OPERATION_PARAMETERS) - if parameters is None: - print("Something is not right") operation: t.Union[ControllerRouteOperation, ControllerWebsocketRouteOperation] if not isinstance(parameters, list): diff --git a/samples/01-carapp/carapp/apps/car/views/car/list.html b/samples/01-carapp/carapp/apps/car/views/car/list.html index fccb3acc..90208f60 100644 --- a/samples/01-carapp/carapp/apps/car/views/car/list.html +++ b/samples/01-carapp/carapp/apps/car/views/car/list.html @@ -2,6 +2,7 @@ + Index.html diff --git a/samples/01-carapp/carapp/apps/car/views/cat/list.html b/samples/01-carapp/carapp/apps/car/views/cat/list.html index fccb3acc..90208f60 100644 --- a/samples/01-carapp/carapp/apps/car/views/cat/list.html +++ b/samples/01-carapp/carapp/apps/car/views/cat/list.html @@ -2,6 +2,7 @@ + Index.html From 7de1142bf9cd2188889f26653ffff0ad9ff1e597 Mon Sep 17 00:00:00 2001 From: Ezeudoh Tochukwu Date: Sun, 5 Jan 2025 14:40:06 +0100 Subject: [PATCH 2/4] ReadMe and index documentation updates --- README.md | 173 +++++++++++++++----------------------------------- docs/index.md | 171 ++++++++++++++++--------------------------------- 2 files changed, 104 insertions(+), 240 deletions(-) diff --git a/README.md b/README.md index 7efca7e3..4989a26c 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ +# Ellar - Modern Python ASGI Framework

- Ellar Logo + Ellar Logo

-

logo by: Azad

- -

Ellar - Python ASGI web framework for building fast, efficient, and scalable RESTful APIs and server-side applications.

![Test](https://github.com/python-ellar/ellar/actions/workflows/test_full.yml/badge.svg) ![Coverage](https://img.shields.io/codecov/c/github/python-ellar/ellar) @@ -11,50 +9,36 @@ [![PyPI version](https://img.shields.io/pypi/v/ellar.svg)](https://pypi.python.org/pypi/ellar) [![PyPI version](https://img.shields.io/pypi/pyversions/ellar.svg)](https://pypi.python.org/pypi/ellar) -## **Introduction** - -Ellar is a lightweight ASGI framework designed to simplify the development of efficient and scalable server-side Python -applications. Whether you're building web services, APIs, or full-fledged web applications, -Ellar offers a high level of abstraction and powerful features to streamline your development process. +## Overview -Ellar allows developers to embrace both Object-Oriented Programming (OOP) and Functional Programming (FP) paradigms. -It is built on top of Starlette, a renowned ASGI toolkit, ensuring robust asynchronous request-handling capabilities. +Ellar is a modern, fast, and lightweight ASGI framework for building scalable web applications and APIs with Python. Built on top of Starlette and inspired by the best practices of frameworks like NestJS, Ellar combines the power of async Python with elegant architecture patterns. -## **Key Features** +## โœจ Key Features -- **Easy to Use**: With an intuitive API, Ellar makes it easy for developers to get started with building fast and scalable Python web applications. -- **Dependency Injection (DI)**: Ellar includes a built-in DI system, enabling easy management of dependencies and reducing coupling between components. -- **Pydantic Integration**: Integrated with Pydantic for seamless data validation, ensuring that input data is always valid. -- **Templating with Jinja2**: Built-in support for Jinja2 templates simplifies the creation of dynamic web pages. -- **OpenAPI Documentation**: Ellar has built-in support for generating OpenAPI documentation and facilitating API documentation generation with Swagger or ReDoc. -- **Controller (MVC) Architecture**: Ellar follows the Model-View-Controller (MVC) pattern, aiding in organizing code and separating concerns. -- **Guards for Authentication and Authorization**: Offers built-in support for guards, making it easy to implement authentication and authorization in applications. -- **Modularity**: Inspired by NestJS, Ellar follows a modular architecture, allowing developers to organize code into reusable modules. -- **Asynchronous Programming**: Leveraging Python's async/await feature, Ellar enables the development of efficient and high-performance applications capable of handling concurrent requests. -- **Type Hints Support**: Built with modern Python type hints for better IDE support and code reliability. -- **WebSocket Support**: Native WebSocket support for real-time bidirectional communication. -- **Database Agnostic**: Freedom to use any database with built-in support for popular ORMs. -- **Testing Utilities**: Comprehensive testing utilities for unit and integration testing. +- ๐Ÿš€ **High Performance**: Built on ASGI standards for maximum performance and scalability +- ๐Ÿ’‰ **Dependency Injection**: Built-in DI system for clean and maintainable code architecture +- ๐Ÿ” **Type Safety**: First-class support for Python type hints and Pydantic validation +- ๐Ÿ“š **OpenAPI Integration**: Automatic Swagger/ReDoc documentation generation +- ๐Ÿ—๏ธ **Modular Architecture**: Organize code into reusable modules inspired by NestJS +- ๐Ÿ” **Built-in Security**: Comprehensive authentication and authorization system +- ๐ŸŽจ **Template Support**: Integrated Jinja2 templating for server-side rendering +- ๐Ÿ”Œ **WebSocket Support**: Real-time bidirectional communication capabilities +- ๐Ÿงช **Testing Utilities**: Comprehensive testing tools for unit and integration tests -## **Installation** +## ๐Ÿš€ Quick Start -You can install Ellar using pip: +### Installation ```bash -# Create and activate virtual environment (recommended) -python -m venv venv -source venv/bin/activate # On Windows use: venv\Scripts\activate - -# Install Ellar pip install ellar ``` -## **Quick Start** +### Basic Example ```python -# main.py from ellar.common import get, Controller, ControllerBase from ellar.app import AppFactory +import uvicorn @Controller() class HomeController(ControllerBase): @@ -64,135 +48,80 @@ class HomeController(ControllerBase): app = AppFactory.create_app(controllers=[HomeController]) -# Run the application if __name__ == "__main__": uvicorn.run("main:app", port=5000, reload=True) ``` -## **Getting Started** +## ๐Ÿ“š Complete Example ```python -# Example code showcasing Ellar usage -# (Please ensure you have properly installed Ellar first) - -import uvicorn -from ellar.common import Body, Controller, ControllerBase, delete, get, post, put, Serializer, Inject -from ellar.app import AppFactory +from ellar.common import Body, Controller, ControllerBase, delete, get, post, put, Serializer from ellar.di import injectable, request_scope -from ellar.openapi import OpenAPIDocumentModule, OpenAPIDocumentBuilder, SwaggerUI +from ellar.app import AppFactory from pydantic import Field -from pathlib import Path -# Define a serializer for creating a car +# Define Data Model class CreateCarSerializer(Serializer): name: str year: int = Field(..., gt=0) model: str -# Define a service class for car operations +# Define Service @injectable(scope=request_scope) class CarService: def __init__(self): - self.detail = 'a service' + self.detail = 'car service' -# Define a controller for car operations +# Define Controller @Controller -class MotoController(ControllerBase): +class CarController(ControllerBase): def __init__(self, service: CarService): self._service = service @post() async def create(self, payload: Body[CreateCarSerializer]): - assert self._service.detail == 'a service' - result = payload.dict() - result.update(message='This action adds a new car') - return result - - @put('/{car_id:str}') - async def update(self, car_id: str, payload: Body[CreateCarSerializer]): result = payload.dict() - result.update(message=f'This action updated #{car_id} car resource') + result.update(message='Car created successfully') return result @get('/{car_id:str}') - async def get_one(self, car_id: str, service: Inject[CarService]): - assert self._service == service - return f"This action returns a #{car_id} car" + async def get_one(self, car_id: str): + return f"Retrieved car #{car_id}" - @delete('/{car_id:str}') - async def delete(self, car_id: str): - return f"This action removes a #{car_id} car" - -# Create the Ellar application app = AppFactory.create_app( - controllers=[MotoController], - providers=[CarService], - base_directory=str(Path(__file__).parent), - config_module=dict(REDIRECT_SLASHES=True), - template_folder='templates' -) - -# Build OpenAPI documentation -document_builder = OpenAPIDocumentBuilder() -document_builder.set_title('Ellar API') \ - .set_version('1.0.2') \ - .set_contact(name='Author', url='https://www.yahoo.com', email='author@gmail.com') \ - .set_license('MIT Licence', url='https://www.google.com') -document = document_builder.build_document(app) - -# Setup OpenAPI documentation module -OpenAPIDocumentModule.setup( - app=app, - docs_ui=SwaggerUI(), - document=document, - guards=[] + controllers=[CarController], + providers=[CarService] ) - -# Run the application -if __name__ == "__main__": - uvicorn.run("main:app", port=5000, reload=True) ``` - -Now we can test our API at [http://127.0.0.1:5000/docs](http://127.0.0.1:5000/docs#/) - -You can also try the [quick-project](https://python-ellar.github.io/ellar/quick-project/) setup to get a good idea of the library. - - -## **Project Documentation Status** -- Authorization: In progress -- Complete documentation available at: https://python-ellar.github.io/ellar/ -- API Reference: https://python-ellar.github.io/ellar/references/ - -## **Dependency Summary** - -Ellar has the following core dependencies: +## ๐Ÿ”ง Requirements - Python >= 3.8 - Starlette >= 0.27.0 - Pydantic >= 2.0 - Injector >= 0.19.0 -- typing-extensions >= 4.5.0 -Optional dependencies: -- jinja2 - For template rendering -- python-multipart - For form data parsing -- itsdangerous - For security features +## ๐Ÿ“– Documentation + +- Complete documentation: [https://python-ellar.github.io/ellar/](https://python-ellar.github.io/ellar/) +- API Reference: [https://python-ellar.github.io/ellar/references/](https://python-ellar.github.io/ellar/references/) + +## ๐Ÿค Contributing + +We welcome contributions! Here's how you can help: + +- Create an issue for bugs or feature requests +- Submit pull requests for improvements +- Create third-party modules +- Share your experience with Ellar +- Build and showcase your applications -## **Contributing** -Contributions are Welcome! You can contribute in the following ways. +See [CONTRIBUTING.md](https://github.com/python-ellar/ellar/blob/main/docs/contribution.md) for detailed guidelines. -- **Create an Issue** - Propose a new feature. Report a bug. -- **Pull Request** - Fix a bug and typo. Refactor the code. -- **Create third-party module** - Just like ellar-throttling, ellar-jwt, ellar-sql that can solve common problem in web application development. -- **Share** - Share your thoughts on the a Blog, Twitter, and others. -- **Build your application** - Please try to use Ellar. For more details, see [docs/CONTRIBUTING.md](https://github.com/python-ellar/ellar/blob/main/docs/contribution.md). +## ๐Ÿ“ License -## **Contributors** -Thanks to all contributors! +Ellar is [MIT Licensed](LICENSE). -## **Author** -Ezeudoh Tochukwu https://github.com/eadwinCode +## ๐Ÿ‘ค Author -## **License** -Ellar is [MIT License](https://github.com/python-ellar/ellar/blob/main/LICENSE). +Ezeudoh Tochukwu [@eadwinCode](https://github.com/eadwinCode) diff --git a/docs/index.md b/docs/index.md index 8e96875c..821eff00 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,45 +1,35 @@ -# **Ellar - ASGI Python Framework** +# Ellar - Modern Python ASGI Framework +

- Ellar Logo + Ellar Logo

logo by: Azad

-

Ellar - Python ASGI web framework for building fast, efficient, and scalable RESTful APIs and server-side applications.

- ![Test](https://github.com/python-ellar/ellar/actions/workflows/test_full.yml/badge.svg) ![Coverage](https://img.shields.io/codecov/c/github/python-ellar/ellar) [![PyPI version](https://badge.fury.io/py/ellar.svg)](https://badge.fury.io/py/ellar) [![PyPI version](https://img.shields.io/pypi/v/ellar.svg)](https://pypi.python.org/pypi/ellar) [![PyPI version](https://img.shields.io/pypi/pyversions/ellar.svg)](https://pypi.python.org/pypi/ellar) -## **Introduction** - -Ellar is a lightweight ASGI framework designed to simplify the development of efficient and scalable server-side Python -applications. Whether you're building web services, APIs, or full-fledged web applications, -Ellar offers a high level of abstraction and powerful features to streamline your development process. +## Overview -Ellar allows developers to embrace both Object-Oriented Programming (OOP) and Functional Programming (FP) paradigms. -It is built on top of Starlette, a renowned ASGI toolkit, ensuring robust asynchronous request-handling capabilities. +Ellar is a modern, fast, and lightweight ASGI framework for building scalable web applications and APIs with Python. Built on top of Starlette and inspired by the best practices of frameworks like NestJS, Ellar combines the power of async Python with elegant architecture patterns. -## **Key Features** +## โœจ Key Features -- **Easy to Use**: With an intuitive API, Ellar makes it easy for developers to get started with building fast and scalable Python web applications. -- **Dependency Injection (DI)**: Ellar includes a built-in DI system, enabling easy management of dependencies and reducing coupling between components. -- **Pydantic Integration**: Integrated with Pydantic for seamless data validation, ensuring that input data is always valid. -- **Templating with Jinja2**: Built-in support for Jinja2 templates simplifies the creation of dynamic web pages. -- **OpenAPI Documentation**: Ellar has built-in support for generating OpenAPI documentation and facilitating API documentation generation with Swagger or ReDoc. -- **Controller (MVC) Architecture**: Ellar follows the Model-View-Controller (MVC) pattern, aiding in organizing code and separating concerns. -- **Guards for Authentication and Authorization**: Offers built-in support for guards, making it easy to implement authentication and authorization in applications. -- **Modularity**: Inspired by NestJS, Ellar follows a modular architecture, allowing developers to organize code into reusable modules. -- **Asynchronous Programming**: Leveraging Python's async/await feature, Ellar enables the development of efficient and high-performance applications capable of handling concurrent requests. -- **Type Hints Support**: Built with modern Python type hints for better IDE support and code reliability. -- **WebSocket Support**: Native WebSocket support for real-time bidirectional communication. -- **Database Agnostic**: Freedom to use any database with built-in support for popular ORMs. -- **Testing Utilities**: Comprehensive testing utilities for unit and integration testing. +- ๐Ÿš€ **High Performance**: Built on ASGI standards for maximum performance and scalability +- ๐Ÿ’‰ **Dependency Injection**: Built-in DI system for clean and maintainable code architecture +- ๐Ÿ” **Type Safety**: First-class support for Python type hints and Pydantic validation +- ๐Ÿ“š **OpenAPI Integration**: Automatic Swagger/ReDoc documentation generation +- ๐Ÿ—๏ธ **Modular Architecture**: Organize code into reusable modules inspired by NestJS +- ๐Ÿ” **Built-in Security**: Comprehensive authentication and authorization system +- ๐ŸŽจ **Template Support**: Integrated Jinja2 templating for server-side rendering +- ๐Ÿ”Œ **WebSocket Support**: Real-time bidirectional communication capabilities +- ๐Ÿงช **Testing Utilities**: Comprehensive testing tools for unit and integration tests -## **Installation** +## ๐Ÿš€ Getting Started -You can install Ellar using pip: +### Installation ```bash # Create and activate virtual environment (recommended) @@ -50,12 +40,12 @@ source venv/bin/activate # On Windows use: venv\Scripts\activate pip install ellar ``` -## **Quick Start** +### Basic Example ```python -# main.py from ellar.common import get, Controller, ControllerBase from ellar.app import AppFactory +import uvicorn @Controller() class HomeController(ControllerBase): @@ -65,145 +55,90 @@ class HomeController(ControllerBase): app = AppFactory.create_app(controllers=[HomeController]) -# Run the application if __name__ == "__main__": uvicorn.run("main:app", port=5000, reload=True) ``` -## **Getting Started** +## ๐Ÿ“š Complete Example ```python -# Example code showcasing Ellar usage -# (Please ensure you have properly installed Ellar first) - -import uvicorn -from ellar.common import Body, Controller, ControllerBase, delete, get, post, put, Serializer, Inject -from ellar.app import AppFactory +from ellar.common import Body, Controller, ControllerBase, delete, get, post, put, Serializer from ellar.di import injectable, request_scope -from ellar.openapi import OpenAPIDocumentModule, OpenAPIDocumentBuilder, SwaggerUI +from ellar.app import AppFactory from pydantic import Field -from pathlib import Path -# Define a serializer for creating a car +# Define Data Model class CreateCarSerializer(Serializer): name: str year: int = Field(..., gt=0) model: str -# Define a service class for car operations +# Define Service @injectable(scope=request_scope) class CarService: def __init__(self): - self.detail = 'a service' + self.detail = 'car service' -# Define a controller for car operations +# Define Controller @Controller -class MotoController(ControllerBase): +class CarController(ControllerBase): def __init__(self, service: CarService): self._service = service @post() async def create(self, payload: Body[CreateCarSerializer]): - assert self._service.detail == 'a service' - result = payload.dict() - result.update(message='This action adds a new car') - return result - - @put('/{car_id:str}') - async def update(self, car_id: str, payload: Body[CreateCarSerializer]): result = payload.dict() - result.update(message=f'This action updated #{car_id} car resource') + result.update(message='Car created successfully') return result @get('/{car_id:str}') - async def get_one(self, car_id: str, service: Inject[CarService]): - assert self._service == service - return f"This action returns a #{car_id} car" + async def get_one(self, car_id: str): + return f"Retrieved car #{car_id}" - @delete('/{car_id:str}') - async def delete(self, car_id: str): - return f"This action removes a #{car_id} car" - -# Create the Ellar application app = AppFactory.create_app( - controllers=[MotoController], - providers=[CarService], - base_directory=str(Path(__file__).parent), - config_module=dict(REDIRECT_SLASHES=True), - template_folder='templates' -) - -# Build OpenAPI documentation -document_builder = OpenAPIDocumentBuilder() -document_builder.set_title('Ellar API') \ - .set_version('1.0.2') \ - .set_contact(name='Author', url='https://www.yahoo.com', email='author@gmail.com') \ - .set_license('MIT Licence', url='https://www.google.com') -document = document_builder.build_document(app) - -# Setup OpenAPI documentation module -OpenAPIDocumentModule.setup( - app=app, - docs_ui=SwaggerUI(), - document=document, - guards=[] + controllers=[CarController], + providers=[CarService] ) - -# Run the application -if __name__ == "__main__": - uvicorn.run("main:app", port=5000, reload=True) ``` -You can access the Ellar API documentation at [http://127.0.0.1:5000/docs](http://127.0.0.1:5000/docs#/). Additionally, you can try the [quick-project setup](quick-project.md) to get started quickly with Ellar. - - -## **Project Documentation Status** -- Authorization: In progress -- Complete documentation available at: https://python-ellar.github.io/ellar/ -- API Reference: https://python-ellar.github.io/ellar/references/ - -## **Dependency Summary** - -Ellar has the following core dependencies: +## ๐Ÿ”ง Requirements +### Core Dependencies - Python >= 3.8 - Starlette >= 0.27.0 - Pydantic >= 2.0 - Injector >= 0.19.0 - typing-extensions >= 4.5.0 -Optional dependencies: +### Optional Dependencies - jinja2 - For template rendering - python-multipart - For form data parsing - itsdangerous - For security features -## **Contributing** -Contributions are Welcome! You can contribute in the following ways. +## ๐Ÿ“– Documentation Structure -- **Create an Issue** - Propose a new feature. Report a bug. -- **Pull Request** - Fix a bug and typo. Refactor the code. -- **Create third-party module** - Just like ellar-throttling, ellar-jwt, ellar-sql that can solve common problem in web application development. -- **Share** - Share your thoughts on the a Blog, Twitter, and others. -- **Build your application** - Please try to use Ellar. For more details, see [docs/CONTRIBUTING.md](https://github.com/python-ellar/ellar/blob/main/docs/contribution.md). +- [Quick Start Guide](quick-project.md) +- [Basic Concepts](basics/index.md) +- [Security & Authentication](security/authentication/index.md) +- [OpenAPI Integration](openapi/index.md) +- [API Reference](references/index.md) -## **Contributors** -Thanks to all contributors! +## ๐Ÿค Contributing -## **Author** -Ezeudoh Tochukwu https://github.com/eadwinCode +We welcome contributions! Here's how you can help: -## **License** -Ellar is [MIT License](https://github.com/python-ellar/ellar/blob/main/LICENSE). +- Create an issue for bugs or feature requests +- Submit pull requests for improvements +- Create third-party modules +- Share your experience with Ellar +- Build and showcase your applications +See [CONTRIBUTING.md](https://github.com/python-ellar/ellar/blob/main/docs/contribution.md) for detailed guidelines. -## **Project Documentation Status** -- Authorization: In progress +## ๐Ÿ“ License -## **Dependency Summary** +Ellar is [MIT Licensed](https://github.com/python-ellar/ellar/blob/main/LICENSE). -Ellar has the following dependencies: +## ๐Ÿ‘ค Author -- Python >= 3.7 -- Starlette -- Pydantic -- Injector +Ezeudoh Tochukwu [@eadwinCode](https://github.com/eadwinCode) From 105a5ea01abb0c5a4be8055720be0b90f9cdc0a1 Mon Sep 17 00:00:00 2001 From: Ezeudoh Tochukwu Date: Sun, 5 Jan 2025 14:42:55 +0100 Subject: [PATCH 3/4] fixed failing test --- samples/01-carapp/carapp/apps/car/tests/test_routers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/samples/01-carapp/carapp/apps/car/tests/test_routers.py b/samples/01-carapp/carapp/apps/car/tests/test_routers.py index 044844aa..3bd1ec3c 100644 --- a/samples/01-carapp/carapp/apps/car/tests/test_routers.py +++ b/samples/01-carapp/carapp/apps/car/tests/test_routers.py @@ -45,6 +45,7 @@ def test_get_cars(self, mock_get_all): def test_get_car_html_with_render(self, mock_get_all): res = self.client.get("/car-as-router/html/outside") assert res.status_code == 200 + print(res.content) assert ( res.content == b'\n\n\n \n Index.html\n\n\n \n

Mercedes

\n \n\n' From c73559ce7caac46d1d41498abcc9aacf129470ba Mon Sep 17 00:00:00 2001 From: Ezeudoh Tochukwu Date: Sun, 5 Jan 2025 14:45:10 +0100 Subject: [PATCH 4/4] fixed failing test --- samples/01-carapp/carapp/apps/car/tests/test_routers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/samples/01-carapp/carapp/apps/car/tests/test_routers.py b/samples/01-carapp/carapp/apps/car/tests/test_routers.py index 3bd1ec3c..8f95f240 100644 --- a/samples/01-carapp/carapp/apps/car/tests/test_routers.py +++ b/samples/01-carapp/carapp/apps/car/tests/test_routers.py @@ -45,9 +45,8 @@ def test_get_cars(self, mock_get_all): def test_get_car_html_with_render(self, mock_get_all): res = self.client.get("/car-as-router/html/outside") assert res.status_code == 200 - print(res.content) assert ( res.content - == b'\n\n\n \n Index.html\n\n\n \n

Mercedes

\n \n\n' + == b'\n\n\n \n \n Index.html\n\n\n \n

Mercedes

\n \n\n' ) assert res.template.name == "car/list.html"