From aecf96055e87b50c0c119778c0ac8580370d3121 Mon Sep 17 00:00:00 2001 From: FITAHIANA Nomeniavo Joe <24nomeniavo@gmail.com> Date: Sun, 26 Jul 2026 12:39:11 +0300 Subject: [PATCH 1/6] chore: remove catcher --- Cargo.lock | 2 +- src/catcher.rs | 83 ------------------------------------------------- src/lib.rs | 35 +++++++++------------ src/request.rs | 4 +-- src/response.rs | 8 ++--- 5 files changed, 20 insertions(+), 112 deletions(-) delete mode 100644 src/catcher.rs diff --git a/Cargo.lock b/Cargo.lock index 7dd3bea..0f5b130 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1485,7 +1485,7 @@ checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" [[package]] name = "oxapy" -version = "0.8.8" +version = "0.9.1" dependencies = [ "ahash", "ctrlc", diff --git a/src/catcher.rs b/src/catcher.rs deleted file mode 100644 index 6681257..0000000 --- a/src/catcher.rs +++ /dev/null @@ -1,83 +0,0 @@ -use pyo3::prelude::*; -use pyo3_stub_gen::derive::*; - -use crate::Status; - -/// A catcher for handling specific HTTP status codes. -/// -/// Catchers allow you to provide custom responses for specific HTTP status codes. -/// They are typically created using the `catcher` decorator function. -/// -/// Args: -/// status (Status): The HTTP status code this catcher will handle. -/// handler (callable): The handler function that will be called when this status occurs. -/// -/// Example: -/// ```python -/// from oxapy import catcher, Status -/// -/// @catcher(Status.NOT_FOUND) -/// def handle_not_found(request, response): -/// return Response("

Custom 404 Page

", content_type="text/html") -/// ``` -#[gen_stub_pyclass] -#[pyclass] -pub struct Catcher { - pub status: Status, - pub handler: Py, -} - -/// Internal builder class for creating catchers. -/// -/// This class is returned by the `catcher` function and is used to create -/// a Catcher when called with a handler function. -#[gen_stub_pyclass] -#[pyclass] -pub struct CatcherBuilder { - status: Status, -} - -#[gen_stub_pymethods] -#[pymethods] -impl CatcherBuilder { - /// Create a Catcher when called with a handler function. - /// - /// Args: - /// handler (callable): The handler function to call when the status occurs. - /// - /// Returns: - /// Catcher: A new catcher for the specified status. - fn __call__(&self, handler: Py) -> Catcher { - Catcher { - status: self.status, - handler, - } - } -} - -/// Decorator for creating status code catchers. -/// -/// A catcher allows you to provide custom responses for specific HTTP status codes. -/// -/// Args: -/// status (Status): The HTTP status code to catch. -/// -/// Returns: -/// CatcherBuilder: A builder that creates a Catcher when called with a handler function. -/// -/// Example: -/// ```python -/// from oxapy import catcher, Status, Response -/// -/// @catcher(Status.NOT_FOUND) -/// def handle_404(request, response): -/// return Response("

Page Not Found

