Skip to content

Applications API

The applications module contains the core Plinx class, which is the main entry point for creating Plinx web applications.

Plinx Class

plinx.applications.Plinx

The main application class for the Plinx web framework.

This class serves as the WSGI application entry point and manages routes, middleware, and request handling. It provides a Flask-like decorator syntax for adding routes and Django-like method for explicitly registering them.

Examples:

Creating a simple app with a route:

from plinx import Plinx

app = Plinx()

@app.route("/")
def home(request, response):
    response.text = "Hello, World!"

Using HTTP method-specific decorators:

@app.get("/users")
def list_users(request, response):
    response.json = {"users": ["user1", "user2"]}

@app.post("/users")
def create_user(request, response):
    response.text = "User created"

Using class-based views:

@app.route("/books")
class BooksResource:
    def get(self, request, response):
        response.text = "List of books"

    def post(self, request, response):
        response.text = "Book created"
Source code in plinx/applications.py
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
class Plinx:
    """
    The main application class for the Plinx web framework.

    This class serves as the WSGI application entry point and manages routes,
    middleware, and request handling. It provides a Flask-like decorator syntax
    for adding routes and Django-like method for explicitly registering them.

    Examples:
        Creating a simple app with a route:

        ```python
        from plinx import Plinx

        app = Plinx()

        @app.route("/")
        def home(request, response):
            response.text = "Hello, World!"
        ```

        Using HTTP method-specific decorators:

        ```python
        @app.get("/users")
        def list_users(request, response):
            response.json = {"users": ["user1", "user2"]}

        @app.post("/users")
        def create_user(request, response):
            response.text = "User created"
        ```

        Using class-based views:

        ```python
        @app.route("/books")
        class BooksResource:
            def get(self, request, response):
                response.text = "List of books"

            def post(self, request, response):
                response.text = "Book created"
        ```
    """

    def __init__(self):
        """
        Initialize a new Plinx application instance.

        Sets up the routing table, middleware stack, and
        dynamically generates HTTP method-specific decorators.
        """
        self.routes: Dict[str, Tuple[HTTPMethods, Callable]] = {}
        self.exception_handler = None
        self.middleware = Middleware(self)

        self._method_decorators = {}
        for method in HTTPMethods:
            self._method_decorators[method.name.lower()] = (
                self._create_method_decorator(method)
            )

    def __call__(
        self,
        environ: WSGIEnvironment,
        start_response: StartResponse,
    ) -> Iterable[bytes]:
        """
        WSGI entry point for the application.

        This method makes the Plinx instance callable as required by the WSGI spec,
        allowing it to be used directly with WSGI servers like Gunicorn or uWSGI.

        Args:
            environ: The WSGI environment dictionary containing request information
            start_response: The WSGI start_response callable

        Returns:
            An iterable of bytes representing the response body
        """
        return self.middleware(environ, start_response)

    def add_route(
        self,
        path: str,
        handler: Callable,
        method: HTTPMethods = HTTPMethods.GET,
    ):
        """
        Explicitly register a route with the application.

        This provides a Django-like syntax for registering routes,
        as an alternative to the decorator approach.

        Args:
            path: URL pattern to match (may contain parameters)
            handler: Function or class to handle matching requests
            method: HTTP method to match (defaults to GET)

        Raises:
            RuntimeError: If the path is already registered

        Example:
            ```python
            def home(request, response):
                response.text = "Hello, World!"

            app.add_route("/home", home)
            ```
        """
        if path in self.routes:
            raise RuntimeError(f"Route '{path}' is already registered.")

        self.routes[path] = (method, handler)

    def route(
        self,
        path: str,
    ):
        """
        Register a route via decorator syntax.

        This implements Flask-like syntax for registering routes. It can be used
        with both function-based handlers and class-based handlers.

        Args:
            path: URL pattern to match (may contain parameters)

        Returns:
            A decorator function that registers the handler

        Example:
            ```python
            @app.route("/home")
            def home(request, response):
                response.text = "Hello, World!"
            ```

            For class-based views:

            ```python
            @app.route("/books")
            class BooksResource:
                def get(self, request, response):
                    response.text = "List of books"
            ```
        """

        def wrapper(handler):
            self.add_route(path, handler)
            return handler

        return wrapper

    def __getattr__(
        self,
        name: str,
    ):
        """
        Enable HTTP method-specific decorators like app.get, app.post, etc.

        This magic method is called when an attribute lookup fails, allowing
        us to dynamically provide HTTP method decorators.

        Args:
            name: The attribute name being looked up

        Returns:
            A method-specific decorator function if name matches a HTTP method

        Raises:
            RuntimeError: If the attribute doesn't match a known HTTP method
        """
        if name in self._method_decorators:
            return self._method_decorators[name]
        raise RuntimeError(
            f"'{self.__class__.__name__}' object has no attribute '{name}'"
        )

    def _create_method_decorator(self, method: HTTPMethods):
        """
        Create a decorator for a specific HTTP method.

        This internal method generates the decorators used for HTTP method-specific
        route registration like @app.get(), @app.post(), etc.

        Args:
            method: The HTTP method enum value

        Returns:
            A decorator function for the specified HTTP method
        """

        def decorator(path: str):
            def wrapper(handler):
                self.add_route(path, handler, method)
                return handler

            return wrapper

        return decorator

    def handle_request(
        self,
        request: Request,
    ) -> Response:
        """
        Process an incoming request and generate a response.

        This is the core request handling logic that finds a matching route handler,
        executes it, and handles any exceptions.

        Args:
            request: The incoming WebOb Request object

        Returns:
            A Response object containing the response data
        """
        response: Response = Response()

        handler_definition, kwargs = self.find_handler(request, response)

        try:
            if handler_definition is not None:
                method, handler = handler_definition

                # Handle CBVs
                if inspect.isclass(handler):
                    handler = getattr(
                        handler(),
                        request.method.lower(),
                        None,
                    )
                    # only allow methods that are defined in the class
                    if handler is None:
                        response.status_code = StatusCodes.METHOD_NOT_ALLOWED.value
                        response.text = "Method Not Allowed"
                        return response

                if inspect.isfunction(handler):
                    # Handle function-based views
                    if request.method != method.value:
                        response.status_code = StatusCodes.METHOD_NOT_ALLOWED.value
                        response.text = "Method Not Allowed"
                        return response

                handler(request, response, **kwargs)

        except Exception as e:
            if self.exception_handler:
                self.exception_handler(request, response, e)
            else:
                response.status_code = StatusCodes.INTERNAL_SERVER_ERROR.value
                response.text = str(e)

        return response

    def find_handler(
        self,
        request: Request,
        response: Response,
    ) -> Tuple[Tuple[HTTPMethods, Callable] | None, dict | None]:
        """
        Find the appropriate handler for a request based on URL path matching.

        This method iterates through registered routes and uses the parse library
        to match URL patterns and extract parameters.

        Args:
            request: The incoming WebOb Request object
            response: The Response object being built

        Returns:
            A tuple containing:
                - The handler definition (method, handler) if found, or None
                - A dictionary of extracted URL parameters, or None
        """
        for path, handler in self.routes.items():
            parse_result = parse(path, request.path)
            if parse_result is not None:
                return handler, parse_result.named

        handle_404(response)
        return None, None

    def add_exception_handler(
        self,
        exception_handler,
    ):
        """
        Register a global exception handler for the application.

        The exception handler will be called whenever an uncaught exception
        occurs during request handling.

        Args:
            exception_handler: Callable that takes (request, response, exception)

        Example:
            ```python
            def handle_exceptions(request, response, exception):
                response.status_code = 500
                response.text = f"Error: {str(exception)}"

            app.add_exception_handler(handle_exceptions)
            ```
        """
        self.exception_handler = exception_handler

    def add_middleware(
        self,
        middleware_cls: type[Middleware],
    ):
        """
        Add a middleware class to the application's middleware stack.

        Middleware classes must inherit from the Middleware base class and can
        process requests before they reach handlers and responses before they're returned.

        Args:
            middleware_cls: A class inheriting from Middleware

        Example:
            ```python
            class SimpleMiddleware(Middleware):
                def process_request(self, request):
                    print("Processing request")

                def process_response(self, request, response):
                    print("Processing response")

            app.add_middleware(SimpleMiddleware)
            ```
        """
        self.middleware.add(middleware_cls)

    def test_session(self, base_url="http://testserver"):
        """
        Create a test client session for this application.

        This provides an interface similar to the requests library for testing
        your application without making actual HTTP calls.

        Args:
            base_url: Base URL to use for requests (default: "http://testserver")

        Returns:
            A requests.Session object configured to call this application

        Example:
            ```python
            client = app.test_session()
            response = client.get("/home")
            assert response.status_code == 200
            ```
        """
        session = RequestsSession()
        session.mount(prefix=base_url, adapter=RequestsWSGIAdapter(self))
        return session

