lema-basic-web-backend 0.1.0__tar.gz
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.
- lema_basic_web_backend-0.1.0/LICENSE +21 -0
- lema_basic_web_backend-0.1.0/PKG-INFO +546 -0
- lema_basic_web_backend-0.1.0/README.md +517 -0
- lema_basic_web_backend-0.1.0/pyproject.toml +66 -0
- lema_basic_web_backend-0.1.0/setup.cfg +4 -0
- lema_basic_web_backend-0.1.0/src/basic_web_backend/__init__.py +86 -0
- lema_basic_web_backend-0.1.0/src/basic_web_backend/adapters.py +39 -0
- lema_basic_web_backend-0.1.0/src/basic_web_backend/application.py +217 -0
- lema_basic_web_backend-0.1.0/src/basic_web_backend/config.py +214 -0
- lema_basic_web_backend-0.1.0/src/basic_web_backend/exceptions.py +176 -0
- lema_basic_web_backend-0.1.0/src/basic_web_backend/logging_config.py +61 -0
- lema_basic_web_backend-0.1.0/src/basic_web_backend/multipart.py +109 -0
- lema_basic_web_backend-0.1.0/src/basic_web_backend/request.py +180 -0
- lema_basic_web_backend-0.1.0/src/basic_web_backend/response.py +197 -0
- lema_basic_web_backend-0.1.0/src/basic_web_backend/routing.py +343 -0
- lema_basic_web_backend-0.1.0/src/basic_web_backend/static.py +29 -0
- lema_basic_web_backend-0.1.0/src/basic_web_backend/template/__init__.py +3 -0
- lema_basic_web_backend-0.1.0/src/basic_web_backend/template/environment.py +110 -0
- lema_basic_web_backend-0.1.0/src/basic_web_backend/template/evaluator.py +114 -0
- lema_basic_web_backend-0.1.0/src/basic_web_backend/template/lexer.py +127 -0
- lema_basic_web_backend-0.1.0/src/basic_web_backend/template/nodes.py +34 -0
- lema_basic_web_backend-0.1.0/src/basic_web_backend/template/parser.py +221 -0
- lema_basic_web_backend-0.1.0/src/lema_basic_web_backend.egg-info/PKG-INFO +546 -0
- lema_basic_web_backend-0.1.0/src/lema_basic_web_backend.egg-info/SOURCES.txt +43 -0
- lema_basic_web_backend-0.1.0/src/lema_basic_web_backend.egg-info/dependency_links.txt +1 -0
- lema_basic_web_backend-0.1.0/src/lema_basic_web_backend.egg-info/requires.txt +9 -0
- lema_basic_web_backend-0.1.0/src/lema_basic_web_backend.egg-info/top_level.txt +1 -0
- lema_basic_web_backend-0.1.0/tests/test_adapters.py +166 -0
- lema_basic_web_backend-0.1.0/tests/test_application.py +1312 -0
- lema_basic_web_backend-0.1.0/tests/test_application_integration.py +208 -0
- lema_basic_web_backend-0.1.0/tests/test_config.py +176 -0
- lema_basic_web_backend-0.1.0/tests/test_exceptions.py +174 -0
- lema_basic_web_backend-0.1.0/tests/test_logging_config.py +53 -0
- lema_basic_web_backend-0.1.0/tests/test_multipart.py +113 -0
- lema_basic_web_backend-0.1.0/tests/test_public_api.py +147 -0
- lema_basic_web_backend-0.1.0/tests/test_request.py +558 -0
- lema_basic_web_backend-0.1.0/tests/test_response.py +286 -0
- lema_basic_web_backend-0.1.0/tests/test_routing.py +412 -0
- lema_basic_web_backend-0.1.0/tests/test_static.py +248 -0
- lema_basic_web_backend-0.1.0/tests/test_template_enviroment.py +345 -0
- lema_basic_web_backend-0.1.0/tests/test_template_evalutor.py +422 -0
- lema_basic_web_backend-0.1.0/tests/test_template_integration.py +89 -0
- lema_basic_web_backend-0.1.0/tests/test_template_lexer.py +131 -0
- lema_basic_web_backend-0.1.0/tests/test_template_nodes.py +96 -0
- lema_basic_web_backend-0.1.0/tests/test_template_parser.py +311 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Martin Lénárt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lema-basic-web-backend
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight Python web backend framework with routing, request handling, templates, static files, and configurable server adapters.
|
|
5
|
+
Author: Martin Lénárt
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/lema2000-dev/basic_web_backend
|
|
8
|
+
Project-URL: Repository, https://github.com/lema2000-dev/basic_web_backend.git
|
|
9
|
+
Project-URL: Issues, https://github.com/lema2000-dev/basic_web_backend/issues
|
|
10
|
+
Keywords: web,backend,framework,http,routing,template-engine,web-server
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Provides-Extra: server
|
|
22
|
+
Requires-Dist: lema-basic-web-server<0.2.0,>=0.1.0; extra == "server"
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
25
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
26
|
+
Requires-Dist: twine>=5; extra == "dev"
|
|
27
|
+
Requires-Dist: lema-basic-web-server<0.2.0,>=0.1.0; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# Basic Web Backend
|
|
31
|
+
|
|
32
|
+
A small Python web backend framework designed to work with
|
|
33
|
+
[`lema-basic-web-server`](https://github.com/lema2000-dev/basic_web_server).
|
|
34
|
+
|
|
35
|
+
The project provides:
|
|
36
|
+
|
|
37
|
+
- static and dynamic routing;
|
|
38
|
+
- HTTP method handling;
|
|
39
|
+
- request parsing;
|
|
40
|
+
- response helper functions;
|
|
41
|
+
- JSON, form and multipart request processing;
|
|
42
|
+
- static file serving;
|
|
43
|
+
- cookies;
|
|
44
|
+
- a lightweight template engine;
|
|
45
|
+
- configurable error handlers;
|
|
46
|
+
- application logging;
|
|
47
|
+
- replaceable request and response adapters.
|
|
48
|
+
|
|
49
|
+
The framework uses only the Python standard library at runtime.
|
|
50
|
+
|
|
51
|
+
## Requirements
|
|
52
|
+
|
|
53
|
+
- Python 3.10 or newer
|
|
54
|
+
- `lema-basic-web-server` when using the default Lema adapters
|
|
55
|
+
|
|
56
|
+
## Development installation
|
|
57
|
+
|
|
58
|
+
Clone the repository and install it in editable mode:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
git clone https://github.com/lema2000-dev/basic_web_backend.git
|
|
62
|
+
cd basic_web_backend
|
|
63
|
+
python -m pip install -e ".[dev]"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Run the test suite:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
python -m pytest -v
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Minimal application
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from basic_web_backend import (
|
|
76
|
+
WebApplication,
|
|
77
|
+
html_response,
|
|
78
|
+
)
|
|
79
|
+
from basic_web_server import Server
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
app = WebApplication()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@app.route("/")
|
|
86
|
+
def index(request):
|
|
87
|
+
return html_response(
|
|
88
|
+
"<h1>Hello from Basic Web Backend!</h1>"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
server = Server(app)
|
|
94
|
+
server.start_console()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
After starting the console, use the server's `run` command to select
|
|
98
|
+
the listening address and port.
|
|
99
|
+
|
|
100
|
+
## Routing
|
|
101
|
+
|
|
102
|
+
Routes are registered with the `route()` decorator:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
@app.route("/about")
|
|
106
|
+
def about(request):
|
|
107
|
+
return html_response("<h1>About</h1>")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Multiple HTTP methods can be registered:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
@app.route(
|
|
114
|
+
"/users",
|
|
115
|
+
methods=["GET", "POST"],
|
|
116
|
+
)
|
|
117
|
+
def users(request):
|
|
118
|
+
return html_response("<h1>Users</h1>")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Dynamic routes
|
|
122
|
+
|
|
123
|
+
Supported route converters:
|
|
124
|
+
|
|
125
|
+
| Converter | Example | Python value |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| `string` | `/users/<string:name>` | `str` |
|
|
128
|
+
| `int` | `/users/<int:user_id>` | `int` |
|
|
129
|
+
| `float` | `/values/<float:value>` | `float` |
|
|
130
|
+
| `path` | `/files/<path:filename>` | `str` |
|
|
131
|
+
|
|
132
|
+
Example:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
@app.route("/users/<int:user_id>")
|
|
136
|
+
def user_profile(request, user_id):
|
|
137
|
+
return html_response(
|
|
138
|
+
f"<h1>User {user_id}</h1>"
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The `path` converter can contain `/` characters and must be the final
|
|
143
|
+
route segment.
|
|
144
|
+
|
|
145
|
+
## Request data
|
|
146
|
+
|
|
147
|
+
A view receives a backend `Request` object:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
@app.route("/search")
|
|
151
|
+
def search(request):
|
|
152
|
+
query = request.query.get(
|
|
153
|
+
"query",
|
|
154
|
+
[""],
|
|
155
|
+
)[0]
|
|
156
|
+
|
|
157
|
+
return html_response(
|
|
158
|
+
f"<p>Search: {query}</p>"
|
|
159
|
+
)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Frequently used request attributes and methods:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
request.method
|
|
166
|
+
request.path
|
|
167
|
+
request.query_string
|
|
168
|
+
request.query
|
|
169
|
+
request.headers
|
|
170
|
+
request.body
|
|
171
|
+
|
|
172
|
+
request.get_header("Content-Type")
|
|
173
|
+
request.get_headers("Cookie")
|
|
174
|
+
request.cookies
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### JSON
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
@app.route("/api/users", methods=["POST"])
|
|
181
|
+
def create_user(request):
|
|
182
|
+
data = request.get_json()
|
|
183
|
+
|
|
184
|
+
return json_response(
|
|
185
|
+
{
|
|
186
|
+
"name": data["name"],
|
|
187
|
+
"created": True,
|
|
188
|
+
},
|
|
189
|
+
status_code=201,
|
|
190
|
+
)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### URL-encoded forms
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
form = request.get_form()
|
|
197
|
+
username = form["username"][0]
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Multipart forms and file uploads
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
form, files = request.get_multipart()
|
|
204
|
+
|
|
205
|
+
description = form["description"][0]
|
|
206
|
+
uploaded_file = files["document"][0]
|
|
207
|
+
|
|
208
|
+
uploaded_file.filename
|
|
209
|
+
uploaded_file.content_type
|
|
210
|
+
uploaded_file.body
|
|
211
|
+
uploaded_file.headers
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Repeated fields and files are preserved as lists.
|
|
215
|
+
|
|
216
|
+
## Response helpers
|
|
217
|
+
|
|
218
|
+
The public response helpers are:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
content_response()
|
|
222
|
+
html_response()
|
|
223
|
+
text_response()
|
|
224
|
+
json_response()
|
|
225
|
+
file_response()
|
|
226
|
+
redirect_response()
|
|
227
|
+
empty_response()
|
|
228
|
+
set_cookie()
|
|
229
|
+
delete_cookie()
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Examples:
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
return text_response(
|
|
236
|
+
"Application is running"
|
|
237
|
+
)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
return json_response(
|
|
242
|
+
{"successful": True},
|
|
243
|
+
status_code=200,
|
|
244
|
+
)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
return redirect_response(
|
|
249
|
+
"/login",
|
|
250
|
+
status_code=302,
|
|
251
|
+
)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
return empty_response(
|
|
256
|
+
status_code=204
|
|
257
|
+
)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Character encoding
|
|
261
|
+
|
|
262
|
+
Text, HTML and JSON responses support configurable character encoding:
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
return html_response(
|
|
266
|
+
"<h1>Hello</h1>",
|
|
267
|
+
charset="utf-16",
|
|
268
|
+
)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Cookies
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
response = json_response(
|
|
275
|
+
{"authenticated": True}
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return set_cookie(
|
|
279
|
+
response=response,
|
|
280
|
+
key="session_id",
|
|
281
|
+
value="abc123",
|
|
282
|
+
max_age=3600,
|
|
283
|
+
secure=True,
|
|
284
|
+
http_only=True,
|
|
285
|
+
same_site="Lax",
|
|
286
|
+
)
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Delete a cookie:
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
response = text_response("Logged out")
|
|
293
|
+
|
|
294
|
+
return delete_cookie(
|
|
295
|
+
response=response,
|
|
296
|
+
key="session_id",
|
|
297
|
+
)
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Static files
|
|
301
|
+
|
|
302
|
+
Static files are served from the `static` directory by default:
|
|
303
|
+
|
|
304
|
+
```text
|
|
305
|
+
static/
|
|
306
|
+
├── css/
|
|
307
|
+
│ └── style.css
|
|
308
|
+
└── js/
|
|
309
|
+
└── application.js
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
They are available under:
|
|
313
|
+
|
|
314
|
+
```text
|
|
315
|
+
/static/css/style.css
|
|
316
|
+
/static/js/application.js
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Custom configuration:
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
from basic_web_backend import (
|
|
323
|
+
ApplicationConfig,
|
|
324
|
+
WebApplication,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
config = ApplicationConfig(
|
|
329
|
+
static_folder="assets",
|
|
330
|
+
static_url_path="/assets",
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
app = WebApplication(config=config)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Disable built-in static file serving:
|
|
337
|
+
|
|
338
|
+
```python
|
|
339
|
+
config = ApplicationConfig(
|
|
340
|
+
static_folder=None
|
|
341
|
+
)
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
The static file handler prevents paths from escaping the configured
|
|
345
|
+
static directory.
|
|
346
|
+
|
|
347
|
+
## Template engine
|
|
348
|
+
|
|
349
|
+
Templates are loaded from the `templates` directory by default.
|
|
350
|
+
|
|
351
|
+
Example template:
|
|
352
|
+
|
|
353
|
+
```html
|
|
354
|
+
<h1>Hello, {{ username }}!</h1>
|
|
355
|
+
|
|
356
|
+
{% if active %}
|
|
357
|
+
<p>Your account is active.</p>
|
|
358
|
+
{% else %}
|
|
359
|
+
<p>Your account is inactive.</p>
|
|
360
|
+
{% endif %}
|
|
361
|
+
|
|
362
|
+
<ul>
|
|
363
|
+
{% for item in items %}
|
|
364
|
+
<li>{{ item.name }}</li>
|
|
365
|
+
{% endfor %}
|
|
366
|
+
</ul>
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Render the template from a view:
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
@app.route("/users/<string:username>")
|
|
373
|
+
def profile(request, username):
|
|
374
|
+
return app.render_template(
|
|
375
|
+
"profile.html",
|
|
376
|
+
username=username,
|
|
377
|
+
active=True,
|
|
378
|
+
items=[
|
|
379
|
+
{"name": "First"},
|
|
380
|
+
{"name": "Second"},
|
|
381
|
+
],
|
|
382
|
+
)
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Template output is HTML-escaped by default.
|
|
386
|
+
|
|
387
|
+
Template configuration:
|
|
388
|
+
|
|
389
|
+
```python
|
|
390
|
+
config = ApplicationConfig(
|
|
391
|
+
template_folder="templates",
|
|
392
|
+
template_encoding="utf-8",
|
|
393
|
+
template_autoescape=True,
|
|
394
|
+
template_cache=True,
|
|
395
|
+
template_auto_reload=False,
|
|
396
|
+
)
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
## HTTP errors
|
|
400
|
+
|
|
401
|
+
Views can raise public HTTP exceptions:
|
|
402
|
+
|
|
403
|
+
```python
|
|
404
|
+
from basic_web_backend import (
|
|
405
|
+
BadRequest,
|
|
406
|
+
NotFound,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@app.route("/products/<int:product_id>")
|
|
411
|
+
def product(request, product_id):
|
|
412
|
+
if product_id == 0:
|
|
413
|
+
raise NotFound(
|
|
414
|
+
path=request.path
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
if product_id < 0:
|
|
418
|
+
raise BadRequest(
|
|
419
|
+
"Product ID cannot be negative."
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
return html_response(
|
|
423
|
+
f"<h1>Product {product_id}</h1>"
|
|
424
|
+
)
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
Supported HTTP exceptions include:
|
|
428
|
+
|
|
429
|
+
- `BadRequest` — 400
|
|
430
|
+
- `Unauthorized` — 401
|
|
431
|
+
- `Forbidden` — 403
|
|
432
|
+
- `NotFound` — 404
|
|
433
|
+
- `MethodNotAllowed` — 405
|
|
434
|
+
- `Conflict` — 409
|
|
435
|
+
- `PayloadTooLarge` — 413
|
|
436
|
+
- `UnsupportedMediaType` — 415
|
|
437
|
+
- `UnprocessableContent` — 422
|
|
438
|
+
|
|
439
|
+
### Custom error handlers
|
|
440
|
+
|
|
441
|
+
```python
|
|
442
|
+
@app.errorhandler(404)
|
|
443
|
+
def handle_not_found(error):
|
|
444
|
+
return html_response(
|
|
445
|
+
"<h1>Custom 404 page</h1>",
|
|
446
|
+
status_code=404,
|
|
447
|
+
)
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
```python
|
|
451
|
+
@app.errorhandler(500)
|
|
452
|
+
def handle_internal_error(error):
|
|
453
|
+
return html_response(
|
|
454
|
+
"<h1>The service is unavailable</h1>",
|
|
455
|
+
status_code=500,
|
|
456
|
+
)
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Unexpected exceptions produce a generic 500 response in production
|
|
460
|
+
mode. Debug mode includes exception details in the response and must
|
|
461
|
+
not be enabled in production.
|
|
462
|
+
|
|
463
|
+
## Logging
|
|
464
|
+
|
|
465
|
+
Enable rotating application logs through `ApplicationConfig`:
|
|
466
|
+
|
|
467
|
+
```python
|
|
468
|
+
config = ApplicationConfig(
|
|
469
|
+
log_file="logs/backend.log",
|
|
470
|
+
log_level="INFO",
|
|
471
|
+
log_max_bytes=1_000_000,
|
|
472
|
+
log_backup_count=5,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
app = WebApplication(config=config)
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
When the active log reaches the configured size, older logs are stored
|
|
479
|
+
as:
|
|
480
|
+
|
|
481
|
+
```text
|
|
482
|
+
backend.log.1
|
|
483
|
+
backend.log.2
|
|
484
|
+
backend.log.3
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
Unexpected application errors and adapter failures are logged with
|
|
488
|
+
their traceback. Normal 4xx client errors are not logged by the
|
|
489
|
+
backend.
|
|
490
|
+
|
|
491
|
+
Do not store request bodies, passwords, authorization headers or
|
|
492
|
+
cookie values in application logs.
|
|
493
|
+
|
|
494
|
+
## Custom server adapters
|
|
495
|
+
|
|
496
|
+
A request adapter must provide:
|
|
497
|
+
|
|
498
|
+
```python
|
|
499
|
+
class CustomRequestAdapter:
|
|
500
|
+
def convert(self, server_request):
|
|
501
|
+
return Request(
|
|
502
|
+
method=server_request.method,
|
|
503
|
+
path=server_request.path,
|
|
504
|
+
query_string=server_request.query_string,
|
|
505
|
+
headers=server_request.headers,
|
|
506
|
+
body=server_request.body,
|
|
507
|
+
)
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
It must return a backend `Request` object.
|
|
511
|
+
|
|
512
|
+
A response adapter must provide:
|
|
513
|
+
|
|
514
|
+
```python
|
|
515
|
+
class CustomResponseAdapter:
|
|
516
|
+
def convert(self, backend_response):
|
|
517
|
+
return backend_response
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
Configure them with:
|
|
521
|
+
|
|
522
|
+
```python
|
|
523
|
+
config = ApplicationConfig(
|
|
524
|
+
request_adapter=CustomRequestAdapter(),
|
|
525
|
+
response_adapter=CustomResponseAdapter(),
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
app = WebApplication(config=config)
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
A response adapter failure is logged and then propagated to the web
|
|
532
|
+
server, because a failed response adapter cannot safely convert a
|
|
533
|
+
backend-generated error response.
|
|
534
|
+
|
|
535
|
+
## Version
|
|
536
|
+
|
|
537
|
+
```python
|
|
538
|
+
import basic_web_backend
|
|
539
|
+
|
|
540
|
+
print(basic_web_backend.__version__)
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
## License
|
|
544
|
+
|
|
545
|
+
This project is licensed under the
|
|
546
|
+
[MIT License](LICENSE).
|