yhttp 9.1.0__tar.gz → 9.4.2__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.
- yhttp-9.4.2/PKG-INFO +289 -0
- yhttp-9.4.2/README.md +246 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/setup.py +5 -1
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_applicationcli.py +15 -2
- yhttp-9.4.2/tests/test_logging.py +102 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_request.py +1 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_rewritecli.py +3 -1
- {yhttp-9.1.0 → yhttp-9.4.2}/yhttp/core/application.py +7 -3
- {yhttp-9.1.0 → yhttp-9.4.2}/yhttp/core/cli.py +61 -7
- {yhttp-9.1.0 → yhttp-9.4.2}/yhttp/core/fswatcher.py +2 -1
- {yhttp-9.1.0 → yhttp-9.4.2}/yhttp/core/guard.py +8 -8
- yhttp-9.4.2/yhttp/core/logging.py +25 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/yhttp/core/request.py +15 -0
- yhttp-9.4.2/yhttp/core/version.py +1 -0
- yhttp-9.4.2/yhttp.egg-info/PKG-INFO +289 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/yhttp.egg-info/SOURCES.txt +2 -0
- yhttp-9.1.0/PKG-INFO +0 -157
- yhttp-9.1.0/README.md +0 -118
- yhttp-9.1.0/yhttp/core/version.py +0 -1
- yhttp-9.1.0/yhttp.egg-info/PKG-INFO +0 -157
- {yhttp-9.1.0 → yhttp-9.4.2}/LICENSE +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/setup.cfg +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_application.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_builtincli_serve.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_builtincli_version.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_builtincli_yhttp_version.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_cookie.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_cookieset.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_extension.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_form.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_form_json.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_form_multipart.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_form_urlencoded.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_fswatcher.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_guard.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_guard_body.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_guard_contenttypes.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_guard_query.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_headerset.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_jsonencoding.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_locale.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_middleware.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_multidict.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_multipart.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_querystring.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_responseheaders.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_rewrite.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_routing.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_static.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_statuses.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/tests/test_statushandler.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/yhttp/core/__init__.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/yhttp/core/contenttypes.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/yhttp/core/cookieset.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/yhttp/core/headerset.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/yhttp/core/main.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/yhttp/core/multidict.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/yhttp/core/multipart.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/yhttp/core/response.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/yhttp/core/rewrite.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/yhttp/core/static.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/yhttp/core/statuses.py +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/yhttp.egg-info/dependency_links.txt +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/yhttp.egg-info/entry_points.txt +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/yhttp.egg-info/requires.txt +0 -0
- {yhttp-9.1.0 → yhttp-9.4.2}/yhttp.egg-info/top_level.txt +0 -0
yhttp-9.4.2/PKG-INFO
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: yhttp
|
|
3
|
+
Version: 9.4.2
|
|
4
|
+
Summary: A very micro http framework.
|
|
5
|
+
Home-page: http://github.com/yhttp/yhttp
|
|
6
|
+
Author: Vahid Mardani
|
|
7
|
+
Author-email: vahid.mardani@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Natural Language :: English
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: License :: Other/Proprietary License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Software Development
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
24
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: snam>=1.0.2
|
|
30
|
+
Requires-Dist: easycli<2,>=1.10.1
|
|
31
|
+
Requires-Dist: ujson
|
|
32
|
+
Requires-Dist: inotify-simple
|
|
33
|
+
Dynamic: author
|
|
34
|
+
Dynamic: author-email
|
|
35
|
+
Dynamic: classifier
|
|
36
|
+
Dynamic: description
|
|
37
|
+
Dynamic: description-content-type
|
|
38
|
+
Dynamic: home-page
|
|
39
|
+
Dynamic: license
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
Dynamic: requires-dist
|
|
42
|
+
Dynamic: summary
|
|
43
|
+
|
|
44
|
+
# YHTTP
|
|
45
|
+
|
|
46
|
+
[](https://pypi.org/project/yhttp/)
|
|
47
|
+
[](https://github.com/yhttp/yhttp/actions/workflows/build.yml)
|
|
48
|
+
[](https://coveralls.io/github/yhttp/yhttp?branch=master)
|
|
49
|
+
[](https://yhttp.github.io/yhttp/)
|
|
50
|
+
[](https://www.python.org/)
|
|
51
|
+
|
|
52
|
+
YHTTP is a small, extensible WSGI framework for building HTTP services in
|
|
53
|
+
Python. It provides regex routing, request guards, form parsing, settings,
|
|
54
|
+
lifecycle hooks, static file serving, and a built-in development CLI while
|
|
55
|
+
leaving features such as templates, authentication, localization, and database
|
|
56
|
+
access to focused extensions.
|
|
57
|
+
|
|
58
|
+
- [Documentation](https://yhttp.github.io/yhttp/)
|
|
59
|
+
- [PyPI](https://pypi.org/project/yhttp/)
|
|
60
|
+
- [Source](https://github.com/yhttp/yhttp)
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
YHTTP requires Python 3.10 or newer.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
python -m pip install yhttp
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Quick start
|
|
71
|
+
|
|
72
|
+
Create `hello.py`:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import sys
|
|
76
|
+
|
|
77
|
+
from yhttp.core import Application, text
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
app = Application('0.1.0', 'hello')
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@app.route('/')
|
|
84
|
+
@text
|
|
85
|
+
def get(req):
|
|
86
|
+
return 'Hello, world!'
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == '__main__':
|
|
90
|
+
sys.exit(app.climain())
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
app.ready()
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Start the built-in development server and make a request:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
python hello.py serve --bind 8080
|
|
100
|
+
curl http://localhost:8080/
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The application is WSGI-compatible, so it can also be served by a WSGI server
|
|
104
|
+
such as Gunicorn:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
python -m pip install gunicorn
|
|
108
|
+
gunicorn hello:app
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The handler name selects the HTTP verb when `verb` is not passed explicitly to
|
|
112
|
+
`app.route()`. Routes are regular expressions, and captured groups are passed
|
|
113
|
+
to the handler after `req`.
|
|
114
|
+
|
|
115
|
+
## Features
|
|
116
|
+
|
|
117
|
+
- Regex routes with captured path parameters and explicit verb overrides
|
|
118
|
+
- Strict query-string and request-body validation through guards
|
|
119
|
+
- URL-encoded, multipart, and JSON form parsing
|
|
120
|
+
- Layered settings and application lifecycle hooks
|
|
121
|
+
- Static files, WSGI rewriting, middleware, and status handlers
|
|
122
|
+
- An extensible command-line interface with a development server
|
|
123
|
+
- A small core that can be composed with YHTTP extensions
|
|
124
|
+
|
|
125
|
+
## SSR development
|
|
126
|
+
|
|
127
|
+
YHTTP keeps server-side rendering outside the core. Use an extension such as
|
|
128
|
+
[`yhttp-mako`](https://github.com/yhttp/yhttp-mako) to render Mako templates,
|
|
129
|
+
then compose localization, authentication, assets, and persistence through the
|
|
130
|
+
extensions already used by your application.
|
|
131
|
+
|
|
132
|
+
Install the Mako extension:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
python -m pip install yhttp-mako
|
|
136
|
+
mkdir -p templates makomodules
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
A minimal rendered page consists of an application module and a template:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
# app.py
|
|
143
|
+
from yhttp.core import Application
|
|
144
|
+
from yhttp.ext import mako
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
app = Application('0.1.0', 'pages')
|
|
148
|
+
mako.install(app)
|
|
149
|
+
|
|
150
|
+
app.settings.mako.lookup = 'templates'
|
|
151
|
+
app.settings.mako.modules = 'makomodules'
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@app.route('/')
|
|
155
|
+
@app.template('index.mako')
|
|
156
|
+
def get(req):
|
|
157
|
+
return {'title': 'Hello from YHTTP'}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
app.ready()
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```mako
|
|
164
|
+
<!-- templates/index.mako -->
|
|
165
|
+
<!doctype html>
|
|
166
|
+
<html lang="en">
|
|
167
|
+
<head>
|
|
168
|
+
<meta charset="utf-8">
|
|
169
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
170
|
+
<title>${title | h}</title>
|
|
171
|
+
</head>
|
|
172
|
+
<body>
|
|
173
|
+
<h1>${title | h}</h1>
|
|
174
|
+
</body>
|
|
175
|
+
</html>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Run it with `gunicorn app:app`. The `makomodules` directory stores compiled
|
|
179
|
+
templates and must be writable by the application process.
|
|
180
|
+
|
|
181
|
+
For a maintainable SSR application, keep the rendering path explicit:
|
|
182
|
+
|
|
183
|
+
1. Create the `Application`, install extensions, and merge settings before
|
|
184
|
+
readiness. Import model modules before route modules, and call `app.ready()`
|
|
185
|
+
only after registration is complete; an unimported model or route module is
|
|
186
|
+
inactive.
|
|
187
|
+
2. Keep handlers responsible for request guards, authorization, database
|
|
188
|
+
access, and status responses. A page handler should return a context
|
|
189
|
+
dictionary to `@app.template(...)`, not build HTML itself.
|
|
190
|
+
3. Put shared document structure in inherited Mako layouts. Keep templates
|
|
191
|
+
presentational, reuse template-provided helpers, and escape user-controlled
|
|
192
|
+
values. Render stored HTML unescaped only when the application explicitly
|
|
193
|
+
treats it as trusted.
|
|
194
|
+
4. Localize visible text with the installed translation helpers, preserve
|
|
195
|
+
locale-prefixed links, and test both LTR and RTL output when the application
|
|
196
|
+
supports both directions.
|
|
197
|
+
5. Keep pages useful without JavaScript where practical. Add focused browser
|
|
198
|
+
behavior through the project's existing asset pipeline, and keep endpoint
|
|
199
|
+
calls in browser-side service modules rather than templates.
|
|
200
|
+
6. Extend the nearest bddrest page test. Assert meaningful rendered behavior,
|
|
201
|
+
including routes, localized text, direction-sensitive markup, resolved
|
|
202
|
+
assets, authentication states, and persisted effects relevant to the page.
|
|
203
|
+
|
|
204
|
+
A typical project keeps its composition root, route registration, page
|
|
205
|
+
handlers, templates, browser assets, and page tests separate:
|
|
206
|
+
|
|
207
|
+
```text
|
|
208
|
+
app.py
|
|
209
|
+
models/
|
|
210
|
+
routes.py
|
|
211
|
+
pages.py
|
|
212
|
+
templates/
|
|
213
|
+
www/
|
|
214
|
+
tests/test_pages.py
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Treat this as a responsibility map rather than a required package layout;
|
|
218
|
+
follow the nearest complete feature in an existing application.
|
|
219
|
+
|
|
220
|
+
## Contributing
|
|
221
|
+
|
|
222
|
+
This repository uses
|
|
223
|
+
[`python-makelib`](https://github.com/pylover/python-makelib). Install it first,
|
|
224
|
+
then create and populate the development environment:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
make venv
|
|
228
|
+
source ./activate.sh
|
|
229
|
+
make env
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Run the test suite:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
make test
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Run a focused test or coverage target with `F`:
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
make test F=tests/test_form.py::test_getform_force
|
|
242
|
+
make cover F=tests/test_static.py
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Run all coverage checks or generate the HTML coverage report:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
make cover
|
|
249
|
+
make cover-html
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Lint the project:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
make lint
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Delete the virtual environment with `make venv-delete`.
|
|
259
|
+
|
|
260
|
+
## Documentation
|
|
261
|
+
|
|
262
|
+
Build and test the Sphinx documentation from the repository root:
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
source ./activate.sh
|
|
266
|
+
make doc
|
|
267
|
+
make doctest
|
|
268
|
+
make doclive
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
The equivalent commands from `sphinx/` are `make html`, `make doctest`, and
|
|
272
|
+
`make livehtml`.
|
|
273
|
+
|
|
274
|
+
## Distribution
|
|
275
|
+
|
|
276
|
+
Build the source and wheel distributions in `dist/`:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
make clean
|
|
280
|
+
make sdist
|
|
281
|
+
make wheel
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Publishing is reserved for project maintainers. Maintainers can upload both
|
|
285
|
+
artifacts with `make pypi`.
|
|
286
|
+
|
|
287
|
+
## License
|
|
288
|
+
|
|
289
|
+
YHTTP is released under the [MIT License](LICENSE).
|
yhttp-9.4.2/README.md
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# YHTTP
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/yhttp/)
|
|
4
|
+
[](https://github.com/yhttp/yhttp/actions/workflows/build.yml)
|
|
5
|
+
[](https://coveralls.io/github/yhttp/yhttp?branch=master)
|
|
6
|
+
[](https://yhttp.github.io/yhttp/)
|
|
7
|
+
[](https://www.python.org/)
|
|
8
|
+
|
|
9
|
+
YHTTP is a small, extensible WSGI framework for building HTTP services in
|
|
10
|
+
Python. It provides regex routing, request guards, form parsing, settings,
|
|
11
|
+
lifecycle hooks, static file serving, and a built-in development CLI while
|
|
12
|
+
leaving features such as templates, authentication, localization, and database
|
|
13
|
+
access to focused extensions.
|
|
14
|
+
|
|
15
|
+
- [Documentation](https://yhttp.github.io/yhttp/)
|
|
16
|
+
- [PyPI](https://pypi.org/project/yhttp/)
|
|
17
|
+
- [Source](https://github.com/yhttp/yhttp)
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
YHTTP requires Python 3.10 or newer.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
python -m pip install yhttp
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
Create `hello.py`:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import sys
|
|
33
|
+
|
|
34
|
+
from yhttp.core import Application, text
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
app = Application('0.1.0', 'hello')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.route('/')
|
|
41
|
+
@text
|
|
42
|
+
def get(req):
|
|
43
|
+
return 'Hello, world!'
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == '__main__':
|
|
47
|
+
sys.exit(app.climain())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
app.ready()
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Start the built-in development server and make a request:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python hello.py serve --bind 8080
|
|
57
|
+
curl http://localhost:8080/
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The application is WSGI-compatible, so it can also be served by a WSGI server
|
|
61
|
+
such as Gunicorn:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
python -m pip install gunicorn
|
|
65
|
+
gunicorn hello:app
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The handler name selects the HTTP verb when `verb` is not passed explicitly to
|
|
69
|
+
`app.route()`. Routes are regular expressions, and captured groups are passed
|
|
70
|
+
to the handler after `req`.
|
|
71
|
+
|
|
72
|
+
## Features
|
|
73
|
+
|
|
74
|
+
- Regex routes with captured path parameters and explicit verb overrides
|
|
75
|
+
- Strict query-string and request-body validation through guards
|
|
76
|
+
- URL-encoded, multipart, and JSON form parsing
|
|
77
|
+
- Layered settings and application lifecycle hooks
|
|
78
|
+
- Static files, WSGI rewriting, middleware, and status handlers
|
|
79
|
+
- An extensible command-line interface with a development server
|
|
80
|
+
- A small core that can be composed with YHTTP extensions
|
|
81
|
+
|
|
82
|
+
## SSR development
|
|
83
|
+
|
|
84
|
+
YHTTP keeps server-side rendering outside the core. Use an extension such as
|
|
85
|
+
[`yhttp-mako`](https://github.com/yhttp/yhttp-mako) to render Mako templates,
|
|
86
|
+
then compose localization, authentication, assets, and persistence through the
|
|
87
|
+
extensions already used by your application.
|
|
88
|
+
|
|
89
|
+
Install the Mako extension:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
python -m pip install yhttp-mako
|
|
93
|
+
mkdir -p templates makomodules
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
A minimal rendered page consists of an application module and a template:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
# app.py
|
|
100
|
+
from yhttp.core import Application
|
|
101
|
+
from yhttp.ext import mako
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
app = Application('0.1.0', 'pages')
|
|
105
|
+
mako.install(app)
|
|
106
|
+
|
|
107
|
+
app.settings.mako.lookup = 'templates'
|
|
108
|
+
app.settings.mako.modules = 'makomodules'
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@app.route('/')
|
|
112
|
+
@app.template('index.mako')
|
|
113
|
+
def get(req):
|
|
114
|
+
return {'title': 'Hello from YHTTP'}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
app.ready()
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```mako
|
|
121
|
+
<!-- templates/index.mako -->
|
|
122
|
+
<!doctype html>
|
|
123
|
+
<html lang="en">
|
|
124
|
+
<head>
|
|
125
|
+
<meta charset="utf-8">
|
|
126
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
127
|
+
<title>${title | h}</title>
|
|
128
|
+
</head>
|
|
129
|
+
<body>
|
|
130
|
+
<h1>${title | h}</h1>
|
|
131
|
+
</body>
|
|
132
|
+
</html>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Run it with `gunicorn app:app`. The `makomodules` directory stores compiled
|
|
136
|
+
templates and must be writable by the application process.
|
|
137
|
+
|
|
138
|
+
For a maintainable SSR application, keep the rendering path explicit:
|
|
139
|
+
|
|
140
|
+
1. Create the `Application`, install extensions, and merge settings before
|
|
141
|
+
readiness. Import model modules before route modules, and call `app.ready()`
|
|
142
|
+
only after registration is complete; an unimported model or route module is
|
|
143
|
+
inactive.
|
|
144
|
+
2. Keep handlers responsible for request guards, authorization, database
|
|
145
|
+
access, and status responses. A page handler should return a context
|
|
146
|
+
dictionary to `@app.template(...)`, not build HTML itself.
|
|
147
|
+
3. Put shared document structure in inherited Mako layouts. Keep templates
|
|
148
|
+
presentational, reuse template-provided helpers, and escape user-controlled
|
|
149
|
+
values. Render stored HTML unescaped only when the application explicitly
|
|
150
|
+
treats it as trusted.
|
|
151
|
+
4. Localize visible text with the installed translation helpers, preserve
|
|
152
|
+
locale-prefixed links, and test both LTR and RTL output when the application
|
|
153
|
+
supports both directions.
|
|
154
|
+
5. Keep pages useful without JavaScript where practical. Add focused browser
|
|
155
|
+
behavior through the project's existing asset pipeline, and keep endpoint
|
|
156
|
+
calls in browser-side service modules rather than templates.
|
|
157
|
+
6. Extend the nearest bddrest page test. Assert meaningful rendered behavior,
|
|
158
|
+
including routes, localized text, direction-sensitive markup, resolved
|
|
159
|
+
assets, authentication states, and persisted effects relevant to the page.
|
|
160
|
+
|
|
161
|
+
A typical project keeps its composition root, route registration, page
|
|
162
|
+
handlers, templates, browser assets, and page tests separate:
|
|
163
|
+
|
|
164
|
+
```text
|
|
165
|
+
app.py
|
|
166
|
+
models/
|
|
167
|
+
routes.py
|
|
168
|
+
pages.py
|
|
169
|
+
templates/
|
|
170
|
+
www/
|
|
171
|
+
tests/test_pages.py
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Treat this as a responsibility map rather than a required package layout;
|
|
175
|
+
follow the nearest complete feature in an existing application.
|
|
176
|
+
|
|
177
|
+
## Contributing
|
|
178
|
+
|
|
179
|
+
This repository uses
|
|
180
|
+
[`python-makelib`](https://github.com/pylover/python-makelib). Install it first,
|
|
181
|
+
then create and populate the development environment:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
make venv
|
|
185
|
+
source ./activate.sh
|
|
186
|
+
make env
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Run the test suite:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
make test
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Run a focused test or coverage target with `F`:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
make test F=tests/test_form.py::test_getform_force
|
|
199
|
+
make cover F=tests/test_static.py
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Run all coverage checks or generate the HTML coverage report:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
make cover
|
|
206
|
+
make cover-html
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Lint the project:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
make lint
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Delete the virtual environment with `make venv-delete`.
|
|
216
|
+
|
|
217
|
+
## Documentation
|
|
218
|
+
|
|
219
|
+
Build and test the Sphinx documentation from the repository root:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
source ./activate.sh
|
|
223
|
+
make doc
|
|
224
|
+
make doctest
|
|
225
|
+
make doclive
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The equivalent commands from `sphinx/` are `make html`, `make doctest`, and
|
|
229
|
+
`make livehtml`.
|
|
230
|
+
|
|
231
|
+
## Distribution
|
|
232
|
+
|
|
233
|
+
Build the source and wheel distributions in `dist/`:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
make clean
|
|
237
|
+
make sdist
|
|
238
|
+
make wheel
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Publishing is reserved for project maintainers. Maintainers can upload both
|
|
242
|
+
artifacts with `make pypi`.
|
|
243
|
+
|
|
244
|
+
## License
|
|
245
|
+
|
|
246
|
+
YHTTP is released under the [MIT License](LICENSE).
|
|
@@ -51,7 +51,11 @@ setup(
|
|
|
51
51
|
'Development Status :: 5 - Production/Stable',
|
|
52
52
|
'License :: Other/Proprietary License',
|
|
53
53
|
'Operating System :: OS Independent',
|
|
54
|
-
'Programming Language :: Python :: 3.
|
|
54
|
+
'Programming Language :: Python :: 3.10',
|
|
55
|
+
'Programming Language :: Python :: 3.11',
|
|
56
|
+
'Programming Language :: Python :: 3.12',
|
|
57
|
+
'Programming Language :: Python :: 3.13',
|
|
58
|
+
'Programming Language :: Python :: 3.14',
|
|
55
59
|
'Topic :: Software Development',
|
|
56
60
|
'Topic :: Internet :: WWW/HTTP',
|
|
57
61
|
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
|
|
@@ -53,10 +53,15 @@ foo:
|
|
|
53
53
|
'''
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
def test_applicationcli(mktmpfile):
|
|
56
|
+
def test_applicationcli(mktmpfile, bddcli_bootpatch):
|
|
57
57
|
cliapp = CLIApplication('foo', 'tests.test_applicationcli:app.climain')
|
|
58
58
|
configfile = mktmpfile(content='title: bar')
|
|
59
|
-
|
|
59
|
+
freezetime = \
|
|
60
|
+
'import time_machine;' \
|
|
61
|
+
'time_machine.travel("2012-02-14 16:00:01", tick=False).start()\n'
|
|
62
|
+
|
|
63
|
+
with bddcli_bootpatch(freezetime), Given(cliapp, '--help'):
|
|
64
|
+
assert stderr == ''
|
|
60
65
|
assert status == 0
|
|
61
66
|
|
|
62
67
|
when('foo')
|
|
@@ -68,6 +73,14 @@ def test_applicationcli(mktmpfile):
|
|
|
68
73
|
assert status == 73
|
|
69
74
|
assert stdout == 'bar\n.\n'
|
|
70
75
|
|
|
76
|
+
when(f'--verbose --configuration-file {configfile} foo')
|
|
77
|
+
assert stderr == ''
|
|
78
|
+
assert status == 73
|
|
79
|
+
assert stdout == (
|
|
80
|
+
'2012-02-14 16:00:01.000 INFO yhttp: loading config file: '
|
|
81
|
+
f'{configfile}\nbar\n.\n'
|
|
82
|
+
)
|
|
83
|
+
|
|
71
84
|
when('--directory /tmp foo')
|
|
72
85
|
assert status == 0
|
|
73
86
|
assert stdout == 'foo\n/tmp\n'
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from bddcli import Application as CLIApplication, Given, stdout, status, \
|
|
2
|
+
when, stderr
|
|
3
|
+
from easycli import SubCommand
|
|
4
|
+
|
|
5
|
+
from yhttp.core import Application, logging
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
logger = logging.getlogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Foo(SubCommand):
|
|
12
|
+
__command__ = 'foo'
|
|
13
|
+
|
|
14
|
+
def __call__(self, args):
|
|
15
|
+
logger.debug('foo')
|
|
16
|
+
logger.info('foo')
|
|
17
|
+
logger.warning('foo')
|
|
18
|
+
logger.error('foo')
|
|
19
|
+
logger.critical('foo')
|
|
20
|
+
print(f'verbosity: {args.verbosity}')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
app = Application('0.1.0', 'foo')
|
|
24
|
+
app.cliarguments.append(Foo)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_logging(bddcli_bootpatch):
|
|
28
|
+
cliapp = CLIApplication('foo', f'{__name__}:app.climain')
|
|
29
|
+
freezetime = \
|
|
30
|
+
'import time_machine;' \
|
|
31
|
+
'time_machine.travel("2012-02-14 16:00:01", tick=False).start()\n'
|
|
32
|
+
|
|
33
|
+
with bddcli_bootpatch(freezetime), Given(cliapp, 'foo'):
|
|
34
|
+
assert stderr == ''
|
|
35
|
+
assert status == 0
|
|
36
|
+
assert stdout == (
|
|
37
|
+
'2012-02-14 16:00:01.000 WARNING tests.test_logging: foo\n'
|
|
38
|
+
'2012-02-14 16:00:01.000 ERROR tests.test_logging: foo\n'
|
|
39
|
+
'2012-02-14 16:00:01.000 CRITICAL tests.test_logging: foo\n'
|
|
40
|
+
'verbosity: 30\n'
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
when('-v foo')
|
|
44
|
+
assert stderr == ''
|
|
45
|
+
assert status == 0
|
|
46
|
+
assert stdout == (
|
|
47
|
+
'2012-02-14 16:00:01.000 INFO tests.test_logging: foo\n'
|
|
48
|
+
'2012-02-14 16:00:01.000 WARNING tests.test_logging: foo\n'
|
|
49
|
+
'2012-02-14 16:00:01.000 ERROR tests.test_logging: foo\n'
|
|
50
|
+
'2012-02-14 16:00:01.000 CRITICAL tests.test_logging: foo\n'
|
|
51
|
+
'verbosity: 20\n'
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
when('-vv foo')
|
|
55
|
+
assert stderr == ''
|
|
56
|
+
assert status == 0
|
|
57
|
+
assert stdout == (
|
|
58
|
+
'2012-02-14 16:00:01.000 DEBUG tests.test_logging: foo\n'
|
|
59
|
+
'2012-02-14 16:00:01.000 INFO tests.test_logging: foo\n'
|
|
60
|
+
'2012-02-14 16:00:01.000 WARNING tests.test_logging: foo\n'
|
|
61
|
+
'2012-02-14 16:00:01.000 ERROR tests.test_logging: foo\n'
|
|
62
|
+
'2012-02-14 16:00:01.000 CRITICAL tests.test_logging: foo\n'
|
|
63
|
+
'verbosity: 10\n'
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
when('-q foo')
|
|
67
|
+
assert stderr == ''
|
|
68
|
+
assert status == 0
|
|
69
|
+
assert stdout == (
|
|
70
|
+
'2012-02-14 16:00:01.000 ERROR tests.test_logging: foo\n'
|
|
71
|
+
'2012-02-14 16:00:01.000 CRITICAL tests.test_logging: foo\n'
|
|
72
|
+
'verbosity: 40\n'
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
when('-qq foo')
|
|
76
|
+
assert stderr == ''
|
|
77
|
+
assert status == 0
|
|
78
|
+
assert stdout == (
|
|
79
|
+
'2012-02-14 16:00:01.000 CRITICAL tests.test_logging: foo\n'
|
|
80
|
+
'verbosity: 50\n'
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
when('-qqq foo')
|
|
84
|
+
assert stderr == ''
|
|
85
|
+
assert status == 0
|
|
86
|
+
assert stdout == 'verbosity: 60\n'
|
|
87
|
+
|
|
88
|
+
when('-vvv foo')
|
|
89
|
+
assert stderr.endswith(
|
|
90
|
+
'foo: error: option -v only allowed up to twice\n'
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
when('-qqqq foo')
|
|
94
|
+
assert stderr.endswith(
|
|
95
|
+
'foo: error: option -q only allowed up to 3 times\n'
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
when('-q -v foo')
|
|
99
|
+
assert stderr.endswith(
|
|
100
|
+
'foo: error: argument -v/--verbose: not allowed with argument '
|
|
101
|
+
'-q/--quiet\n'
|
|
102
|
+
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import tempfile
|
|
2
2
|
|
|
3
|
-
from bddcli import Application as CLIApplication, Given, stdout, status,
|
|
3
|
+
from bddcli import Application as CLIApplication, Given, stdout, status, \
|
|
4
|
+
when, stderr
|
|
4
5
|
from easycli import SubCommand
|
|
5
6
|
|
|
6
7
|
from yhttp.core import Rewrite
|
|
@@ -36,5 +37,6 @@ def test_rewritecli_default():
|
|
|
36
37
|
f.write(b'title: bar')
|
|
37
38
|
f.flush()
|
|
38
39
|
when(f'--configuration-file {f.name} foo')
|
|
40
|
+
assert stderr == ''
|
|
39
41
|
assert status == 73
|
|
40
42
|
assert stdout == 'bar\n'
|