Functions

__call__(environ, start_response)

WSGI entry point for the application.

This method makes the Plinx instance callable as required by the WSGI spec, allowing it to be used directly with WSGI servers like Gunicorn or uWSGI.

Parameters:

Name Type Description Default
environ WSGIEnvironment

The WSGI environment dictionary containing request information

required
start_response StartResponse

The WSGI start_response callable

required

Returns:

Type Description
Iterable[bytes]

An iterable of bytes representing the response body

Source code in plinx/applications.py
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def __call__(
    self,
    environ: WSGIEnvironment,
    start_response: StartResponse,
) -> Iterable[bytes]:
    """
    WSGI entry point for the application.

    This method makes the Plinx instance callable as required by the WSGI spec,
    allowing it to be used directly with WSGI servers like Gunicorn or uWSGI.

    Args:
        environ: The WSGI environment dictionary containing request information
        start_response: The WSGI start_response callable

    Returns:
        An iterable of bytes representing the response body
    """
    return self.middleware(environ, start_response)
__getattr__(name)

Enable HTTP method-specific decorators like app.get, app.post, etc.

This magic method is called when an attribute lookup fails, allowing us to dynamically provide HTTP method decorators.

Parameters:

Name Type Description Default
name str

The attribute name being looked up

required

Returns:

