Skip to content
Open
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
91 changes: 91 additions & 0 deletions docs/routing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Routing

URL based routing ...

```python
>>> u = httpx.URL('/users/alex')
>>> r = httpx.Router([
... httpx.Path('/', endpoint=homepage),
... httpx.Path('/users', endpoint=user_list),
... httpx.Path('/users/{userid}', endpoint=user_profile),
... ])
>>> m = r.match(u)
<Match ['user_profile', kwargs={'userid': 'alex'}]>
>>> m.endpoint
<function user_profile>
>>> m.name
'user_profile'
>>> m.kwargs
{'userid': 'alex'}
```

---

## Router

The `Router` class implements the `Transport` API and can be used to provide either server-side or client-side URL routing.

...

---

## Path

...

## PathPrefix

...

## Domain

...

## Scheme

...

---

## Route

The `Route` class is an interface defining the core API for URL matching.

### match()

...

### reverse()

...

---

# Server Side Routing

URL based routing is designed to integrate with server applications...

```python
routes = [
httpx.Scheme('http', endpoint=https_redirect),
httpx.Scheme('https', routes=[
httpx.Host('www.example.com', routes=[
httpx.Path('/', endpoint=...),
httpx.Path('/users', endpoint=...),
httpx.Path('/users/{user_id}', endpoint=...),
httpx.PathPrefix('/static', endpoint=serve_static),
httpx.PathPrefix('/', endpoint=trailing_slash_redirect_or_not_found),
]),
httpx.Host('example.com', endpoint=www_redirect),
httpx.Host('*', endpoint=host_disallowed),
])
]
```

---

# Client Side Routing

While URL based routing is most typically used server-side, it can also be useful in some client side contexts.

...
2 changes: 2 additions & 0 deletions scripts/unasync
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ unasync.unasync_files(
"src/ahttpx/_client.py",
"src/ahttpx/_models.py",
"src/ahttpx/_pool.py",
"src/ahttpx/_routing.py",
"src/ahttpx/_transport.py",
"src/ahttpx/_urlparse.py",
"src/ahttpx/_urls.py"
],
Expand Down
1 change: 1 addition & 0 deletions src/ahttpx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from ._models import * # Content, File, Files, Form, Headers, JSON, MultiPart, Response, Request, Text
from ._network import * # NetworkBackend, NetworkStream, timeout
from ._pool import * # Connection, ConnectionPool, Transport, open_connection_pool, open_connection
from ._transport import * # Transport
from ._urls import * # QueryParams, URL


Expand Down
140 changes: 140 additions & 0 deletions src/ahttpx/_routing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
from __future__ import annotations

from ._models import Response, Request, URL
from ._transport import Transport

import re
import typing


__all__ = ["Path", "Route", "Router", "Match"]


class Route:
def match(self, url: URL) -> "Match" | None:
raise NotImplementedError()

def reverse(self, name: str, **kwargs: str) -> URL | None:
raise NotImplementedError()


class Path(Route):
def __init__(self, path: str, endpoint: typing.Any, name: str | None = None):
self._path = path
self._endpoint = endpoint
self._pattern = self._compile_regex(path)
self._name = self._generate_name(endpoint, name)

def _generate_name(self, endpoint: typing.Any, name: str | None) -> str:
return name or endpoint.__name__

def _compile_regex(self, path) -> re.Pattern:
# Escape all non-placeholder characters in the pattern
escaped_path = re.escape(path)
# Replace escaped curly brace placeholders with regex groups
regex_pattern = re.sub(r'\\{([^{}]+)\\}', r'(?P<\1>[^/]+)', escaped_path)
return re.compile(f'^{regex_pattern}$')

@property
def name(self) -> str:
return self._name

@property
def path(self) -> str:
return self._path

@property
def endpoint(self) -> typing.Any:
return self._endpoint

def match(self, url: URL) -> "Match" | None:
m = self._pattern.match(url.path)
if m is None:
return None

return Match(
name=self.name,
kwargs=m.groupdict(),
endpoint=self.endpoint
)

def reverse(self, name: str, **kwargs: str) -> URL | None:
if name != self._name:
return None
if kwargs.keys() != self._pattern.groupindex.keys():
return None
return URL(self._path.format(**kwargs))

def __repr__(self):
return f"<Path [{self.name!r} {self.path!r}]>"


class Scheme(Route):
def __init__(self, scheme: str, routes: list[Route]):
self._scheme = scheme
self._routes = routes
self._router = Router(routes)

def match(self, url: URL) -> "Match" | None:
if url.scheme == self._scheme:
return self._router.match(url)
return None

def reverse(self, name: str, **kwargs: str) -> URL | None:
return self._router.reverse(name, **kwargs)

def __repr__(self):
return f"<Scheme [{self.scheme!r}]>"


class Match:
def __init__(self, name: str, kwargs: dict[str, str], endpoint: typing.Any):
self._name = name
self._kwargs = kwargs
self._endpoint = endpoint

@property
def name(self) -> str:
return self._name

@property
def kwargs(self) -> dict[str, str]:
return dict(self._kwargs)

@property
def endpoint(self) -> typing.Any:
return self._endpoint

def __repr__(self) -> str:
return f"<Match [{self.name!r} {self.kwargs!r}]>"


class Router(Transport):
def __init__(self, routes: list[Route]):
self._routes = list(routes)

@property
def routes(self) -> list[Route]:
return list(self._routes)

def match(self, url: URL) -> "Match" | None:
for route in self._routes:
m = route.match(url)
if m is not None:
return m

def reverse(self, name: str, **kwargs: str) -> URL | None:
for route in self._routes:
r = route.reverse(name, kwargs)
if r is not None:
return r

async def send(self, request: Request) -> typing.AsyncIterator[Response]:
match = self.match(request.url)
if match is not None:
response = await match.endpoint(request)
return response
return Response(404)

def __repr__(self) -> str:
return "<Router>"
37 changes: 37 additions & 0 deletions src/ahttpx/_transport.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import contextlib
import typing

from ._models import Content, Request, Response, URL


class Transport:
@contextlib.asynccontextmanager
async def send(self, request: Request) -> typing.AsyncIterator[Response]:
raise NotImplementedError()

async def close(self):
pass

async def request(
self,
method: str,
url: URL | str,
headers: typing.Mapping[str, str] | None = None,
content: Content | typing.AsyncIterable[bytes] | bytes | None = None,
) -> Response:
request = Request(method, url, headers=headers, content=content)
async with self.send(request) as response:
await response.read()
return response

@contextlib.asynccontextmanager
async def stream(
self,
method: str,
url: URL | str,
headers: typing.Mapping[str, str] | None = None,
content: Content | typing.AsyncIterable[bytes] | bytes | None = None,
) -> typing.AsyncIterator[Response]:
request = Request(method, url, headers=headers, content=content)
async with self.send(request) as response:
yield response
1 change: 1 addition & 0 deletions src/httpx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from ._models import * # Content, File, Files, Form, Headers, JSON, MultiPart, Response, Request, Text
from ._network import * # NetworkBackend, NetworkStream, timeout
from ._pool import * # Connection, ConnectionPool, Transport, open_connection_pool, open_connection
from ._transport import * # Transport
from ._urls import * # QueryParams, URL


Expand Down
Loading