", content_type="text/html") -/// -/// # Add the catcher to your server -/// app.catchers([handle_404]) -/// ``` -#[gen_stub_pyfunction] -#[pyfunction] -pub fn catcher(status: Status) -> CatcherBuilder { - CatcherBuilder { status } -} diff --git a/src/lib.rs b/src/lib.rs index 123ca34..1c08feb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,6 @@ use std::ops::Deref; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; -use ahash::HashMap; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pyo3::types::{PyBytes, PyDict, PyInt, PyString}; @@ -15,7 +14,6 @@ use tokio::net::{TcpListener, TcpStream}; use tokio::sync::Semaphore; use tokio::sync::mpsc::{Receiver, Sender, channel}; -use catcher::Catcher; use cors::Cors; use exceptions::IntoPyException; use into_response::convert_to_response; @@ -27,7 +25,6 @@ use routing::*; use status::Status; use templating::Template; -mod catcher; mod cors; #[macro_use] mod exceptions; @@ -47,8 +44,8 @@ pyo3_stub_gen::export_verbatim!("oxapy", "from typing_extensions import Self"); pyo3_stub_gen::define_stub_info_gatherer!(stub_info); struct ProcessRequest { - catchers: Option>>>, cors: Option>, + wrapper: Option>>, router: Option>, match_route: Option>, request: Arc, @@ -58,7 +55,7 @@ struct ProcessRequest { #[derive(Clone)] struct RequestContext { app_data: Option>>, - catchers: Option>>>, + wrapper: Option>>, channel_capacity: usize, cors: Option>, routers: Vec>, @@ -117,7 +114,7 @@ struct RequestContext { struct HttpServer { addr: SocketAddr, app_data: Option>>, - catchers: Option>>>, + wrapper: Option>>, channel_capacity: usize, cors: Option>, is_async: bool, @@ -176,7 +173,7 @@ impl HttpServer { Ok(Self { addr: SocketAddr::new(ip.parse()?, port), app_data: None, - catchers: None, + wrapper: None, channel_capacity: 100, cors: None, is_async: false, @@ -347,22 +344,19 @@ impl HttpServer { /// /// Example: /// ```python - /// @catcher(Status.NOT_FOUND) - /// def not_found(request, response): - /// return Response("

Page Not Found

", content_type="text/html") + /// def global_middleware(request, response): + /// if response.status.code == 200: + /// return Response("

Page Not Found