Type Description

A method-specific decorator function if name matches a HTTP method

Raises:

Type Description
RuntimeError

If the attribute doesn't match a known HTTP method

Source code in plinx/applications.py
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
def __getattr__(
    self,
    name: str,
):
    """
    Enable HTTP method-specific decorators like app.get, app.post, etc.

    This magic method is called when an attribute lookup fails, allowing
    us to dynamically provide HTTP method decorators.

    Args:
        name: The attribute name being looked up

    Returns:
        A method-specific decorator function if name matches a HTTP method

    Raises:
        RuntimeError: If the attribute doesn't match a known HTTP method
    """
    if name in self._method_decorators:
        return self._method_decorators[name]
    raise RuntimeError(
        f"'{self.__class__.__name__}' object has no attribute '{name}'"
    )
__init__()

Initialize a new Plinx application instance.

Sets up the routing table, middleware stack, and dynamically generates HTTP method-specific decorators.

Source code in plinx/applications.py
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
def __init__(self):
    """
    Initialize a new Plinx application instance.

    Sets up the routing table, middleware stack, and
    dynamically generates HTTP method-specific decorators.
    """
    self.routes: Dict[str, Tuple[HTTPMethods, Callable]] = {}
    self.exception_handler = None
    self.middleware = Middleware(self)

    self._method_decorators = {}
    for method in HTTPMethods:
        self._method_decorators[method.name.lower()] = (
            self._create_method_decorator(method)
        )
add_exception_handler(exception_handler)

Register a global exception handler for the application.

The exception handler will be called whenever an uncaught exception occurs during request handling.

Parameters:

Name Type Description Default
exception_handler

Callable that takes (request, response, exception)

required
Example
def handle_exceptions(request, response, exception):
    response.status_code = 500
    response.text = f"Error: {str(exception)}"

app.add_exception_handler(handle_exceptions)
Source code in plinx/applications.py
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
def add_exception_handler(
    self,
    exception_handler,
):
    """
    Register a global exception handler for the application.

    The exception handler will be called whenever an uncaught exception
    occurs during request handling.

    Args:
        exception_handler: Callable that takes (request, response, exception)

    Example:
        ```python
        def handle_exceptions(request, response, exception):
            response.status_code = 500
            response.text = f"Error: {str(exception)}"

        app.add_exception_handler(handle_exceptions)
        ```
    """
    self.exception_handler = exception_handler
add_middleware(middleware_cls)

Add a middleware class to the application's middleware stack.

Middleware classes must inherit from the Middleware base class and can process requests before they reach handlers and responses before they're returned.

Parameters:

Name Type Description Default
middleware_cls type[Middleware]

A class inheriting from Middleware

required
Example
class SimpleMiddleware(Middleware):
    def process_request(self, request):
        print("Processing request")

    def process_response(self, request, response):
        print("Processing response")

