Skip to content

Commit

Permalink
Added TemplateResponse
Browse files Browse the repository at this point in the history
  • Loading branch information
0keeper1 committed Aug 22, 2024
1 parent 244e271 commit 6e2973e
Show file tree
Hide file tree
Showing 11 changed files with 101 additions and 5 deletions.
8 changes: 8 additions & 0 deletions docs/docs/configs.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ It is used when you set `cache=True` in `@API` decorator

_Example:_ `DEFAULT_CACHE_EXP = timedelta(seconds=10)`

---
### [TEMPLATES_DIR](https://pantherpy.github.io/templates_dir)
> <b>Type:</b> `str | list[str]` (<b>Default:</b> `'tempaltes'`)
We use it when want to have different template directories

_Example:_ `TEMPLATES_DIR = ['templates', 'app/templates']

---
### [THROTTLING](https://pantherpy.github.io/throttling)
> <b>Type:</b> `Throttling | None` (<b>Default:</b> `None`)
Expand Down
8 changes: 7 additions & 1 deletion example/app/apis.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from panther.generics import ListAPI
from panther.pagination import Pagination
from panther.request import Request
from panther.response import HTMLResponse, Response, StreamingResponse
from panther.response import HTMLResponse, Response, StreamingResponse, TemplateResponse
from panther.throttling import Throttling
from panther.websocket import close_websocket_connection, send_message_to_websocket

Expand Down Expand Up @@ -167,6 +167,11 @@ def get(self, *args, **kwargs):
return HTMLResponse(data=html_data)


class TemplateAPI(GenericAPI):
def get(self, *args, **kwargs) -> TemplateResponse:
return TemplateResponse(path='index.html', context={'username': 'Ali', 'age': 12})


@API()
async def send_message_to_websocket_api(connection_id: str):
await send_message_to_websocket(connection_id=connection_id, data='Hello From API')
Expand Down Expand Up @@ -222,6 +227,7 @@ def logout_api(request: Request):
def reader():
from faker import Faker
import time

f = Faker()
for _ in range(5):
name = f.name()
Expand Down
1 change: 1 addition & 0 deletions example/app/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ async def test(*args, **kwargs):
'patch-user-class/': PatchUser,
'file-class/': FileAPI,
'html-response/': HTMLAPI,
'template-response/': TemplateAPI,
'': single_user,
'ws/<user_id>/': UserWebsocket,
'send/<connection_id>/': send_message_to_websocket_api,
Expand Down
2 changes: 2 additions & 0 deletions example/core/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@

# THROTTLING = Throttling(rate=10, duration=timedelta(seconds=10))

# TEMPLATES_DIR = 'templates'


async def startup():
print('Starting Up')
Expand Down
9 changes: 9 additions & 0 deletions example/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>

<body>
<p>{{ username }}</p>
<p>{{ age }}</p>
</body>

</html>
6 changes: 6 additions & 0 deletions panther/_load_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
'load_user_model',
'load_log_queries',
'load_middlewares',
'load_templates_dir',
'load_auto_reformat',
'load_background_tasks',
'load_default_cache_exp',
Expand Down Expand Up @@ -82,6 +83,11 @@ def load_timezone(_configs: dict, /) -> None:
config.TIMEZONE = timezone


def load_templates_dir(_configs: dict, /) -> None:
if templates_dir := _configs.get('TEMPLATES_DIR'):
config.TEMPLATES_DIR = templates_dir


def load_database(_configs: dict, /) -> None:
database_config = _configs.get('DATABASE', {})
if 'engine' in database_config:
Expand Down
2 changes: 2 additions & 0 deletions panther/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class Config:
STARTUPS: list[Callable]
SHUTDOWNS: list[Callable]
TIMEZONE: str
TEMPLATES_DIR: str | list[str]
AUTO_REFORMAT: bool
QUERY_ENGINE: typing.Callable | None
DATABASE: typing.Callable | None
Expand Down Expand Up @@ -110,6 +111,7 @@ def refresh(self):
'STARTUPS': [],
'SHUTDOWNS': [],
'TIMEZONE': 'UTC',
'TEMPLATES_DIR': 'templates',
'AUTO_REFORMAT': False,
'QUERY_ENGINE': None,
'DATABASE': None,
Expand Down
1 change: 1 addition & 0 deletions panther/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def load_configs(self) -> None:
load_throttling(self._configs_module)
load_user_model(self._configs_module)
load_log_queries(self._configs_module)
load_templates_dir(self._configs_module)
load_middlewares(self._configs_module)
load_auto_reformat(self._configs_module)
load_background_tasks(self._configs_module)
Expand Down
30 changes: 29 additions & 1 deletion panther/response.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import asyncio
from types import NoneType
from typing import Generator, AsyncGenerator, Any, Type
from typing import Generator, AsyncGenerator, Any, LiteralString, Type

import orjson as json
from pydantic import BaseModel
from jinja2 import Environment, FileSystemLoader

from panther import status
from panther.configs import config
from panther._utils import to_async_generator
from panther.db.cursor import Cursor
from pantherdb import Cursor as PantherDBCursor
Expand Down Expand Up @@ -215,3 +217,29 @@ def body(self) -> bytes:
if isinstance(self.data, bytes):
return self.data
return self.data.encode()


class TemplateResponse(HTMLResponse):
environment = Environment(loader=FileSystemLoader(config.TEMPLATES_DIR))

def __init__(
self,
source: str | LiteralString | NoneType = None,
path: str | NoneType = None,
context: dict | NoneType = None,
headers: dict | NoneType = None,
status_code: int = status.HTTP_200_OK,
pagination: Pagination | NoneType = None,
):
"""
:param source: should be a string
:param path: should be path of template file
:param context: should be dict of items
:param headers: should be dict of headers
:param status_code: should be int
:param pagination: instance of Pagination or None
Its template() method will be used
"""

template = self.environment.get_template(path) if path is not None else self.environment.from_string(source)
super().__init__(template.render(context), headers, status_code, pagination=pagination)
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def panther_version() -> str:
'rich~=13.7.1',
'uvicorn~=0.27.1',
'pytz~=2024.1',
'Jinja2~=3.1',
],
extras_require=EXTRAS_REQUIRE,
)
38 changes: 35 additions & 3 deletions tests/test_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from panther import Panther
from panther.app import API, GenericAPI
from panther.response import Response, HTMLResponse, PlainTextResponse, StreamingResponse
from panther.response import Response, HTMLResponse, PlainTextResponse, StreamingResponse, TemplateResponse
from panther.test import APIClient


Expand Down Expand Up @@ -116,6 +116,18 @@ def get(self):
return HTMLResponse('<html><head><title></title></head></html>')


@API()
async def return_template_response() -> TemplateResponse:
return TemplateResponse(source='<html><body><p>{{ content }}</p></body></html>', context={'content': 'Hello World'})


class ReturnTemplateResponse(GenericAPI):
def get(self) -> TemplateResponse:
return TemplateResponse(
source='<html><body><p>{{ content }}</p></body></html>', context={'content': 'Hello World'}
)


@API()
async def return_plain_response():
return PlainTextResponse('Hello World')
Expand Down Expand Up @@ -160,7 +172,7 @@ def get(self):
'response-tuple': return_response_tuple,
'html': return_html_response,
'plain': return_plain_response,

'template': return_template_response,
'nothing-cls': ReturnNothing,
'none-cls': ReturnNone,
'dict-cls': ReturnDict,
Expand All @@ -172,8 +184,8 @@ def get(self):
'response-list-cls': ReturnResponseList,
'response-tuple-cls': ReturnResponseTuple,
'html-cls': ReturnHTMLResponse,
'template-cls': ReturnTemplateResponse,
'plain-cls': ReturnPlainResponse,

'stream': ReturnStreamingResponse,
'async-stream': ReturnAsyncStreamingResponse,
'invalid-status-code': ReturnInvalidStatusCode,
Expand Down Expand Up @@ -406,6 +418,26 @@ async def test_response_html_cls(self):
assert res.headers['Access-Control-Allow-Origin'] == '*'
assert res.headers['Content-Length'] == '41'

async def test_response_template(self) -> None:
res: Response = await self.client.get('template/')
assert res.status_code == 200
assert res.data == '<html><body><p>Hello World</p></body></html>'
assert res.body == b'<html><body><p>Hello World</p></body></html>'
assert set(res.headers.keys()) == {'Content-Type', 'Access-Control-Allow-Origin', 'Content-Length'}
assert res.headers['Content-Type'] == 'text/html; charset=utf-8'
assert res.headers['Access-Control-Allow-Origin'] == '*'
assert res.headers['Content-Length'] == '44'

async def test_response_template_cls(self) -> None:
res: Response = await self.client.get('template-cls/')
assert res.status_code == 200
assert res.data == '<html><body><p>Hello World</p></body></html>'
assert res.body == b'<html><body><p>Hello World</p></body></html>'
assert set(res.headers.keys()) == {'Content-Type', 'Access-Control-Allow-Origin', 'Content-Length'}
assert res.headers['Content-Type'] == 'text/html; charset=utf-8'
assert res.headers['Access-Control-Allow-Origin'] == '*'
assert res.headers['Content-Length'] == '44'

async def test_response_plain(self):
res = await self.client.get('plain/')
assert res.status_code == 200
Expand Down

0 comments on commit 6e2973e

Please sign in to comment.