", content_type="text/html") + /// return response /// - /// server.catchers([not_found]) + /// server.wrap(global_middleware) /// ``` - fn catchers<'py>( + fn wrap<'py>( mut slf: PyRefMut<'py, Self>, - catchers: Vec>, + wrapper: Py, py: Python<'py>, ) -> PyRefMut<'py, Self> { - let map = catchers - .into_iter() - .map(|c| (c.status, c.handler.clone_ref(py))) - .collect(); - slf.catchers = Some(Arc::new(map)); + slf.wrapper = Some(Arc::new(wrapper)); slf } @@ -455,7 +449,7 @@ impl HttpServer { let (tx, rx) = channel::(self.channel_capacity); let ctx = RequestContext { app_data: self.app_data.clone(), - catchers: self.catchers.clone(), + wrapper: self.wrapper.clone(), channel_capacity: self.channel_capacity, cors: self.cors.clone(), routers: self.routers.clone(), @@ -530,7 +524,7 @@ impl HttpServer { call_python_handler(&req.router, &req.match_route, &req.request, self.is_async) .await .unwrap_or_else(Response::from) - .apply_catcher(&req) + .call_wrapper(&req) .apply_cors(&req.cors)?; let _ = req.tx.send(response).await; Ok(()) @@ -669,7 +663,6 @@ fn oxapy(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_function(wrap_pyfunction!(catcher::catcher, m)?)?; m.add_function(wrap_pyfunction!(convert_to_response, m)?)?; m.add_function(wrap_pyfunction!(delete, m)?)?; m.add_function(wrap_pyfunction!(get, m)?)?; diff --git a/src/request.rs b/src/request.rs index 7bcb691..f583edd 100644 --- a/src/request.rs +++ b/src/request.rs @@ -285,7 +285,7 @@ impl Request { match_route: Some(transmutate_route), tx, cors: ctx.cors.clone(), - catchers: ctx.catchers.clone(), + wrapper: ctx.wrapper.clone(), }; Self::send_and_wait_response(ctx, process_request, rx).await @@ -303,7 +303,7 @@ impl Request { match_route: None, tx, cors: ctx.cors.clone(), - catchers: ctx.catchers.clone(), + wrapper: ctx.wrapper.clone(), }; Self::send_and_wait_response(ctx, process_request, rx).await diff --git a/src/response.rs b/src/response.rs index e70e411..95b4d45 100644 --- a/src/response.rs +++ b/src/response.rs @@ -231,13 +231,11 @@ impl Response { }) } - pub(crate) fn apply_catcher(mut self, req: &ProcessRequest) -> Self { - if let Some(catchers) = &req.catchers - && let Some(handler) = catchers.get(&self.status) - { + pub(crate) fn call_wrapper(mut self, req: &ProcessRequest) -> Self { + if let Some(wrapper) = &req.wrapper { let request = req.request.as_ref().clone(); self = Python::attach(|py| { - let result = handler.call(py, (request, self), None)?; + let result = wrapper.call(py, (request, self), None)?; convert_to_response(result, py) }) .unwrap_or_else(Response::from); From 37d92bb5fb3885dec2a08e23c5d56a100d7dcb71 Mon Sep 17 00:00:00 2001 From: FITAHIANA Nomeniavo Joe <24nomeniavo@gmail.com> Date: Sun, 26 Jul 2026 16:35:14 +0300 Subject: [PATCH 2/6] chore: add wrapper --- src/lib.rs | 2 +- src/middleware.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1c08feb..de30e11 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -583,7 +583,7 @@ fn execute_route_handler( if router.middlewares.is_empty() { route.handler.call(py, (request.clone(),), Some(&kwargs)) } else { - let chain = MiddlewareChain::new(router.middlewares.clone()); + let chain = MiddlewareChain::new(&router.middlewares); chain.execute( py, route.sequence, diff --git a/src/middleware.rs b/src/middleware.rs index 01ce50c..0396faa 100644 --- a/src/middleware.rs +++ b/src/middleware.rs @@ -17,12 +17,12 @@ impl Middleware { } } -pub struct MiddlewareChain { - middlewares: Vec, +pub struct MiddlewareChain<'l> { + middlewares: &'l [Middleware], } -impl MiddlewareChain { - pub fn new(middlewares: Vec) -> Self { +impl<'l> MiddlewareChain<'l> { + pub fn new(middlewares: &'l [Middleware]) -> Self { Self { middlewares } } From d6bf8bcbecf4a46819bfebb64cf84d281b76b405 Mon Sep 17 00:00:00 2001 From: FITAHIANA Nomeniavo Joe <24nomeniavo@gmail.com> Date: Wed, 29 Jul 2026 19:51:17 +0300 Subject: [PATCH 3/6] feat: wraper & registe function to template - remove cors - remplace catcher with wrappe function - allows user to inject function in template engine - only tera is available --- Cargo.lock | 19 +------- Cargo.toml | 3 +- README.md | 2 +- build.sh | 2 +- oxapy/__init__.py | 2 +- src/lib.rs | 6 +-- src/request.rs | 2 - src/response.rs | 9 +--- src/templating/minijinja.rs | 65 ------------------------- src/templating/mod.rs | 96 +++++++++++++++++++++++++++---------- src/templating/tera.rs | 46 ------------------ 11 files changed, 79 insertions(+), 173 deletions(-) delete mode 100644 src/templating/minijinja.rs delete mode 100644 src/templating/tera.rs diff --git a/Cargo.lock b/Cargo.lock index 0f5b130..b80038e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1247,12 +1247,6 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" -[[package]] -name = "memo-map" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b" - [[package]] name = "micromap" version = "0.3.0" @@ -1265,16 +1259,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "minijinja" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3d648e68cea56d9858d535ee28f9538404e2dd8cb08ed0bd05dca379477f39" -dependencies = [ - "memo-map", - "serde", -] - [[package]] name = "mio" version = "1.2.2" @@ -1485,7 +1469,7 @@ checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" [[package]] name = "oxapy" -version = "0.9.1" +version = "0.10.0" dependencies = [ "ahash", "ctrlc", @@ -1497,7 +1481,6 @@ dependencies = [ "jsonschema", "jsonwebtoken", "matchit", - "minijinja", "multer", "pyo3", "pyo3-async-runtimes", diff --git a/Cargo.toml b/Cargo.toml index 1fdb745..c30e2ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "oxapy" -version = "0.9.1" +version = "0.10.0" edition = "2024" authors = ["FITAHIANA Nomeniavo Joe <24nomeniavo@gmail.com>"] repository = "https://github.com/j03-dev/oxapy" @@ -44,7 +44,6 @@ serde_json = "1.0.145" jsonschema = { version = "0.46.4", default-features = false } # Template engines -minijinja = "2.14.0" tera = "1.20" # Utilities diff --git a/README.md b/README.md index a928ead..2240c59 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ OxAPY is Python HTTP server library build in Rust - a fast, safe and featureementation.