app.add_middleware(SimpleMiddleware)
Source code in plinx/applications.py
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
def add_middleware(
    self,
    middleware_cls: type[Middleware],
):
    """
    Add a middleware class to the application's middleware stack.

    Middleware classes must inherit from the Middleware base class and can
    process requests before they reach handlers and responses before they're returned.

    Args:
        middleware_cls: A class inheriting from Middleware

    Example:
        ```python
        class SimpleMiddleware(Middleware):
            def process_request(self, request):
                print("Processing request")

            def process_response(self, request, response):
                print("Processing response")

        app.add_middleware(SimpleMiddleware)
        ```
    """
    self.middleware.add(middleware_cls)
add_route(path, handler, method=HTTPMethods.GET)

Explicitly register a route with the application.

This provides a Django-like syntax for registering routes, as an alternative to the decorator approach.

Parameters:

Name Type Description Default
path str

URL pattern to match (may contain parameters)

required
handler Callable

Function or class to handle matching requests

required
method HTTPMethods

HTTP method to match (defaults to GET)

GET

Raises:

Type Description
RuntimeError

If the path is already registered

Example
def home(request, response):
    response.text = "Hello, World!"

app.add_route("/home", home)
Source code in plinx/applications.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
def add_route(
    self,
    path: str,
    handler: Callable,
    method: HTTPMethods = HTTPMethods.GET,
):
    """
    Explicitly register a route with the application.

    This provides a Django-like syntax for registering routes,
    as an alternative to the decorator approach.

    Args:
        path: URL pattern to match (may contain parameters)
        handler: Function or class to handle matching requests
        method: HTTP method to match (defaults to GET)

    Raises:
        RuntimeError: If the path is already registered

    Example:
        ```python
        def home(request, response):
            response.text = "Hello, World!"

        app.add_route("/home", home)
        ```
    """
    if path in self.routes:
        raise RuntimeError(f"Route '{path}' is already registered.")

    self.routes[path] = (method, handler)
find_handler(request, response)

Find the appropriate handler for a request based on URL path matching.

This method iterates through registered routes and uses the parse library to match URL patterns and extract parameters.

Parameters:

Name Type Description Default
request Request

The incoming WebOb Request object

required
response PlinxResponse

The Response object being built

required

Returns:

Type Description
Tuple[Tuple[HTTPMethods, Callable] | None, dict | None]

A tuple containing: - The handler definition (method, handler) if found, or None - A dictionary of extracted URL parameters, or None

Source code in plinx/applications.py
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
def find_handler(
    self,
    request: Request,
    response: Response,
) -> Tuple[Tuple[HTTPMethods, Callable] | None, dict | None]:
    """
    Find the appropriate handler for a request based on URL path matching.

    This method iterates through registered routes and uses the parse library
    to match URL patterns and extract parameters.

    Args:
        request: The incoming WebOb Request object
        response: The Response object being built

    Returns:
        A tuple containing:
            - The handler definition (method, handler) if found, or None
            - A dictionary of extracted URL parameters, or None
    """
    for path, handler in self.routes.items():
        parse_result = parse(path, request.path)
        if parse_result is not None:
            return handler, parse_result.named

    handle_404(response)
    return None, None
handle_request(request)

Process an incoming request and generate a response.

This is the core request handling logic that finds a matching route handler, executes it, and handles any exceptions.

Parameters:

Name Type Description Default
request Request

The incoming WebOb Request object

required

Returns:

Type Description
PlinxResponse

A Response object containing the response data

Source code in plinx/applications.py
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
def handle_request(
    self,
    request: Request,
) -> Response:
    """
    Process an incoming request and generate a response.

    This is the core request handling logic that finds a matching route handler,
    executes it, and handles any exceptions.

    Args:
        request: The incoming WebOb Request object

    Returns:
        A Response object containing the response data
    """
    response: Response = Response()

    handler_definition, kwargs = self.find_handler(request, response)

    try:
        if handler_definition is not None:
            method, handler = handler_definition

            # Handle CBVs
            if inspect.isclass(handler):
                handler = getattr(
                    handler(),
                    request.method.lower(),
                    None,
                )
                # only allow methods that are defined in the class
                if handler is None:
                    response.status_code = StatusCodes.METHOD_NOT_ALLOWED.value
                    response.text = "Method Not Allowed"
                    return response

            if inspect.isfunction(handler):
                # Handle function-based views
                if request.method != method.value:
                    response.status_code = StatusCodes.METHOD_NOT_ALLOWED.value
                    response.text = "Method Not Allowed"
                    return response

            handler(request, response, **kwargs)

    except Exception as e:
        if self.exception_handler:
            self.exception_handler(request, response, e)
        else:
            response.status_code = StatusCodes.INTERNAL_SERVER_ERROR.value
            response.text = str(e)

    return response
