Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test_full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: 3.9
python-version: '3.13'
- name: Install Flit
run: pip install flit
- name: Install Dependencies
Expand Down
88 changes: 88 additions & 0 deletions docs/techniques/configurations.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,91 @@ application = AppFactory.create_from_app_module(
)
)
```

## **Complete Configuration Example**

Below is a complete configuration example showing all available configuration options with their default values:

```python
import typing as t

from ellar.common import IExceptionHandler, JSONResponse
from ellar.core import ConfigDefaultTypesMixin
from ellar.core.versioning import BaseAPIVersioning, DefaultAPIVersioning
from ellar.pydantic import ENCODERS_BY_TYPE as encoders_by_type
from starlette.middleware import Middleware
from starlette.requests import Request


class BaseConfig(ConfigDefaultTypesMixin):
DEBUG: bool = False

DEFAULT_JSON_CLASS: t.Type[JSONResponse] = JSONResponse
SECRET_KEY: str = "your-secret-key-here"

# injector auto_bind = True allows you to resolve types that are not registered on the container
# For more info, read: https://injector.readthedocs.io/en/latest/index.html
INJECTOR_AUTO_BIND = False

# jinja Environment options
# https://jinja.palletsprojects.com/en/3.0.x/api/#high-level-api
JINJA_TEMPLATES_OPTIONS: t.Dict[str, t.Any] = {}

# Injects context to jinja templating context values
TEMPLATES_CONTEXT_PROCESSORS: t.List[
t.Union[str, t.Callable[[t.Union[Request]], t.Dict[str, t.Any]]]
] = [
"ellar.core.templating.context_processors:request_context",
"ellar.core.templating.context_processors:user",
"ellar.core.templating.context_processors:request_state",
]

# Application route versioning scheme
VERSIONING_SCHEME: BaseAPIVersioning = DefaultAPIVersioning()

# Enable or Disable Application Router route searching by appending backslash
REDIRECT_SLASHES: bool = False

# Define references to static folders in python packages.
# eg STATIC_FOLDER_PACKAGES = [('boostrap4', 'statics')]
STATIC_FOLDER_PACKAGES: t.Optional[t.List[t.Union[str, t.Tuple[str, str]]]] = []

# Define references to static folders defined within the project
STATIC_DIRECTORIES: t.Optional[t.List[t.Union[str, t.Any]]] = []

# static route path
STATIC_MOUNT_PATH: str = "/static"

CORS_ALLOW_ORIGINS: t.List[str] = ["*"]
CORS_ALLOW_METHODS: t.List[str] = ["*"]
CORS_ALLOW_HEADERS: t.List[str] = ["*"]
ALLOWED_HOSTS: t.List[str] = ["*"]

# Application middlewares
MIDDLEWARE: t.List[t.Union[str, Middleware]] = [
"ellar.core.middleware.trusted_host:trusted_host_middleware",
"ellar.core.middleware.cors:cors_middleware",
"ellar.core.middleware.errors:server_error_middleware",
"ellar.core.middleware.versioning:versioning_middleware",
"ellar.auth.middleware.session:session_middleware",
"ellar.auth.middleware.auth:identity_middleware",
"ellar.core.middleware.exceptions:exception_middleware",
]

# A dictionary mapping either integer status codes,
# or exception class types onto callables which handle the exceptions.
# Exception handler callables should be of the form
# `handler(context:IExecutionContext, exc: Exception) -> response`
# and may be either standard functions, or async functions.
EXCEPTION_HANDLERS: t.List[t.Union[str, IExceptionHandler]] = [
"ellar.core.exceptions:error_404_handler"
]

