chameleon_flask 0.0.1__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.
- chameleon_flask-0.0.1/LICENSE +21 -0
- chameleon_flask-0.0.1/PKG-INFO +101 -0
- chameleon_flask-0.0.1/README.md +79 -0
- chameleon_flask-0.0.1/chameleon_flask/__init__.py +10 -0
- chameleon_flask-0.0.1/chameleon_flask/engine.py +130 -0
- chameleon_flask-0.0.1/chameleon_flask/exceptions.py +13 -0
- chameleon_flask-0.0.1/chameleon_flask.egg-info/PKG-INFO +101 -0
- chameleon_flask-0.0.1/chameleon_flask.egg-info/SOURCES.txt +14 -0
- chameleon_flask-0.0.1/chameleon_flask.egg-info/dependency_links.txt +1 -0
- chameleon_flask-0.0.1/chameleon_flask.egg-info/requires.txt +2 -0
- chameleon_flask-0.0.1/chameleon_flask.egg-info/top_level.txt +1 -0
- chameleon_flask-0.0.1/pyproject.toml +35 -0
- chameleon_flask-0.0.1/setup.cfg +4 -0
- chameleon_flask-0.0.1/tests/test_init.py +29 -0
- chameleon_flask-0.0.1/tests/test_not_found.py +56 -0
- chameleon_flask-0.0.1/tests/test_render.py +110 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 Michael Kennedy
|
|
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,101 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: chameleon_flask
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Adds integration of the Chameleon template language to Flask and Quart.
|
|
5
|
+
Author-email: Michael Kennedy <michael@talkpython.fm>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mikeckennedy/chameleon-flask
|
|
8
|
+
Keywords: Flask,Chameleon,template,integration
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: flask[async]
|
|
21
|
+
Requires-Dist: Chameleon
|
|
22
|
+
|
|
23
|
+
# chameleon-flask
|
|
24
|
+
|
|
25
|
+
Adds integration of the Chameleon template language to Flask and Quart.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
Simply `pip install chameleon_flask`.
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
This is easy to use. Just create a folder within your web app to hold the templates such as:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
├── app.py
|
|
37
|
+
├── views.py
|
|
38
|
+
│
|
|
39
|
+
├── templates
|
|
40
|
+
│ ├── home
|
|
41
|
+
│ │ └── index.pt
|
|
42
|
+
│ └── shared
|
|
43
|
+
│ └── layout.pt
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
In the app startup, tell the library about the folder you wish to use:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import os
|
|
51
|
+
from pathlib import Path
|
|
52
|
+
import chameleon_flask
|
|
53
|
+
|
|
54
|
+
dev_mode = True
|
|
55
|
+
|
|
56
|
+
BASE_DIR = Path(__file__).resolve().parent
|
|
57
|
+
template_folder = str(BASE_DIR / 'templates')
|
|
58
|
+
chameleon_flask.global_init(template_folder, auto_reload=dev_mode)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Then just decorate the Flask or Quart view methods (works on sync and async methods):
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
@app.get('/async')
|
|
65
|
+
@chameleon_flask.template('async.pt')
|
|
66
|
+
async def async_world():
|
|
67
|
+
await asyncio.sleep(.01)
|
|
68
|
+
return {'message': "Let's go async Chameleon!"}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The view method should return a `dict` to be passed as variables/values to the template.
|
|
72
|
+
|
|
73
|
+
If a `flask.Response` is returned, the template is skipped and the response along with status_code and
|
|
74
|
+
other values is directly passed through. This is common for redirects and error responses not meant
|
|
75
|
+
for this page template. Otherwise the dictionary is used to render `async.pt` in this example.
|
|
76
|
+
|
|
77
|
+
## Friendly 404s and errors
|
|
78
|
+
|
|
79
|
+
A common technique for user-friendly sites is to use a [custom HTML page for 404 responses](http://www.instantshift.com/2019/10/16/user-friendly-404-pages/).
|
|
80
|
+
This library has support for friendly 404 pages using the `chameleon_flask.not_found()` function.
|
|
81
|
+
|
|
82
|
+
Here's an example:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
@app.get('/catalog/item/{item_id}')
|
|
86
|
+
@chameleon_flask.template('catalog/item.pt')
|
|
87
|
+
async def item(item_id: int):
|
|
88
|
+
item = service.get_item_by_id(item_id)
|
|
89
|
+
if not item:
|
|
90
|
+
chameleon_flask.not_found()
|
|
91
|
+
|
|
92
|
+
return item.dict()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
This will render a 404 response with using the template file `templates/errors/404.pt`.
|
|
96
|
+
You can specify another template to use for the response, but it's not required.
|
|
97
|
+
|
|
98
|
+
## An example
|
|
99
|
+
|
|
100
|
+
See the `example/example_app.py` file for a working example to play with.
|
|
101
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# chameleon-flask
|
|
2
|
+
|
|
3
|
+
Adds integration of the Chameleon template language to Flask and Quart.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Simply `pip install chameleon_flask`.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
This is easy to use. Just create a folder within your web app to hold the templates such as:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
├── app.py
|
|
15
|
+
├── views.py
|
|
16
|
+
│
|
|
17
|
+
├── templates
|
|
18
|
+
│ ├── home
|
|
19
|
+
│ │ └── index.pt
|
|
20
|
+
│ └── shared
|
|
21
|
+
│ └── layout.pt
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
In the app startup, tell the library about the folder you wish to use:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import os
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
import chameleon_flask
|
|
31
|
+
|
|
32
|
+
dev_mode = True
|
|
33
|
+
|
|
34
|
+
BASE_DIR = Path(__file__).resolve().parent
|
|
35
|
+
template_folder = str(BASE_DIR / 'templates')
|
|
36
|
+
chameleon_flask.global_init(template_folder, auto_reload=dev_mode)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Then just decorate the Flask or Quart view methods (works on sync and async methods):
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
@app.get('/async')
|
|
43
|
+
@chameleon_flask.template('async.pt')
|
|
44
|
+
async def async_world():
|
|
45
|
+
await asyncio.sleep(.01)
|
|
46
|
+
return {'message': "Let's go async Chameleon!"}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The view method should return a `dict` to be passed as variables/values to the template.
|
|
50
|
+
|
|
51
|
+
If a `flask.Response` is returned, the template is skipped and the response along with status_code and
|
|
52
|
+
other values is directly passed through. This is common for redirects and error responses not meant
|
|
53
|
+
for this page template. Otherwise the dictionary is used to render `async.pt` in this example.
|
|
54
|
+
|
|
55
|
+
## Friendly 404s and errors
|
|
56
|
+
|
|
57
|
+
A common technique for user-friendly sites is to use a [custom HTML page for 404 responses](http://www.instantshift.com/2019/10/16/user-friendly-404-pages/).
|
|
58
|
+
This library has support for friendly 404 pages using the `chameleon_flask.not_found()` function.
|
|
59
|
+
|
|
60
|
+
Here's an example:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
@app.get('/catalog/item/{item_id}')
|
|
64
|
+
@chameleon_flask.template('catalog/item.pt')
|
|
65
|
+
async def item(item_id: int):
|
|
66
|
+
item = service.get_item_by_id(item_id)
|
|
67
|
+
if not item:
|
|
68
|
+
chameleon_flask.not_found()
|
|
69
|
+
|
|
70
|
+
return item.dict()
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This will render a 404 response with using the template file `templates/errors/404.pt`.
|
|
74
|
+
You can specify another template to use for the response, but it's not required.
|
|
75
|
+
|
|
76
|
+
## An example
|
|
77
|
+
|
|
78
|
+
See the `example/example_app.py` file for a working example to play with.
|
|
79
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""chameleon-flask - Adds integration of the Chameleon template language to Flask and Quart."""
|
|
2
|
+
|
|
3
|
+
__version__ = '0.0.1'
|
|
4
|
+
__author__ = 'Michael Kennedy <michael@talkpython.fm>'
|
|
5
|
+
__all__ = ['template', 'global_init', 'not_found', 'response', ]
|
|
6
|
+
|
|
7
|
+
from .engine import global_init
|
|
8
|
+
from .engine import not_found
|
|
9
|
+
from .engine import response
|
|
10
|
+
from .engine import template
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import os
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from typing import Optional, Union, Callable
|
|
5
|
+
|
|
6
|
+
import flask
|
|
7
|
+
from chameleon import PageTemplateLoader, PageTemplate
|
|
8
|
+
|
|
9
|
+
from chameleon_flask.exceptions import FlaskChameleonException, FlaskChameleonNotFoundException
|
|
10
|
+
|
|
11
|
+
__templates: Optional[PageTemplateLoader] = None
|
|
12
|
+
template_path: Optional[str] = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def global_init(template_folder: str, auto_reload=False, cache_init=True):
|
|
16
|
+
global __templates, template_path
|
|
17
|
+
|
|
18
|
+
if __templates and cache_init:
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
if not template_folder:
|
|
22
|
+
msg = 'The template_folder must be specified.'
|
|
23
|
+
raise FlaskChameleonException(msg)
|
|
24
|
+
|
|
25
|
+
if not os.path.isdir(template_folder):
|
|
26
|
+
msg = f"The specified template folder must be a folder, it's not: {template_folder}"
|
|
27
|
+
raise FlaskChameleonException(msg)
|
|
28
|
+
|
|
29
|
+
template_path = template_folder
|
|
30
|
+
__templates = PageTemplateLoader(template_folder, auto_reload=auto_reload)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def clear():
|
|
34
|
+
global __templates, template_path
|
|
35
|
+
__templates = None
|
|
36
|
+
template_path = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def render(template_file: str, **template_data: dict) -> str:
|
|
40
|
+
if not __templates:
|
|
41
|
+
raise FlaskChameleonException('You must call global_init() before rendering templates.')
|
|
42
|
+
|
|
43
|
+
page: PageTemplate = __templates[template_file]
|
|
44
|
+
return page.render(encoding='utf-8', **template_data)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def response(template_file: str, content_type='text/html', status_code=200, **template_data) -> flask.Response:
|
|
48
|
+
html = render(template_file, **template_data)
|
|
49
|
+
return flask.Response(response=html, content_type=content_type, status=status_code)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def template(template_file: Optional[Union[Callable, str]] = None, mimetype: str = 'text/html'):
|
|
53
|
+
"""
|
|
54
|
+
Decorate a FastAPI view method to render an HTML response.
|
|
55
|
+
|
|
56
|
+
:param str template_file: Optional, the Chameleon template file (path relative to template folder, *.pt).
|
|
57
|
+
:param str mimetype: The mimetype response (defaults to text/html).
|
|
58
|
+
:return: Decorator to be consumed by FastAPI
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
wrapped_function = None
|
|
62
|
+
if callable(template_file):
|
|
63
|
+
wrapped_function = template_file
|
|
64
|
+
template_file = None
|
|
65
|
+
|
|
66
|
+
def response_inner(f):
|
|
67
|
+
nonlocal template_file
|
|
68
|
+
global template_path
|
|
69
|
+
|
|
70
|
+
if not template_path:
|
|
71
|
+
template_path = 'templates'
|
|
72
|
+
# raise FlaskChameleonException("Cannot continue: fastapi_chameleon.global_init() has not been called.")
|
|
73
|
+
|
|
74
|
+
if not template_file:
|
|
75
|
+
# Use the default naming scheme: template_folder/module_name/function_name.pt
|
|
76
|
+
module = f.__module__
|
|
77
|
+
if '.' in module:
|
|
78
|
+
module = module.split('.')[-1]
|
|
79
|
+
view = f.__name__
|
|
80
|
+
template_file = f'{module}/{view}.html'
|
|
81
|
+
|
|
82
|
+
if not os.path.exists(os.path.join(template_path, template_file)):
|
|
83
|
+
template_file = f'{module}/{view}.pt'
|
|
84
|
+
|
|
85
|
+
@wraps(f)
|
|
86
|
+
def sync_view_method(*args, **kwargs) -> flask.Response:
|
|
87
|
+
try:
|
|
88
|
+
response_val = f(*args, **kwargs)
|
|
89
|
+
return __render_response(template_file, response_val, mimetype)
|
|
90
|
+
except FlaskChameleonNotFoundException as nfe:
|
|
91
|
+
return __render_response(nfe.template_file, {}, 'text/html', 404)
|
|
92
|
+
|
|
93
|
+
@wraps(f)
|
|
94
|
+
async def async_view_method(*args, **kwargs) -> flask.Response:
|
|
95
|
+
try:
|
|
96
|
+
response_val = await f(*args, **kwargs)
|
|
97
|
+
return __render_response(template_file, response_val, mimetype)
|
|
98
|
+
except FlaskChameleonNotFoundException as nfe:
|
|
99
|
+
return __render_response(nfe.template_file, {}, 'text/html', 404)
|
|
100
|
+
|
|
101
|
+
if inspect.iscoroutinefunction(f):
|
|
102
|
+
return async_view_method
|
|
103
|
+
else:
|
|
104
|
+
return sync_view_method
|
|
105
|
+
|
|
106
|
+
return response_inner(wrapped_function) if wrapped_function else response_inner
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def __render_response(template_file, response_val, content_type, status_code: int = 200) -> flask.Response:
|
|
110
|
+
# source skip: assign-if-exp
|
|
111
|
+
if isinstance(response_val, flask.Response):
|
|
112
|
+
return response_val
|
|
113
|
+
|
|
114
|
+
if template_file and not isinstance(response_val, dict):
|
|
115
|
+
msg = f'Invalid return type {type(response_val)}, we expected a dict or fastapi.Response as the return value.'
|
|
116
|
+
raise FlaskChameleonException(msg)
|
|
117
|
+
|
|
118
|
+
model = response_val
|
|
119
|
+
|
|
120
|
+
html = render(template_file, **model)
|
|
121
|
+
return flask.Response(response=html, content_type=content_type, status=status_code)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def not_found(four04template_file: str = 'errors/404.pt'):
|
|
125
|
+
msg = 'The URL resulted in a 404 response.'
|
|
126
|
+
|
|
127
|
+
if four04template_file and four04template_file.strip():
|
|
128
|
+
raise FlaskChameleonNotFoundException(msg, four04template_file)
|
|
129
|
+
else:
|
|
130
|
+
raise FlaskChameleonNotFoundException(msg)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FlaskChameleonException(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FlaskChameleonNotFoundException(FlaskChameleonException):
|
|
9
|
+
def __init__(self, message: Optional[str] = None, four04template_file: str = 'errors/404.pt'):
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
|
|
12
|
+
self.template_file: str = four04template_file
|
|
13
|
+
self.message: Optional[str] = message
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: chameleon_flask
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Adds integration of the Chameleon template language to Flask and Quart.
|
|
5
|
+
Author-email: Michael Kennedy <michael@talkpython.fm>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mikeckennedy/chameleon-flask
|
|
8
|
+
Keywords: Flask,Chameleon,template,integration
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: flask[async]
|
|
21
|
+
Requires-Dist: Chameleon
|
|
22
|
+
|
|
23
|
+
# chameleon-flask
|
|
24
|
+
|
|
25
|
+
Adds integration of the Chameleon template language to Flask and Quart.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
Simply `pip install chameleon_flask`.
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
This is easy to use. Just create a folder within your web app to hold the templates such as:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
├── app.py
|
|
37
|
+
├── views.py
|
|
38
|
+
│
|
|
39
|
+
├── templates
|
|
40
|
+
│ ├── home
|
|
41
|
+
│ │ └── index.pt
|
|
42
|
+
│ └── shared
|
|
43
|
+
│ └── layout.pt
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
In the app startup, tell the library about the folder you wish to use:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import os
|
|
51
|
+
from pathlib import Path
|
|
52
|
+
import chameleon_flask
|
|
53
|
+
|
|
54
|
+
dev_mode = True
|
|
55
|
+
|
|
56
|
+
BASE_DIR = Path(__file__).resolve().parent
|
|
57
|
+
template_folder = str(BASE_DIR / 'templates')
|
|
58
|
+
chameleon_flask.global_init(template_folder, auto_reload=dev_mode)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Then just decorate the Flask or Quart view methods (works on sync and async methods):
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
@app.get('/async')
|
|
65
|
+
@chameleon_flask.template('async.pt')
|
|
66
|
+
async def async_world():
|
|
67
|
+
await asyncio.sleep(.01)
|
|
68
|
+
return {'message': "Let's go async Chameleon!"}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The view method should return a `dict` to be passed as variables/values to the template.
|
|
72
|
+
|
|
73
|
+
If a `flask.Response` is returned, the template is skipped and the response along with status_code and
|
|
74
|
+
other values is directly passed through. This is common for redirects and error responses not meant
|
|
75
|
+
for this page template. Otherwise the dictionary is used to render `async.pt` in this example.
|
|
76
|
+
|
|
77
|
+
## Friendly 404s and errors
|
|
78
|
+
|
|
79
|
+
A common technique for user-friendly sites is to use a [custom HTML page for 404 responses](http://www.instantshift.com/2019/10/16/user-friendly-404-pages/).
|
|
80
|
+
This library has support for friendly 404 pages using the `chameleon_flask.not_found()` function.
|
|
81
|
+
|
|
82
|
+
Here's an example:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
@app.get('/catalog/item/{item_id}')
|
|
86
|
+
@chameleon_flask.template('catalog/item.pt')
|
|
87
|
+
async def item(item_id: int):
|
|
88
|
+
item = service.get_item_by_id(item_id)
|
|
89
|
+
if not item:
|
|
90
|
+
chameleon_flask.not_found()
|
|
91
|
+
|
|
92
|
+
return item.dict()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
This will render a 404 response with using the template file `templates/errors/404.pt`.
|
|
96
|
+
You can specify another template to use for the response, but it's not required.
|
|
97
|
+
|
|
98
|
+
## An example
|
|
99
|
+
|
|
100
|
+
See the `example/example_app.py` file for a working example to play with.
|
|
101
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
chameleon_flask/__init__.py
|
|
5
|
+
chameleon_flask/engine.py
|
|
6
|
+
chameleon_flask/exceptions.py
|
|
7
|
+
chameleon_flask.egg-info/PKG-INFO
|
|
8
|
+
chameleon_flask.egg-info/SOURCES.txt
|
|
9
|
+
chameleon_flask.egg-info/dependency_links.txt
|
|
10
|
+
chameleon_flask.egg-info/requires.txt
|
|
11
|
+
chameleon_flask.egg-info/top_level.txt
|
|
12
|
+
tests/test_init.py
|
|
13
|
+
tests/test_not_found.py
|
|
14
|
+
tests/test_render.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
chameleon_flask
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "chameleon_flask"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Adds integration of the Chameleon template language to Flask and Quart."
|
|
9
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Michael Kennedy", email = "michael@talkpython.fm" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["Flask", "Chameleon", "template", "integration"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13"
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"flask[async]",
|
|
28
|
+
"Chameleon"
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
"Homepage" = "https://github.com/mikeckennedy/chameleon-flask"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools]
|
|
35
|
+
packages = ["chameleon_flask"]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
import chameleon_flask as fc
|
|
4
|
+
from chameleon_flask.exceptions import FlaskChameleonException
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_cannot_decorate_with_missing_init():
|
|
8
|
+
fc.engine.clear()
|
|
9
|
+
|
|
10
|
+
with pytest.raises(FlaskChameleonException):
|
|
11
|
+
|
|
12
|
+
@fc.template('home/index.pt')
|
|
13
|
+
def view_method(a, b, c):
|
|
14
|
+
return {'a': a, 'b': b, 'c': c}
|
|
15
|
+
|
|
16
|
+
view_method(1, 2, 3)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_can_call_init_with_good_path(test_templates_path):
|
|
20
|
+
fc.global_init(str(test_templates_path), cache_init=False)
|
|
21
|
+
|
|
22
|
+
# Clear paths to no effect future tests
|
|
23
|
+
fc.engine.clear()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_cannot_call_init_with_bad_path(test_templates_path):
|
|
27
|
+
bad_path = test_templates_path / 'missing'
|
|
28
|
+
with pytest.raises(Exception):
|
|
29
|
+
fc.global_init(str(bad_path), cache_init=False)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import flask
|
|
4
|
+
|
|
5
|
+
import chameleon_flask
|
|
6
|
+
import chameleon_flask as fc
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# setup_global_template - needed as pytest mix-in.
|
|
10
|
+
# noinspection PyUnusedLocal
|
|
11
|
+
def test_friendly_404_sync_method(setup_global_template):
|
|
12
|
+
@fc.template('home/index.pt')
|
|
13
|
+
def view_method(a, b, c):
|
|
14
|
+
chameleon_flask.not_found()
|
|
15
|
+
return {'a': a, 'b': b, 'c': c}
|
|
16
|
+
|
|
17
|
+
resp = view_method(1, 2, 3)
|
|
18
|
+
html = response_to_html(resp)
|
|
19
|
+
assert isinstance(resp, flask.Response)
|
|
20
|
+
assert resp.status_code == 404
|
|
21
|
+
assert '<h1>This is a pretty 404 page.</h1>' in html
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# setup_global_template - needed as pytest mix-in.
|
|
25
|
+
# noinspection PyUnusedLocal
|
|
26
|
+
def test_friendly_404_custom_template_sync_method(setup_global_template):
|
|
27
|
+
@fc.template('home/index.pt')
|
|
28
|
+
def view_method(a, b, c):
|
|
29
|
+
chameleon_flask.not_found(four04template_file='errors/other_error_page.pt')
|
|
30
|
+
return {'a': a, 'b': b, 'c': c}
|
|
31
|
+
|
|
32
|
+
resp = view_method(1, 2, 3)
|
|
33
|
+
html = response_to_html(resp)
|
|
34
|
+
assert isinstance(resp, flask.Response)
|
|
35
|
+
assert resp.status_code == 404
|
|
36
|
+
assert '<h1>Another pretty 404 page.</h1>' in html
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# setup_global_template - needed as pytest mix-in.
|
|
40
|
+
# noinspection PyUnusedLocal
|
|
41
|
+
def test_friendly_404_async_method(setup_global_template):
|
|
42
|
+
@fc.template('home/index.pt')
|
|
43
|
+
async def view_method(a, b, c) -> flask.Response:
|
|
44
|
+
chameleon_flask.not_found()
|
|
45
|
+
return {'a': a, 'b': b, 'c': c}
|
|
46
|
+
|
|
47
|
+
resp = asyncio.run(view_method(1, 2, 3))
|
|
48
|
+
html = response_to_html(resp)
|
|
49
|
+
assert isinstance(resp, flask.Response)
|
|
50
|
+
assert resp.status_code == 404
|
|
51
|
+
assert '<h1>This is a pretty 404 page.</h1>' in html
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def response_to_html(response: flask.Response) -> str:
|
|
55
|
+
# noinspection PyUnresolvedReferences
|
|
56
|
+
return response.response[0].decode('utf-8')
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import flask
|
|
4
|
+
# noinspection PyPackageRequirements
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
import chameleon_flask as fc
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# setup_global_template - needed as pytest mix-in.
|
|
11
|
+
# noinspection PyUnusedLocal
|
|
12
|
+
def test_cannot_decorate_missing_template(setup_global_template):
|
|
13
|
+
with pytest.raises(ValueError):
|
|
14
|
+
|
|
15
|
+
@fc.template('home/missing.pt')
|
|
16
|
+
def view_method():
|
|
17
|
+
return {}
|
|
18
|
+
|
|
19
|
+
view_method()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# setup_global_template - needed as pytest mix-in.
|
|
23
|
+
# noinspection PyUnusedLocal
|
|
24
|
+
def test_requires_template_for_default_name(setup_global_template):
|
|
25
|
+
with pytest.raises(ValueError):
|
|
26
|
+
|
|
27
|
+
@fc.template(None)
|
|
28
|
+
def view_method():
|
|
29
|
+
return {}
|
|
30
|
+
|
|
31
|
+
view_method()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# setup_global_template - needed as pytest mix-in.
|
|
35
|
+
# noinspection PyUnusedLocal
|
|
36
|
+
def test_default_template_name_pt(setup_global_template):
|
|
37
|
+
@fc.template()
|
|
38
|
+
def index(a, b, c):
|
|
39
|
+
return {'a': a, 'b': b, 'c': c, 'world': 'WORLD'}
|
|
40
|
+
|
|
41
|
+
resp = index(1, 2, 3)
|
|
42
|
+
assert isinstance(resp, flask.Response)
|
|
43
|
+
assert resp.status_code == 200
|
|
44
|
+
html = response_to_html(resp)
|
|
45
|
+
assert '<h1>Hello default WORLD!</h1>' in html
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# setup_global_template - needed as pytest mix-in.
|
|
49
|
+
# noinspection PyUnusedLocal
|
|
50
|
+
def test_default_template_name_no_parentheses(setup_global_template):
|
|
51
|
+
@fc.template
|
|
52
|
+
def index(a, b, c):
|
|
53
|
+
return {'a': a, 'b': b, 'c': c, 'world': 'WORLD'}
|
|
54
|
+
|
|
55
|
+
resp = index(1, 2, 3)
|
|
56
|
+
assert isinstance(resp, flask.Response)
|
|
57
|
+
assert resp.status_code == 200
|
|
58
|
+
html = response_to_html(resp)
|
|
59
|
+
assert '<h1>Hello default WORLD!</h1>' in html
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_default_template_name_html(setup_global_template):
|
|
63
|
+
@fc.template()
|
|
64
|
+
def details(a, b, c):
|
|
65
|
+
return {'a': a, 'b': b, 'c': c, 'world': 'WORLD'}
|
|
66
|
+
|
|
67
|
+
resp = details(1, 2, 3)
|
|
68
|
+
assert isinstance(resp, flask.Response)
|
|
69
|
+
assert resp.status_code == 200
|
|
70
|
+
html = response_to_html(resp)
|
|
71
|
+
assert '<h1>Hello default WORLD!</h1>' in html
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# setup_global_template - needed as pytest mix-in.
|
|
75
|
+
# noinspection PyUnusedLocal
|
|
76
|
+
def test_can_decorate_dict_sync_method(setup_global_template):
|
|
77
|
+
@fc.template('home/index.pt')
|
|
78
|
+
def view_method(a, b, c):
|
|
79
|
+
return {'a': a, 'b': b, 'c': c}
|
|
80
|
+
|
|
81
|
+
resp = view_method(1, 2, 3)
|
|
82
|
+
assert isinstance(resp, flask.Response)
|
|
83
|
+
assert resp.status_code == 200
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_can_decorate_dict_async_method(setup_global_template):
|
|
87
|
+
@fc.template('home/index.pt')
|
|
88
|
+
async def view_method(a, b, c):
|
|
89
|
+
return {'a': a, 'b': b, 'c': c}
|
|
90
|
+
|
|
91
|
+
resp = asyncio.run(view_method(1, 2, 3))
|
|
92
|
+
assert isinstance(resp, flask.Response)
|
|
93
|
+
assert resp.status_code == 200
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_direct_response_pass_through():
|
|
97
|
+
@fc.template('home/index.pt')
|
|
98
|
+
def view_method(a, b, c):
|
|
99
|
+
return flask.Response(response='abc', status=418)
|
|
100
|
+
|
|
101
|
+
resp = view_method(1, 2, 3)
|
|
102
|
+
html = response_to_html(resp)
|
|
103
|
+
assert isinstance(resp, flask.Response)
|
|
104
|
+
assert resp.status_code == 418
|
|
105
|
+
assert html == 'abc'
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def response_to_html(response: flask.Response) -> str:
|
|
109
|
+
# noinspection PyUnresolvedReferences
|
|
110
|
+
return response.response[0].decode('utf-8')
|