- + PyPI Downloads

diff --git a/build.sh b/build.sh index 77cd63f..ef58702 100755 --- a/build.sh +++ b/build.sh @@ -1,4 +1,4 @@ #!/usr/bin/env bash -cargo run --bin stub_gen +cargo run --bin stub_gen --features stub_gen ./.venv/bin/maturin dev --release diff --git a/oxapy/__init__.py b/oxapy/__init__.py index 3aec8f7..6268c64 100644 --- a/oxapy/__init__.py +++ b/oxapy/__init__.py @@ -216,7 +216,7 @@ def _session_middleware(request, next, secret, max_age, **kwargs): request.session = session_data initial_state = json.dumps(session_data) - response = convert_to_response(next(request, **kwargs)) # ty:ignore + response = convert_to_response(next(request, **kwargs)) current_state = json.dumps(request.session) if current_state != initial_state: diff --git a/src/lib.rs b/src/lib.rs index de30e11..da5cbeb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,7 +44,6 @@ pyo3_stub_gen::export_verbatim!("oxapy", "from typing_extensions import Self"); pyo3_stub_gen::define_stub_info_gatherer!(stub_info); struct ProcessRequest { - cors: Option>, wrapper: Option>>, router: Option>, match_route: Option>, @@ -57,7 +56,6 @@ struct RequestContext { app_data: Option>>, wrapper: Option>>, channel_capacity: usize, - cors: Option>, routers: Vec>, request_sender: Sender, template: Option>, @@ -451,7 +449,6 @@ impl HttpServer { app_data: self.app_data.clone(), wrapper: self.wrapper.clone(), channel_capacity: self.channel_capacity, - cors: self.cors.clone(), routers: self.routers.clone(), request_sender: tx, template: self.template.clone(), @@ -524,8 +521,7 @@ impl HttpServer { call_python_handler(&req.router, &req.match_route, &req.request, self.is_async) .await .unwrap_or_else(Response::from) - .call_wrapper(&req) - .apply_cors(&req.cors)?; + .call_wrapper(&req); let _ = req.tx.send(response).await; Ok(()) } diff --git a/src/request.rs b/src/request.rs index f583edd..a8c4a6b 100644 --- a/src/request.rs +++ b/src/request.rs @@ -284,7 +284,6 @@ impl Request { router: Some(router), match_route: Some(transmutate_route), tx, - cors: ctx.cors.clone(), wrapper: ctx.wrapper.clone(), }; @@ -302,7 +301,6 @@ impl Request { router: None, match_route: None, tx, - cors: ctx.cors.clone(), wrapper: ctx.wrapper.clone(), }; diff --git a/src/response.rs b/src/response.rs index 95b4d45..15f7392 100644 --- a/src/response.rs +++ b/src/response.rs @@ -17,7 +17,7 @@ use pyo3::prelude::*; use pyo3::types::{PyBytes, PyString}; use pyo3_stub_gen::derive::*; -use crate::{Cors, IntoPyException, ProcessRequest, Status, convert_to_response, json}; +use crate::{IntoPyException, ProcessRequest, Status, convert_to_response, json}; pub type Body = BoxBody; @@ -242,13 +242,6 @@ impl Response { } self } - - pub(crate) fn apply_cors(mut self, cors: &Option>) -> PyResult { - if let Some(cors) = cors { - self = cors.apply_to_response(self)?; - } - Ok(self) - } } /// HTTP redirect response. diff --git a/src/templating/minijinja.rs b/src/templating/minijinja.rs deleted file mode 100644 index 0234b72..0000000 --- a/src/templating/minijinja.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::sync::Arc; - -use ahash::HashMap; -use minijinja::Environment; -use pyo3::{prelude::*, types::PyDict}; -use pyo3_stub_gen::derive::*; - -use crate::IntoPyException; -use crate::json; - -#[gen_stub_pyclass] -#[pyclass(from_py_object, module = "oxapy.templating")] -#[derive(Debug, Clone)] -pub struct Jinja { - engine: Arc>, -} - -#[gen_stub_pymethods] -#[pymethods] -impl Jinja { - #[new] - #[gen_stub(override_return_type(type_repr = "typing_extensions.Self", imports = ("typing_extensions",)))] - pub fn new(dir: String) -> PyResult { - let mut env = Environment::new(); - - let paths = glob::glob(&dir).into_py_exception()?; - - for entry in paths { - let path = entry.into_py_exception()?; - if path.is_file() { - let name = { - let full_path = path.to_str().unwrap().to_string(); - let name = full_path.split("/").skip(1); - name.collect::>().join("/") - }; - let content = std::fs::read_to_string(&path)?; - let name = Box::leak(name.into_boxed_str()); - let content = Box::leak(content.into_boxed_str()); - env.add_template(name, content).into_py_exception()?; - } - } - - Ok(Self { - engine: Arc::new(env), - }) - } - - #[pyo3(signature=(template_name, context=None))] - pub fn render( - &self, - template_name: String, - context: Option>, - ) -> PyResult { - let template = self - .engine - .get_template(&template_name) - .into_py_exception()?; - let mut ctx_values: HashMap = HashMap::default(); - if let Some(context) = context { - let value = json::from_pydict2rstruct(&context)?; - ctx_values = value; - } - template.render(ctx_values).into_py_exception() - } -} diff --git a/src/templating/mod.rs b/src/templating/mod.rs index aa8183c..dcd56c1 100644 --- a/src/templating/mod.rs +++ b/src/templating/mod.rs @@ -1,20 +1,47 @@ +use std::sync::Arc; + +use ahash::HashMap; use hyper::{HeaderMap, header::CONTENT_TYPE}; use pyo3::{ Bound, PyResult, - exceptions::{PyException, PyValueError}, + exceptions::PyValueError, prelude::*, types::{PyDict, PyModule, PyModuleMethods}, }; use pyo3_stub_gen::derive::*; +use tera::{Function, Result as TeraResult, Value}; use crate::{ + exceptions::IntoPyException, + json, request::Request, response::{Response, ResponseBody}, status::Status, }; -mod minijinja; -mod tera; +struct PyTeraFunction { + callable: Py, +} + +impl Function for PyTeraFunction { + fn call(&self, args: &std::collections::HashMap) -> TeraResult { + Python::attach(|py| { + let py_kwargs = json::from_rstruct2pydict(args, py) + .map_err(tera::Error::msg)? + .into_bound(py); + let result = self + .callable + .call(py, (), Some(&py_kwargs)) + .map_err(tera::Error::msg)?; + + Ok(Value::String(result.to_string())) + }) + } + + fn is_safe(&self) -> bool { + true + } +} /// Template engine for rendering HTML templates. /// @@ -43,12 +70,11 @@ mod tera; /// # Or use Tera with custom template directory /// app.template(templating.Template("./views/**/*.html", "tera")) /// ``` -#[gen_stub_pyclass_enum] #[pyclass(from_py_object, module = "oxapy.templating")] +#[gen_stub_pyclass] #[derive(Clone, Debug)] -pub enum Template { - Jinja(minijinja::Jinja), - Tera(tera::Tera), +pub struct Template { + engine: Arc, } #[gen_stub_pymethods] @@ -58,7 +84,6 @@ impl Template { /// /// Args: /// dir (str, optional): Directory pattern to search for templates (default: "./templates/**/*.html"). - /// engine (str, optional): Template engine to use, either "jinja" or "tera" (default: "jinja"). /// /// Returns: /// Template: A new template engine instance. @@ -74,18 +99,46 @@ impl Template { /// template = templating.Template() /// /// # Use Tera with custom template directory - /// template = templating.Template("./views/**/*.html", "tera") + /// template = templating.Template("./views/**/*.html") /// ``` #[new] - #[pyo3(signature=(dir="./templates/**/*.html", engine="jinja"))] + #[pyo3(signature=(dir="./templates/**/*.html"))] #[gen_stub(override_return_type(type_repr = "typing_extensions.Self", imports = ("typing_extensions",)))] - fn new(dir: &str, engine: &str) -> PyResult