# Object Serializer custom encoders
SERIALIZER_CUSTOM_ENCODER: t.Dict[t.Any, t.Callable[[t.Any], t.Any]] = (
encoders_by_type
)
```

!!! tip
You can copy this configuration as a starting point and modify only the values you need to change for your application.
2 changes: 1 addition & 1 deletion ellar/core/conf/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class Config(ConfigDefaultTypesMixin):

def __init__(
self,
config_module: t.Optional[str] = None,
config_module: t.Optional[t.Union[str, dict]] = None,
config_prefix: t.Optional[str] = None,
**mapping: t.Any,
):
Expand Down
2 changes: 1 addition & 1 deletion ellar/core/routing/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def matches(self, scope: TScope) -> t.Tuple[Match, TScope]:
if match[0] is Match.FULL:
version_scheme_resolver: "BaseAPIVersioningResolver" = t.cast(
"BaseAPIVersioningResolver",
scope[constants.SCOPE_API_VERSIONING_RESOLVER],
scope.get(constants.SCOPE_API_VERSIONING_RESOLVER),
)
if not version_scheme_resolver.can_activate(
route_versions=self.allowed_version
Expand Down
2 changes: 1 addition & 1 deletion ellar/core/routing/mount.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def router_default_decorator(func: ASGIApp) -> ASGIApp:
@functools.wraps(func)
async def _wrap(scope: TScope, receive: TReceive, send: TSend) -> None:
version_scheme_resolver: "BaseAPIVersioningResolver" = t.cast(
"BaseAPIVersioningResolver", scope[SCOPE_API_VERSIONING_RESOLVER]
"BaseAPIVersioningResolver", scope.get(SCOPE_API_VERSIONING_RESOLVER)
)
if version_scheme_resolver and version_scheme_resolver.matched_any_route:
version_scheme_resolver.raise_exception()
Expand Down
42 changes: 42 additions & 0 deletions ellar/socket_io/testing/module.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import typing as t
from contextlib import asynccontextmanager
from pathlib import Path

import socketio
from ellar.testing.module import Test, TestingModule
from ellar.testing.uvicorn_server import EllarUvicornServer
from starlette.routing import Host, Mount

if t.TYPE_CHECKING: # pragma: no cover
from ellar.common import ControllerBase, GuardCanActivate, ModuleRouter
from ellar.core import ModuleBase
from ellar.core.routing import EllarControllerMount
from ellar.di import ProviderConfig


class RunWithServerContext:
Expand Down Expand Up @@ -62,3 +70,37 @@ async def run_with_server(

class TestGateway(Test):
TESTING_MODULE = SocketIOTestingModule

@classmethod
def create_test_module(
cls,
controllers: t.Sequence[t.Union[t.Type["ControllerBase"], t.Type]] = (),
routers: t.Sequence[
t.Union["ModuleRouter", "EllarControllerMount", Mount, Host, t.Callable]
] = (),
providers: t.Sequence[t.Union[t.Type, "ProviderConfig"]] = (),
template_folder: t.Optional[str] = "templates",
base_directory: t.Optional[t.Union[Path, str]] = None,
static_folder: str = "static",
modules: t.Sequence[t.Union[t.Type, t.Any]] = (),
application_module: t.Optional[t.Union[t.Type["ModuleBase"], str]] = None,
global_guards: t.Optional[
t.List[t.Union[t.Type["GuardCanActivate"], "GuardCanActivate"]]
] = None,
config_module: t.Optional[t.Union[str, t.Dict]] = None,
) -> SocketIOTestingModule:
return t.cast(
SocketIOTestingModule,
super().create_test_module(
controllers=controllers,
routers=routers,
providers=providers,
template_folder=template_folder,
base_directory=base_directory,
static_folder=static_folder,
modules=modules,
application_module=application_module,
global_guards=global_guards,
config_module=config_module,
),
)
6 changes: 3 additions & 3 deletions requirements-tests.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
aiohttp == 3.10.5
aiohttp == 3.13.2
anyio[trio] >= 3.2.1
argon2-cffi == 25.1.0
autoflake
Expand All @@ -12,7 +12,7 @@ pytest >= 6.2.4,< 9.0.0
pytest-asyncio
pytest-cov >= 2.12.0,< 8.0.0
python-multipart >= 0.0.5
python-socketio
python-socketio==5.16.0
regex==2025.9.18
ruff ==0.14.7
types-dataclasses ==0.6.6
Expand All @@ -21,4 +21,4 @@ types-redis ==4.6.0.20241004
# types
types-ujson ==5.10.0.20250822
ujson >= 4.0.1
uvicorn[standard] == 0.39.0
uvicorn[standard] >= 0.39.0
16 changes: 16 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import contextlib
import functools
import socket
from pathlib import PurePath, PurePosixPath, PureWindowsPath
from uuid import uuid4

Expand Down Expand Up @@ -52,3 +54,17 @@ def reflect_context():
async def async_reflect_context():
async with reflect.async_context():
yield


def _unused_port(socket_type: int) -> int:
"""Find an unused localhost port from 1024-65535 and return it."""
with contextlib.closing(socket.socket(type=socket_type)) as sock:
sock.bind(("127.0.0.1", 0))
return sock.getsockname()[1]


# This was copied from pytest-asyncio.
# Ref.: https://github.com/pytest-dev/pytest-asyncio/blob/25d9592286682bc6dbfbf291028ff7a9594cf283/pytest_asyncio/plugin.py#L525-L527
@pytest.fixture
def unused_tcp_port() -> int:
return _unused_port(socket.SOCK_STREAM)
38 changes: 20 additions & 18 deletions tests/test_socket_io/test_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@
class TestEventGateway:
test_client = TestGateway.create_test_module(controllers=[EventGateway])

async def test_socket_connection_work(self):
async def test_socket_connection_work(self, unused_tcp_port):
my_response_message = []
connected_called = False
disconnected_called = False

async with self.test_client.run_with_server() as ctx:
async with self.test_client.run_with_server(port=unused_tcp_port) as ctx:

@ctx.sio.event
async def my_response(message):
Expand All @@ -52,11 +52,11 @@ async def connect(*args):
]
assert disconnected_called and connected_called

async def test_broadcast_work(self):
async def test_broadcast_work(self, unused_tcp_port):
sio_1_response_message = []
sio_2_response_message = []

async with self.test_client.run_with_server() as ctx:
async with self.test_client.run_with_server(port=unused_tcp_port) as ctx:
ctx_2 = ctx.new_socket_client_context()

@ctx.sio.on("my_response")
Expand Down Expand Up @@ -94,10 +94,10 @@ async def my_response_case_2(message):
class TestGatewayWithGuards:
test_client = TestGateway.create_test_module(controllers=[GatewayWithGuards])

async def test_socket_connection_work(self):
async def test_socket_connection_work(self, unused_tcp_port):
my_response_message = []

async with self.test_client.run_with_server() as ctx:
async with self.test_client.run_with_server(port=unused_tcp_port) as ctx:

@ctx.sio.event
async def my_response(message):
Expand All @@ -113,10 +113,10 @@ async def my_response(message):
{"auth-key": "supersecret", "data": "Testing Broadcast"}
]

async def test_event_with_header_work(self):
async def test_event_with_header_work(self, unused_tcp_port):
my_response_message = []

async with self.test_client.run_with_server() as ctx:
async with self.test_client.run_with_server(port=unused_tcp_port) as ctx:

@ctx.sio.event
async def my_response(message):
Expand All @@ -132,10 +132,10 @@ async def my_response(message):
{"data": "Testing Broadcast", "x_auth_key": "supersecret"}
]

async def test_event_with_plain_response(self):
async def test_event_with_plain_response(self, unused_tcp_port):
my_response_message = []

async with self.test_client.run_with_server() as ctx:
async with self.test_client.run_with_server(port=unused_tcp_port) as ctx:

@ctx.sio.on("my_plain_response")
async def message_receive(message):
Expand All @@ -151,10 +151,10 @@ async def message_receive(message):
{"data": "Testing Broadcast", "x_auth_key": "supersecret"}
]

async def test_failed_to_connect(self):
async def test_failed_to_connect(self, unused_tcp_port):
my_response_message = []

async with self.test_client.run_with_server() as ctx:
async with self.test_client.run_with_server(port=unused_tcp_port) as ctx:
ctx = typing.cast(RunWithServerContext, ctx)

@ctx.sio.on("error")
Expand All @@ -169,10 +169,10 @@ async def error(message):

assert my_response_message == [{"code": 1011, "reason": "Authorization Failed"}]

async def test_failed_process_message_sent(self):
async def test_failed_process_message_sent(self, unused_tcp_port):
my_response_message = []

async with self.test_client.run_with_server() as ctx:
async with self.test_client.run_with_server(port=unused_tcp_port) as ctx:
ctx = typing.cast(RunWithServerContext, ctx)

@ctx.sio.on("error")
Expand Down Expand Up @@ -224,13 +224,15 @@ class TestGatewayExceptions:
),
],
)
async def test_exception_handling_works_debug_true_or_false(self, debug, result):
async def test_exception_handling_works_debug_true_or_false(
self, debug, result, unused_tcp_port
):
test_client = TestGateway.create_test_module(
controllers=[GatewayOthers], config_module={"DEBUG": debug}
)
my_response_message = []

async with test_client.run_with_server() as ctx:
async with test_client.run_with_server(port=unused_tcp_port) as ctx:
ctx = typing.cast(RunWithServerContext, ctx)
ctx2 = ctx.new_socket_client_context()

Expand All @@ -253,11 +255,11 @@ async def error_2(message):

assert my_response_message == result

async def test_message_with_extra_args(self):
async def test_message_with_extra_args(self, unused_tcp_port):
test_client = TestGateway.create_test_module(controllers=[GatewayOthers])
my_response_message = []

async with test_client.run_with_server() as ctx:
async with test_client.run_with_server(port=unused_tcp_port) as ctx:
ctx = typing.cast(RunWithServerContext, ctx)

@ctx.sio.on("error")
Expand Down