rilweb 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rilweb/__init__.py +3 -0
- rilweb/cookies.py +30 -0
- rilweb/proto.py +30 -0
- rilweb/request.py +58 -0
- rilweb/response.py +82 -0
- rilweb/rilweb.py +284 -0
- rilweb/templating.py +34 -0
- rilweb-0.1.0.dist-info/METADATA +13 -0
- rilweb-0.1.0.dist-info/RECORD +11 -0
- rilweb-0.1.0.dist-info/WHEEL +5 -0
- rilweb-0.1.0.dist-info/top_level.txt +1 -0
rilweb/__init__.py
ADDED
rilweb/cookies.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import hmac
|
|
2
|
+
import hashlib
|
|
3
|
+
import base64
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def sign_cookie(value: str, secret: str) -> str:
|
|
7
|
+
signature = hmac.new(
|
|
8
|
+
secret.encode(),
|
|
9
|
+
value.encode(),
|
|
10
|
+
hashlib.sha256
|
|
11
|
+
).digest()
|
|
12
|
+
|
|
13
|
+
signature = base64.urlsafe_b64encode(signature).decode().rstrip("=")
|
|
14
|
+
|
|
15
|
+
return f"{value}.{signature}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def unsign_cookie(signed_value: str, secret: str):
|
|
19
|
+
try:
|
|
20
|
+
value, signature = signed_value.rsplit(".", 1)
|
|
21
|
+
|
|
22
|
+
expected = sign_cookie(value, secret).rsplit(".", 1)[1]
|
|
23
|
+
|
|
24
|
+
if not hmac.compare_digest(signature, expected):
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
return value
|
|
28
|
+
|
|
29
|
+
except (ValueError, AttributeError):
|
|
30
|
+
return None
|
rilweb/proto.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# === IMPORTS ===
|
|
2
|
+
from typing import Dict, List, Optional, Protocol, Self, Callable
|
|
3
|
+
|
|
4
|
+
# === REQUEST ===
|
|
5
|
+
|
|
6
|
+
class RequestLike(Protocol):
|
|
7
|
+
method: str
|
|
8
|
+
path: str
|
|
9
|
+
headers: Dict[str, str]
|
|
10
|
+
query: Dict[str, List[str]]
|
|
11
|
+
body: Optional[bytes]
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def from_raw(cls, data: bytes) -> Self:
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
# === RESPONSE ===
|
|
18
|
+
|
|
19
|
+
class ResponseLike(Protocol):
|
|
20
|
+
status_code: int
|
|
21
|
+
body: bytes
|
|
22
|
+
headers: Optional[Dict[str, str]]
|
|
23
|
+
|
|
24
|
+
def to_bytes(self) -> bytes:
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
# === MIDDLEWARE ===
|
|
28
|
+
|
|
29
|
+
class MiddlewareLike(Protocol):
|
|
30
|
+
async def __call__(self, request: RequestLike, call_next: Callable) -> Optional[ResponseLike]: ...
|
rilweb/request.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# === IMPORTS ===
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Dict, List, Self, Optional
|
|
4
|
+
from urllib.parse import urlsplit, parse_qs
|
|
5
|
+
|
|
6
|
+
# === HTTP REQUEST ===
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class HTTPRequest:
|
|
10
|
+
method: str
|
|
11
|
+
path: str
|
|
12
|
+
headers: Dict[str, str]
|
|
13
|
+
query: Dict[str, List[str]]
|
|
14
|
+
body: Optional[bytes]
|
|
15
|
+
cookies: Dict[str, str] = field(default_factory=dict)
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_raw(cls, data: bytes) -> Self:
|
|
19
|
+
header_part, _, body = data.partition(b'\r\n\r\n')
|
|
20
|
+
lines = header_part.split(b'\r\n')
|
|
21
|
+
|
|
22
|
+
req_line_parts = lines[0].split(b' ')
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
method = req_line_parts[0].decode()
|
|
26
|
+
url = req_line_parts[1].decode()
|
|
27
|
+
urlsplitted = urlsplit(url)
|
|
28
|
+
|
|
29
|
+
except (IndexError, UnicodeDecodeError):
|
|
30
|
+
raise ValueError("Invalid HTTP request line")
|
|
31
|
+
|
|
32
|
+
headers = {}
|
|
33
|
+
|
|
34
|
+
for line in lines[1:]:
|
|
35
|
+
if b':' in line:
|
|
36
|
+
key, val = line.split(b':', 1)
|
|
37
|
+
|
|
38
|
+
headers[key.decode('latin-1').strip().lower()] = val.decode('latin-1').strip()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
return cls(
|
|
42
|
+
method=method,
|
|
43
|
+
path=urlsplitted.path,
|
|
44
|
+
headers=headers,
|
|
45
|
+
query=parse_qs(urlsplitted.query) if urlsplitted.query else {},
|
|
46
|
+
body=body if body else None,
|
|
47
|
+
cookies=cls._parse_cookies(headers)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def _parse_cookies(headers: Dict[str, str]) -> Dict[str, str]:
|
|
52
|
+
cookies = {}
|
|
53
|
+
cookie_header = headers.get("cookie", "")
|
|
54
|
+
if cookie_header:
|
|
55
|
+
for cookie in cookie_header.split(";"):
|
|
56
|
+
name, _, value = cookie.strip().partition("=")
|
|
57
|
+
cookies[name] = value
|
|
58
|
+
return cookies
|
rilweb/response.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# === IMPORTS ===
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
# === HTTP RESPONSE ===
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class HTTPResponse:
|
|
10
|
+
status_code: int
|
|
11
|
+
body: bytes
|
|
12
|
+
headers: Optional[Dict[str, str]] = None
|
|
13
|
+
|
|
14
|
+
def __post_init__(self):
|
|
15
|
+
if self.headers is None:
|
|
16
|
+
self.headers = {}
|
|
17
|
+
self.cookies = {}
|
|
18
|
+
|
|
19
|
+
def to_bytes(self) -> bytes:
|
|
20
|
+
headers = self.headers.copy()
|
|
21
|
+
|
|
22
|
+
if self.body is not None:
|
|
23
|
+
headers.setdefault("Content-Length", str(len(self.body)))
|
|
24
|
+
else:
|
|
25
|
+
headers.setdefault("Content-Length", "0")
|
|
26
|
+
|
|
27
|
+
headers.setdefault("Connection", "close")
|
|
28
|
+
|
|
29
|
+
status_line = f"HTTP/1.1 {self.status_code}\r\n"
|
|
30
|
+
|
|
31
|
+
header_lines = list((
|
|
32
|
+
f"{key}: {value}\r\n"
|
|
33
|
+
for key, value in headers.items()
|
|
34
|
+
))
|
|
35
|
+
|
|
36
|
+
for name, (value, options) in self.cookies.items():
|
|
37
|
+
cookie = f"{name}={value}"
|
|
38
|
+
if "expires" in options:
|
|
39
|
+
cookie += f"; Expires={options['expires']}"
|
|
40
|
+
if "max_age" in options:
|
|
41
|
+
cookie += f"; Max-Age={options['max_age']}"
|
|
42
|
+
if "domain" in options:
|
|
43
|
+
cookie += f"; Domain={options['domain']}"
|
|
44
|
+
if "path" in options:
|
|
45
|
+
cookie += f"; Path={options['path']}"
|
|
46
|
+
if "samesite" in options:
|
|
47
|
+
cookie += f"; SameSite={options['samesite']}"
|
|
48
|
+
if options.get("secure"):
|
|
49
|
+
cookie += "; Secure"
|
|
50
|
+
if options.get("httponly"):
|
|
51
|
+
cookie += "; HttpOnly"
|
|
52
|
+
|
|
53
|
+
header_lines.append(f"Set-Cookie: {cookie}\r\n")
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
status_line.encode("ascii")
|
|
57
|
+
+ "".join(header_lines).encode("latin-1")
|
|
58
|
+
+ b"\r\n"
|
|
59
|
+
+ (self.body or b"")
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_file(cls, file_path: str, **kw) -> "HTTPResponse":
|
|
64
|
+
with open(file_path, "rb") as f:
|
|
65
|
+
body = f.read()
|
|
66
|
+
|
|
67
|
+
kw['body'] = body
|
|
68
|
+
return cls(**kw)
|
|
69
|
+
|
|
70
|
+
def set_cookie(self, name: str, value: str, **options):
|
|
71
|
+
if not all([option in ("expires", "max_age", "domain", "path", "secure", "httponly") for option in options]):
|
|
72
|
+
raise ValueError("Invalid cookie option")
|
|
73
|
+
|
|
74
|
+
self.cookies[name] = (value, options)
|
|
75
|
+
|
|
76
|
+
class JSONResponse(HTTPResponse):
|
|
77
|
+
def __init__(self, status_code: int, headers: Dict[str, str], body: Any, **json_kw):
|
|
78
|
+
headers = headers.copy()
|
|
79
|
+
headers.setdefault("Content-Type", "application/json; charset=utf-8")
|
|
80
|
+
|
|
81
|
+
body = json.dumps(body, **json_kw).encode("utf-8")
|
|
82
|
+
super().__init__(status_code, body, headers)
|
rilweb/rilweb.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# === IMPORTS ===
|
|
2
|
+
import socket
|
|
3
|
+
from .proto import ResponseLike, MiddlewareLike
|
|
4
|
+
from .request import HTTPRequest
|
|
5
|
+
from .response import HTTPResponse
|
|
6
|
+
from typing import Callable, Tuple, Dict, Awaitable, TypeAlias, Optional
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import traceback
|
|
9
|
+
import asyncio
|
|
10
|
+
from .cookies import sign_cookie as _sign_cookie, unsign_cookie as _unsign_cookie
|
|
11
|
+
|
|
12
|
+
# === TYPES ===
|
|
13
|
+
|
|
14
|
+
Handler: TypeAlias = Callable[[HTTPRequest], Awaitable[ResponseLike]]
|
|
15
|
+
|
|
16
|
+
# === MAIN CLASS ===
|
|
17
|
+
|
|
18
|
+
class RilWeb:
|
|
19
|
+
def __init__(self, host: str, port: int, dev: bool = False, secret_key: Optional[str] = None):
|
|
20
|
+
"""
|
|
21
|
+
:param host: The host address to bind the server to.
|
|
22
|
+
:param port: The port number to bind the server to.
|
|
23
|
+
:param dev: Whether to run the server in development mode (default is False).
|
|
24
|
+
"""
|
|
25
|
+
self.host = host
|
|
26
|
+
self.port = port
|
|
27
|
+
self.dev = dev
|
|
28
|
+
self.secret_key = secret_key
|
|
29
|
+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # create and configure
|
|
30
|
+
# socket options
|
|
31
|
+
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # allow reuse of the address
|
|
32
|
+
self.sock.bind((self.host, self.port))
|
|
33
|
+
self.routes: Dict[Tuple[str, str], Handler] = {}
|
|
34
|
+
self.error_handlers: Dict[int, Handler] = {}
|
|
35
|
+
self.middlewares: list[MiddlewareLike] = []
|
|
36
|
+
|
|
37
|
+
self.tasks: set[asyncio.Task] = set()
|
|
38
|
+
|
|
39
|
+
self.set_error_handler(404, self._404) # set up default error handlers
|
|
40
|
+
self.set_error_handler(500, self._500)
|
|
41
|
+
|
|
42
|
+
# route management
|
|
43
|
+
|
|
44
|
+
def add_route(self, path: str, handler: Handler, method: str = "GET"):
|
|
45
|
+
"""
|
|
46
|
+
:param path: The URL path to route.
|
|
47
|
+
:param handler: The handler function to call for this route.
|
|
48
|
+
:param method: The HTTP method for this route (default is "GET").
|
|
49
|
+
"""
|
|
50
|
+
self.routes[(path, method)] = handler
|
|
51
|
+
|
|
52
|
+
def route(self, path: str, method: str = "GET"):
|
|
53
|
+
"""
|
|
54
|
+
Decorator to add a route to the server.
|
|
55
|
+
|
|
56
|
+
:param path: The URL path to route.
|
|
57
|
+
:param method: The HTTP method for this route (default is "GET").
|
|
58
|
+
"""
|
|
59
|
+
def decorator(handler):
|
|
60
|
+
self.add_route(path, handler, method)
|
|
61
|
+
return handler
|
|
62
|
+
return decorator
|
|
63
|
+
|
|
64
|
+
# error handlers
|
|
65
|
+
|
|
66
|
+
def set_error_handler(self, status_code: int, handler: Handler):
|
|
67
|
+
"""
|
|
68
|
+
Set an error handler for a specific HTTP status code.
|
|
69
|
+
|
|
70
|
+
:param status_code: The HTTP status code to handle.
|
|
71
|
+
:param handler: The handler function to call for this error.
|
|
72
|
+
"""
|
|
73
|
+
self.error_handlers[status_code] = handler
|
|
74
|
+
|
|
75
|
+
def error_handler(self, status_code: int):
|
|
76
|
+
"""
|
|
77
|
+
Decorator to set an error handler for a specific HTTP status code.
|
|
78
|
+
|
|
79
|
+
:param status_code: The HTTP status code to handle.
|
|
80
|
+
"""
|
|
81
|
+
def decorator(handler):
|
|
82
|
+
self.set_error_handler(status_code, handler)
|
|
83
|
+
return handler
|
|
84
|
+
return decorator
|
|
85
|
+
|
|
86
|
+
# default error handler helpers
|
|
87
|
+
|
|
88
|
+
async def _404(self, request: HTTPRequest) -> ResponseLike:
|
|
89
|
+
html_path = Path(__file__).parent / "404.html"
|
|
90
|
+
return HTTPResponse.from_file(str(html_path), status_code=404)
|
|
91
|
+
|
|
92
|
+
async def _500(self, request: HTTPRequest) -> ResponseLike:
|
|
93
|
+
html_path = Path(__file__).parent / "500.html"
|
|
94
|
+
return HTTPResponse.from_file(str(html_path), status_code=500)
|
|
95
|
+
|
|
96
|
+
async def _make_500(self, request, traceback_str):
|
|
97
|
+
if self.dev and self.error_handlers[500] is self._500:
|
|
98
|
+
return HTTPResponse(500, traceback_str.encode())
|
|
99
|
+
|
|
100
|
+
return await self.error_handlers[500](request)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# request handling
|
|
104
|
+
|
|
105
|
+
async def _handle_req(self, request: HTTPRequest, client_addr: str, client_port: int) -> ResponseLike:
|
|
106
|
+
key = (request.path, request.method)
|
|
107
|
+
handler = self.routes.get(key)
|
|
108
|
+
|
|
109
|
+
print(f'[INCOMING] from {client_addr}:{client_port}')
|
|
110
|
+
|
|
111
|
+
# 404 handling
|
|
112
|
+
if handler is None:
|
|
113
|
+
res = await self.error_handlers[404](request)
|
|
114
|
+
print(f'[{res.status_code}] {request.method} to {request.path}') # logging
|
|
115
|
+
return res
|
|
116
|
+
|
|
117
|
+
# define middleware caller
|
|
118
|
+
|
|
119
|
+
async def call_middleware(index: int, req: HTTPRequest):
|
|
120
|
+
if index >= len(self.middlewares):
|
|
121
|
+
return await handler(req)
|
|
122
|
+
|
|
123
|
+
middleware = self.middlewares[index]
|
|
124
|
+
|
|
125
|
+
return await middleware(
|
|
126
|
+
req,
|
|
127
|
+
lambda r: call_middleware(index + 1, r)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# run the route handler
|
|
131
|
+
try:
|
|
132
|
+
res = await call_middleware(0, request)
|
|
133
|
+
except Exception:
|
|
134
|
+
traceback.print_exc()
|
|
135
|
+
res = await self._make_500(request, traceback.format_exc())
|
|
136
|
+
|
|
137
|
+
print(f'[{res.status_code}] {request.method} to {request.path!r}') # logging
|
|
138
|
+
|
|
139
|
+
return res
|
|
140
|
+
|
|
141
|
+
# low-level request reception
|
|
142
|
+
async def _receive_request(self, client_sock: socket.socket) -> bytes:
|
|
143
|
+
loop = asyncio.get_running_loop()
|
|
144
|
+
data = bytearray()
|
|
145
|
+
|
|
146
|
+
# define constants
|
|
147
|
+
|
|
148
|
+
MAX_HEADER_SIZE = 64 * 1024
|
|
149
|
+
MAX_BODY_SIZE = 10 * 1024 * 1024
|
|
150
|
+
|
|
151
|
+
# read http headers
|
|
152
|
+
|
|
153
|
+
while b"\r\n\r\n" not in data:
|
|
154
|
+
chunk = await loop.sock_recv(client_sock, 4096)
|
|
155
|
+
|
|
156
|
+
if not chunk:
|
|
157
|
+
raise ConnectionError("Client disconnected before sending headers")
|
|
158
|
+
|
|
159
|
+
data.extend(chunk)
|
|
160
|
+
|
|
161
|
+
if len(data) > MAX_HEADER_SIZE:
|
|
162
|
+
raise ValueError("HTTP headers are too large")
|
|
163
|
+
|
|
164
|
+
header_end = data.index(b"\r\n\r\n")
|
|
165
|
+
header_data = bytes(data[:header_end])
|
|
166
|
+
|
|
167
|
+
content_length = 0
|
|
168
|
+
|
|
169
|
+
# parse headers
|
|
170
|
+
|
|
171
|
+
for line in header_data.split(b"\r\n")[1:]:
|
|
172
|
+
name, separator, value = line.partition(b":")
|
|
173
|
+
|
|
174
|
+
if not separator:
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
name = name.strip().lower()
|
|
178
|
+
value = value.strip()
|
|
179
|
+
|
|
180
|
+
if name == b"transfer-encoding":
|
|
181
|
+
raise NotImplementedError("Chunked transfer encoding is not supported yet")
|
|
182
|
+
|
|
183
|
+
if name == b"content-length":
|
|
184
|
+
content_length = int(value)
|
|
185
|
+
|
|
186
|
+
if content_length < 0:
|
|
187
|
+
raise ValueError("Invalid Content-Length")
|
|
188
|
+
|
|
189
|
+
if content_length > MAX_BODY_SIZE:
|
|
190
|
+
raise ValueError("HTTP request body is too large")
|
|
191
|
+
|
|
192
|
+
body_start = header_end + 4
|
|
193
|
+
body_end = body_start + content_length
|
|
194
|
+
|
|
195
|
+
while len(data) < body_end:
|
|
196
|
+
chunk = await loop.sock_recv(client_sock, 4096)
|
|
197
|
+
|
|
198
|
+
if not chunk:
|
|
199
|
+
raise ConnectionError("Client disconnected before sending the complete body")
|
|
200
|
+
|
|
201
|
+
data.extend(chunk)
|
|
202
|
+
|
|
203
|
+
return bytes(data[:body_end])
|
|
204
|
+
|
|
205
|
+
# internal run function
|
|
206
|
+
|
|
207
|
+
async def _run(self):
|
|
208
|
+
self.sock.setblocking(False)
|
|
209
|
+
self.sock.listen(5)
|
|
210
|
+
|
|
211
|
+
loop = asyncio.get_running_loop()
|
|
212
|
+
self.tasks = set()
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
while True:
|
|
216
|
+
client_sock, client_addr = await loop.sock_accept(self.sock)
|
|
217
|
+
|
|
218
|
+
task = asyncio.create_task(
|
|
219
|
+
self._handle_client(client_sock, client_addr)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
self.tasks.add(task)
|
|
223
|
+
task.add_done_callback(self.tasks.discard)
|
|
224
|
+
|
|
225
|
+
finally:
|
|
226
|
+
self.sock.close()
|
|
227
|
+
|
|
228
|
+
for task in self.tasks:
|
|
229
|
+
task.cancel()
|
|
230
|
+
|
|
231
|
+
await asyncio.gather(*self.tasks, return_exceptions=True)
|
|
232
|
+
self.tasks.clear()
|
|
233
|
+
|
|
234
|
+
# client handling
|
|
235
|
+
|
|
236
|
+
async def _handle_client(self, client_sock, client_addr):
|
|
237
|
+
client_sock.setblocking(False)
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
loop = asyncio.get_running_loop()
|
|
241
|
+
|
|
242
|
+
request_data = await self._receive_request(client_sock)
|
|
243
|
+
request = HTTPRequest.from_raw(request_data)
|
|
244
|
+
|
|
245
|
+
response = await self._handle_req(request, *client_addr)
|
|
246
|
+
|
|
247
|
+
await loop.sock_sendall(client_sock, response.to_bytes())
|
|
248
|
+
|
|
249
|
+
except Exception:
|
|
250
|
+
traceback.print_exc()
|
|
251
|
+
|
|
252
|
+
finally:
|
|
253
|
+
client_sock.close()
|
|
254
|
+
|
|
255
|
+
# redirects
|
|
256
|
+
def redirect(self, url: str, status_code: int = 302):
|
|
257
|
+
return HTTPResponse(status_code, b"", headers={"Location": url})
|
|
258
|
+
|
|
259
|
+
# middleware management
|
|
260
|
+
|
|
261
|
+
def add_middleware(self, middleware: MiddlewareLike):
|
|
262
|
+
self.middlewares.append(middleware)
|
|
263
|
+
|
|
264
|
+
def run(self):
|
|
265
|
+
try:
|
|
266
|
+
asyncio.run(self._run())
|
|
267
|
+
except KeyboardInterrupt:
|
|
268
|
+
print("\nRilweb: server stopped.")
|
|
269
|
+
|
|
270
|
+
def sign_cookie(self, value: str) -> str:
|
|
271
|
+
if self.secret_key is None:
|
|
272
|
+
raise RuntimeError("Secret key is not set")
|
|
273
|
+
|
|
274
|
+
return _sign_cookie(value, self.secret_key)
|
|
275
|
+
|
|
276
|
+
def verify_cookie(self, request: HTTPRequest, name: str):
|
|
277
|
+
if self.secret_key is None:
|
|
278
|
+
raise RuntimeError("Secret key is not set")
|
|
279
|
+
|
|
280
|
+
signed_value = request.cookies.get(name)
|
|
281
|
+
if not signed_value:
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
return _unsign_cookie(signed_value, self.secret_key)
|
rilweb/templating.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
try:
|
|
2
|
+
import jinja2
|
|
3
|
+
except ImportError as e:
|
|
4
|
+
raise RuntimeError(
|
|
5
|
+
"Jinja2 is not installed. "
|
|
6
|
+
"Please install it with `pip install jinja2` "
|
|
7
|
+
"to use templating features."
|
|
8
|
+
) from e
|
|
9
|
+
|
|
10
|
+
from .response import HTTPResponse
|
|
11
|
+
from typing import Optional, Dict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TemplateResponse(HTTPResponse):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
fp: str,
|
|
18
|
+
status_code: int = 200,
|
|
19
|
+
headers: Optional[Dict[str, str]] = None,
|
|
20
|
+
**context
|
|
21
|
+
):
|
|
22
|
+
env = jinja2.Environment(
|
|
23
|
+
loader=jinja2.FileSystemLoader("."),
|
|
24
|
+
autoescape=True
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
template = env.get_template(fp)
|
|
28
|
+
rendered = template.render(**context)
|
|
29
|
+
|
|
30
|
+
super().__init__(
|
|
31
|
+
status_code,
|
|
32
|
+
rendered.encode("utf-8"),
|
|
33
|
+
headers=headers
|
|
34
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rilweb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A simple http framework built in python using `socket` and `asyncio`.
|
|
5
|
+
Author-email: Arlo Talkington <mongpod67@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: example,package,template
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Provides-Extra: templating
|
|
13
|
+
Requires-Dist: jinja2>=3.1.2; extra == "templating"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
rilweb/__init__.py,sha256=KCnbpSn2v8Me506_2tGT7CRMZPabQAlCQtR66br4DtY,110
|
|
2
|
+
rilweb/cookies.py,sha256=lIXuAap78JAVm_3SCQYBP_clSCjyW7jSjJ4C6hhjiek,690
|
|
3
|
+
rilweb/proto.py,sha256=s0nMC-EdzH6poF-r60Nx45oGZd7t67-Rsl2tw4NkOD8,685
|
|
4
|
+
rilweb/request.py,sha256=YXsExbtJJxWSMyo2P1OpB_Okef9Z6jAGjoMK1SiFsu0,1773
|
|
5
|
+
rilweb/response.py,sha256=gvSGiYIt03wokb3uNHBKFg61t8p0ugWNtUj0Dv_BXw4,2741
|
|
6
|
+
rilweb/rilweb.py,sha256=kpk54U2K_l1fxKIJkK9xrWYQVyhTjMl6eOBulxrZNOQ,9239
|
|
7
|
+
rilweb/templating.py,sha256=fiYvxkYJXXuxUSwj8lzRaJjRmi8WDR1MUaKfsAY4-L4,848
|
|
8
|
+
rilweb-0.1.0.dist-info/METADATA,sha256=SiBrUIOGjp_C_df5jSIi6zteui4_Lev-lhgCa12Ruac,494
|
|
9
|
+
rilweb-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
10
|
+
rilweb-0.1.0.dist-info/top_level.txt,sha256=H4yJFVQTb11llvNoivbTV7ogOF_p4jsRAKYiMrqdYvY,7
|
|
11
|
+
rilweb-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
rilweb
|