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.
Files changed (45) hide show
  1. lema_basic_web_backend-0.1.0/LICENSE +21 -0
  2. lema_basic_web_backend-0.1.0/PKG-INFO +546 -0
  3. lema_basic_web_backend-0.1.0/README.md +517 -0
  4. lema_basic_web_backend-0.1.0/pyproject.toml +66 -0
  5. lema_basic_web_backend-0.1.0/setup.cfg +4 -0
  6. lema_basic_web_backend-0.1.0/src/basic_web_backend/__init__.py +86 -0
  7. lema_basic_web_backend-0.1.0/src/basic_web_backend/adapters.py +39 -0
  8. lema_basic_web_backend-0.1.0/src/basic_web_backend/application.py +217 -0
  9. lema_basic_web_backend-0.1.0/src/basic_web_backend/config.py +214 -0
  10. lema_basic_web_backend-0.1.0/src/basic_web_backend/exceptions.py +176 -0
  11. lema_basic_web_backend-0.1.0/src/basic_web_backend/logging_config.py +61 -0
  12. lema_basic_web_backend-0.1.0/src/basic_web_backend/multipart.py +109 -0
  13. lema_basic_web_backend-0.1.0/src/basic_web_backend/request.py +180 -0
  14. lema_basic_web_backend-0.1.0/src/basic_web_backend/response.py +197 -0
  15. lema_basic_web_backend-0.1.0/src/basic_web_backend/routing.py +343 -0
  16. lema_basic_web_backend-0.1.0/src/basic_web_backend/static.py +29 -0
  17. lema_basic_web_backend-0.1.0/src/basic_web_backend/template/__init__.py +3 -0
  18. lema_basic_web_backend-0.1.0/src/basic_web_backend/template/environment.py +110 -0
  19. lema_basic_web_backend-0.1.0/src/basic_web_backend/template/evaluator.py +114 -0
  20. lema_basic_web_backend-0.1.0/src/basic_web_backend/template/lexer.py +127 -0
  21. lema_basic_web_backend-0.1.0/src/basic_web_backend/template/nodes.py +34 -0
  22. lema_basic_web_backend-0.1.0/src/basic_web_backend/template/parser.py +221 -0
  23. lema_basic_web_backend-0.1.0/src/lema_basic_web_backend.egg-info/PKG-INFO +546 -0
  24. lema_basic_web_backend-0.1.0/src/lema_basic_web_backend.egg-info/SOURCES.txt +43 -0
  25. lema_basic_web_backend-0.1.0/src/lema_basic_web_backend.egg-info/dependency_links.txt +1 -0
  26. lema_basic_web_backend-0.1.0/src/lema_basic_web_backend.egg-info/requires.txt +9 -0
  27. lema_basic_web_backend-0.1.0/src/lema_basic_web_backend.egg-info/top_level.txt +1 -0
  28. lema_basic_web_backend-0.1.0/tests/test_adapters.py +166 -0
  29. lema_basic_web_backend-0.1.0/tests/test_application.py +1312 -0
  30. lema_basic_web_backend-0.1.0/tests/test_application_integration.py +208 -0
  31. lema_basic_web_backend-0.1.0/tests/test_config.py +176 -0
  32. lema_basic_web_backend-0.1.0/tests/test_exceptions.py +174 -0
  33. lema_basic_web_backend-0.1.0/tests/test_logging_config.py +53 -0
  34. lema_basic_web_backend-0.1.0/tests/test_multipart.py +113 -0
  35. lema_basic_web_backend-0.1.0/tests/test_public_api.py +147 -0
  36. lema_basic_web_backend-0.1.0/tests/test_request.py +558 -0
  37. lema_basic_web_backend-0.1.0/tests/test_response.py +286 -0
  38. lema_basic_web_backend-0.1.0/tests/test_routing.py +412 -0
  39. lema_basic_web_backend-0.1.0/tests/test_static.py +248 -0
  40. lema_basic_web_backend-0.1.0/tests/test_template_enviroment.py +345 -0
  41. lema_basic_web_backend-0.1.0/tests/test_template_evalutor.py +422 -0
  42. lema_basic_web_backend-0.1.0/tests/test_template_integration.py +89 -0
  43. lema_basic_web_backend-0.1.0/tests/test_template_lexer.py +131 -0
  44. lema_basic_web_backend-0.1.0/tests/test_template_nodes.py +96 -0
  45. 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).