route(path)

Register a route via decorator syntax.

This implements Flask-like syntax for registering routes. It can be used with both function-based handlers and class-based handlers.

Parameters:

Name Type Description Default
path str

URL pattern to match (may contain parameters)

required

Returns:

Type Description

A decorator function that registers the handler

Example
@app.route("/home")
def home(request, response):
    response.text = "Hello, World!"

For class-based views:

@app.route("/books")
class BooksResource:
    def get(self, request, response):
        response.text = "List of books"
Source code in plinx/applications.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
def route(
    self,
    path: str,
):
    """
    Register a route via decorator syntax.

    This implements Flask-like syntax for registering routes. It can be used
    with both function-based handlers and class-based handlers.

    Args:
        path: URL pattern to match (may contain parameters)

    Returns:
        A decorator function that registers the handler

    Example:
        ```python
        @app.route("/home")
        def home(request, response):
            response.text = "Hello, World!"
        ```

        For class-based views:

        ```python
        @app.route("/books")
        class BooksResource:
            def get(self, request, response):
                response.text = "List of books"
        ```
    """

    def wrapper(handler):
        self.add_route(path, handler)
        return handler

    return wrapper
test_session(base_url='http://testserver')

Create a test client session for this application.

This provides an interface similar to the requests library for testing your application without making actual HTTP calls.

Parameters:

Name Type Description Default
base_url

Base URL to use for requests (default: "http://testserver")

'http://testserver'

Returns:

Type Description

A requests.Session object configured to call this application

Example
client = app.test_session()
response = client.get("/home")
assert response.status_code == 200
Source code in plinx/applications.py
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
def test_session(self, base_url="http://testserver"):
    """
    Create a test client session for this application.

    This provides an interface similar to the requests library for testing
    your application without making actual HTTP calls.

    Args:
        base_url: Base URL to use for requests (default: "http://testserver")

    Returns:
        A requests.Session object configured to call this application

    Example:
        ```python
        client = app.test_session()
        response = client.get("/home")
        assert response.status_code == 200
        ```
    """
    session = RequestsSession()
    session.mount(prefix=base_url, adapter=RequestsWSGIAdapter(self))
    return session

Example Usage

Basic Application

from plinx import Plinx

app = Plinx()

@app.route("/")
def home(request, response):
    response.text = "Hello, World!"

@app.route("/about")
def about(request, response):
    response.text = "About page"

HTTP Method-Specific Routes

@app.get("/api/items")
def get_items(request, response):
    response.json = {"items": ["item1", "item2"]}

@app.post("/api/items")
def create_item(request, response):
    response.text = "Item created"
    response.status_code = 201  # Created

Dynamic Route Parameters

@app.route("/users/{user_id}")
def get_user(request, response, user_id):
    response.json = {"user_id": user_id, "name": f"User {user_id}"}

Class-Based Views

@app.route("/resources")
class ResourceHandler:
    def get(self, request, response):
        response.json = {"resources": ["res1", "res2"]}

    def post(self, request, response):
        response.text = "Resource created"
        response.status_code = 201

Adding Middleware

from plinx.middleware import Middleware

class LoggingMiddleware(Middleware):
    def process_request(self, request):
        print(f"Request: {request.method} {request.path}")

    def process_response(self, request, response):
        print(f"Response: {response.status_code}")

app = Plinx()
app.add_middleware(LoggingMiddleware)

Exception Handling

def exception_handler(request, response, exception):
    response.status_code = 500
    response.json = {
        "error": str(exception),
        "type": exception.__class__.__name__
    }

app = Plinx()
app.add_exception_handler(exception_handler)

Running with a WSGI Server

# app.py
from plinx import Plinx

app = Plinx()

@app.route("/")
def home(request, response):
    response.text = "Hello, World!"
# Run with a WSGI server like Gunicorn
gunicorn app:app

Testing

app = Plinx()

@app.route("/")
def home(request, response):
    response.text = "Hello, World!"

# Create a test client
client = app.test_session()

# Make requests
response = client.get("/")
assert response.text == "Hello, World!"
assert response.status_code == 200