Skip to content

Commit e1becb2

Browse files
committed
Added file (@file) decorator to manage creation of FileResponseModel or StreamingResponseModel
1 parent 34905e1 commit e1becb2

File tree

4 files changed

+89
-7
lines changed

4 files changed

+89
-7
lines changed

ellar/common/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .decorators.command import command
66
from .decorators.controller import Controller
77
from .decorators.exception import exception_handler
8+
from .decorators.file import file
89
from .decorators.guards import guards
910
from .decorators.html import render, template_filter, template_global
1011
from .decorators.middleware import middleware
@@ -89,4 +90,5 @@
8990
"Host",
9091
"Http",
9192
"UploadFile",
93+
"file",
9294
]

ellar/common/decorators/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from .command import command # noqa
33
from .controller import Controller # noqa
44
from .exception import exception_handler # noqa
5+
from .file import file # noqa
56
from .guards import guards # noqa
67
from .html import render, template_filter, template_global # noqa
78
from .middleware import middleware # noqa

ellar/common/decorators/file.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import inspect
2+
import typing as t
3+
import warnings
4+
5+
from ellar.constants import NOT_SET, RESPONSE_OVERRIDE_KEY
6+
from ellar.core.response.model import (
7+
FileResponseModel,
8+
IResponseModel,
9+
StreamingResponseModel,
10+
)
11+
from ellar.shortcuts import fail_silently
12+
13+
from .base import set_meta
14+
15+
16+
def file(media_type: t.Optional[str] = NOT_SET, streaming: bool = False) -> t.Callable:
17+
"""
18+
========= ROUTE FUNCTION DECORATOR ==============
19+
20+
Renders route function response as FileResponse
21+
22+
23+
:param media_type: MIME Type.
24+
:param streaming: Defaults ResponseModel to use. False=FileResponseModel, True=StreamingResponseModel.
25+
IF STREAMING == FALSE:
26+
Decorated Function is expected to return an object of dict with values keys:
27+
{
28+
path: mandatory path to file,
29+
media_type: optional
30+
filename: optional filename
31+
method: optional HTTP Method
32+
content_disposition_type: `attachment` | `inline`
33+
status_code: 200
34+
}
35+
IF STREAMING == TRUE
36+
Decorated Function is expected to return:
37+
typing.Iterator[Content] OR typing.AsyncIterable[Content]
38+
:return: typing.Callable
39+
"""
40+
if media_type is not NOT_SET:
41+
assert isinstance(media_type, str), "File decorator must invoked eg. @file()"
42+
43+
if media_type is NOT_SET:
44+
media_type = "text/plain"
45+
46+
def _decorator(func: t.Union[t.Callable, t.Any]) -> t.Union[t.Callable, t.Any]:
47+
if not inspect.isfunction(func):
48+
line_nos = fail_silently(
49+
inspect.getsourcelines, getattr(func, "endpoint", func)
50+
)
51+
warnings.warn_explicit(
52+
UserWarning(
53+
"\n@file should be used only as a function decorator. "
54+
"\nUse @file before @HTTPMethod decorator."
55+
),
56+
category=None,
57+
filename=inspect.getfile(getattr(func, "endpoint", func)),
58+
lineno=line_nos[1] if line_nos and len(line_nos) > 0 else None,
59+
source=None,
60+
)
61+
return func
62+
63+
response: IResponseModel
64+
if streaming:
65+
response = StreamingResponseModel(media_type=media_type)
66+
else:
67+
response = FileResponseModel(media_type=media_type)
68+
69+
target_decorator = set_meta(RESPONSE_OVERRIDE_KEY, {200: response})
70+
return target_decorator(func)
71+
72+
return _decorator

ellar/common/decorators/html.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
)
1212
from ellar.core.exceptions import ImproperConfiguration
1313
from ellar.core.response.model import HTMLResponseModel
14-
from ellar.core.routing import RouteOperationBase
1514
from ellar.core.templating import TemplateFunctionData
1615
from ellar.helper import class_base_function_regex, get_name
16+
from ellar.shortcuts import fail_silently
1717
from ellar.types import TemplateFilterCallable, TemplateGlobalCallable
1818

1919
from .base import set_meta
@@ -29,9 +29,13 @@ def render(template_name: t.Optional[str] = NOT_SET) -> t.Callable:
2929
3030
Renders route function response to HTML Response
3131
32-
:param template_name: template name.
33-
when @render is used in a Controller Class, the function becomes the template_name and the path to the html file
32+
Decorated Function is expected to return an object of dict as a context variable for the template to be rendered.
33+
34+
When @render is used in a Controller Class, the function becomes the template_name and the path to the html file
3435
becomes `templateFolder/ControllerName/functionName`. This can be overridden by providing `template_name`.
36+
37+
:param template_name: template name.
38+
3539
:return:
3640
"""
3741
if template_name is not NOT_SET:
@@ -41,15 +45,18 @@ def render(template_name: t.Optional[str] = NOT_SET) -> t.Callable:
4145
template_name = None if template_name is NOT_SET else template_name
4246

4347
def _decorator(func: t.Union[t.Callable, t.Any]) -> t.Union[t.Callable, t.Any]:
44-
if not callable(func) or isinstance(func, RouteOperationBase):
48+
if not inspect.isfunction(func):
49+
line_nos = fail_silently(
50+
inspect.getsourcelines, getattr(func, "endpoint", func)
51+
)
4552
warnings.warn_explicit(
4653
UserWarning(
47-
"\n@Render should be used only as a function decorator. "
48-
"\nUse @Render before @Method decorator."
54+
"\n@render should be used only as a function decorator. "
55+
"\nUse @render before @HTTPMethod decorator."
4956
),
5057
category=None,
5158
filename=inspect.getfile(getattr(func, "endpoint", func)),
52-
lineno=inspect.getsourcelines(getattr(func, "endpoint", func))[1],
59+
lineno=line_nos[1] if line_nos and len(line_nos) > 0 else None,
5360
source=None,
5461
)
5562
return func

0 commit comments

Comments
 (0)