3. Install Panther"},{"location":"#usage","title":"Usage","text":""},{"location":"#create-project","title":"Create Project","text":"$ panther create\n
"},{"location":"#run-project","title":"Run Project","text":"
$ panther run --reload\n
* Panther uses Uvicorn as ASGI (Asynchronous Server Gateway Interface) but you can run the project with Granian, daphne or any ASGI server too"},{"location":"#monitoring-requests","title":"Monitoring Requests","text":"$ panther monitor \n
"},{"location":"#python-shell","title":"Python Shell","text":"$ panther shell\n
"},{"location":"#single-file-structure-example","title":"Single-File Structure Example","text":" Create main.py
from datetime import datetime, timedelta\n\nfrom panther import version, status, Panther\nfrom panther.app import API\nfrom panther.request import Request\nfrom panther.response import Response\nfrom panther.throttling import Throttling\n\nInfoThrottling = Throttling(rate=5, duration=timedelta(minutes=1))\n\n\n@API()\nasync def hello_world():\n return {'detail': 'Hello World'}\n\n\n@API(cache=True, throttling=InfoThrottling)\nasync def info(request: Request):\n data = {\n 'panther_version': version(),\n 'datetime_now': datetime.now().isoformat(),\n 'user_agent': request.headers.user_agent\n }\n return Response(data=data, status_code=status.HTTP_202_ACCEPTED)\n\n\nurl_routing = {\n '': hello_world,\n 'info': info,\n}\n\napp = Panther(__name__, configs=__name__, urls=url_routing)\n
Run the project:
now you can see these two urls:
http://127.0.0.1:8000/ http://127.0.0.1:8000/info/ Next Step: First CRUD
Real Word Example: Https://GitHub.com/PantherPy/panther-example
"},{"location":"#roadmap","title":"Roadmap","text":""},{"location":"authentications/","title":"Authentications","text":"Variable: AUTHENTICATION
Type: str
Default: None
You can set your Authentication class in core/configs.py
, then Panther will use this class for authentication in every API
, if you set auth=True
in @API()
, and put the user
in request.user
or raise HTTP_401_UNAUTHORIZED
We already have one built-in authentication class which is used JWT
for authentication.
You can write your own authentication class too (we are going to discuss it)
"},{"location":"authentications/#jwtauthentication","title":"JWTAuthentication","text":"This class will
Get the token
from Authorization
header of request with keyword of Bearer
decode
it Find the match user
in USER_MODEL
you have already set JWTAuthentication
is going to use panther.db.models.BaseUser
if you didn't set the USER_MODEL
in your core/configs.py
You can customize these 3 variables for JWTAuthentication
in your core/configs.py
as JWTConfig
like below (JWTConfig
is optional):
...\nfrom datetime import timedelta\nfrom panther.utils import load_env \nfrom pathlib import Path\n\nBASE_DIR = Path(__name__).resolve().parent \nenv = load_env(BASE_DIR / '.env')\n\nSECRET_KEY = env['SECRET_KEY']\n\nJWTConfig = { \n 'key': SECRET_KEY, \n 'algorithm': 'HS256', \n 'life_time': timedelta(days=2), \n}\n
key \u2003\u2003\u2003\u2003--> default is SECRET_KEY
algorithm \u2003 --> default is HS256
life_time\u2003\u2003--> default is timedelta(days=1)
"},{"location":"authentications/#custom-authentication","title":"Custom Authentication","text":" Create a class and inherits it from panther.authentications.BaseAuthentication
Implement authentication(cls, request: Request)
method
Process the request.headers.authorization
or ... Return Instance of USER_MODEL
Or raise panther.exceptions.AuthenticationException
Address it in core/configs.py
AUTHENTICATION = 'project_name.core.authentications.CustomAuthentication'
You can look at the source code of JWTAuthentication for
"},{"location":"background_tasks/","title":"Background Tasks","text":""},{"location":"background_tasks/#intro","title":"Intro","text":"Panther is going to run the background tasks as a thread in the background
"},{"location":"background_tasks/#usage","title":"Usage","text":" Add the BACKGROUND_TASKS = True
in the core/configs.py
Import the background_tasks
from panther.background_tasks
:
from panther.background_tasks import background_tasks\n
Create a task
from panther.background_tasks import background_tasks, BackgroundTask\n\ndef do_something(name: str, age: int):\n pass\n\ntask = BackgroundTask(do_something, name='Ali', age=26)\n
Now you can add your task to the background_tasks
from panther.background_tasks import background_tasks, BackgroundTask\n\ndef do_something(name: str, age: int):\n pass\n\ntask = BackgroundTask(do_something, name='Ali', age=26)\nbackground_tasks.add_task(task)\n
"},{"location":"background_tasks/#options","title":"Options","text":" let's say we want to run the task
below every day on 8:00
o'clock.
from datetime import time\n\nfrom panther.background_tasks import BackgroundTask, background_tasks\n\n\ndef do_something(name: str, age: int):\n pass\n\ntask = BackgroundTask(do_something, name='Ali', age=26).at(time(hour=8))\nbackground_tasks.add_task(task)\n
"},{"location":"background_tasks/#interval","title":"Interval","text":"You can set custom interval
for the task
, let's say we want to run the task
below for 3 times
.
from panther.background_tasks import BackgroundTask, background_tasks\n\n\ndef do_something(name: str, age: int):\n pass\n\ntask = BackgroundTask(do_something, name='Ali', age=26).interval(3)\nbackground_tasks.add_task(task)\n
"},{"location":"background_tasks/#schedule","title":"Schedule","text":"BackgroundTask
has some methods to schedule
the run time, (Default value of them is 1
)
"},{"location":"background_tasks/#time-specification","title":"Time Specification","text":"You can set a custom time
to tasks too
"},{"location":"background_tasks/#notice","title":"Notice","text":" The task
function can be sync
or async
You can pass the arguments to the task as args
and kwargs
def do_something(name: str, age: int):\n pass\n\ntask = BackgroundTask(do_something, name='Ali', age=26)\nor \ntask = BackgroundTask(do_something, 'Ali', age=26)\nor \ntask = BackgroundTask(do_something, 'Ali', 26)\n
Default interval is 1.
The -1 interval means infinite,
The .at()
only useful when you are using .every_days()
or .every_weeks()
"},{"location":"class_first_crud/","title":"Class Base","text":"We assume you could run the project with Introduction
Now let's write custom API Create
, Retrieve
, Update
and Delete
for a Book
:
"},{"location":"class_first_crud/#structure-requirements","title":"Structure & Requirements","text":""},{"location":"class_first_crud/#create-model","title":"Create Model","text":"Create a model named Book
in app/models.py
:
from panther.db import Model\n\n\nclass Book(Model):\n name: str\n author: str\n pages_count: int\n
"},{"location":"class_first_crud/#create-api-class","title":"Create API Class","text":"Create the BookAPI()
in app/apis.py
:
from panther.app import GenericAPI\n\n\nclass BookAPI(GenericAPI):\n ... \n
We are going to complete it later ...
"},{"location":"class_first_crud/#update-urls","title":"Update URLs","text":"Add the BookAPI
in app/urls.py
:
from app.apis import BookAPI\n\n\nurls = {\n 'book/': BookAPI,\n}\n
We assume that the urls
in core/urls.py
pointing to app/urls.py
, like below:
from app.urls import urls as app_urls\n\n\nurls = {\n '/': app_urls,\n}\n
"},{"location":"class_first_crud/#add-database-middleware","title":"Add Database Middleware","text":"Add one database middleware in core/configs.py
MIDDLEWARES
, we are going to add pantherdb
PantherDB is a Simple, FileBase and Document Oriented database:
...\n\nMIDDLEWARES = [\n ('panther.middlewares.db.DatabaseMiddleware', {'url': f'pantherdb://{BASE_DIR}/{DB_NAME}.pdb'}),\n]\n
"},{"location":"class_first_crud/#apis","title":"APIs","text":""},{"location":"class_first_crud/#api-create-a-book","title":"API - Create a Book","text":"Now we are going to create a book on post
request, We need to:
Declare post
method in BookAPI
:
from panther.app import GenericAPI\n\n\nclass BookAPI(GenericAPI):\n\n def post(self):\n ...\n
Declare request: Request
in BookAPI.post()
function:
from panther.app import GenericAPI\nfrom panther.request import Request\n\n\nclass BookAPI(GenericAPI):\n\n def post(self, request: Request):\n ...\n
Create serializer in app/serializers.py
, we used pydantic
for the validation
of request.data
:
from pydantic import BaseModel\n\n\nclass BookSerializer(BaseModel):\n name: str\n author: str\n pages_count: int\n
Pass the created serializer to our BookAPI
as input_model
so the incoming data will be validated and cleaned automatically:
from panther.app import GenericAPI\nfrom panther.request import Request\n\nfrom app.serializers import BookSerializer\n\n\nclass BookAPI(GenericAPI):\n input_model = BookSerializer\n\n def post(self, request: Request):\n ...\n
Now we have access to request.data
, We are going to use it like the below for ease of use, so the auto-suggest helps us in development: from panther.app import GenericAPI\nfrom panther.request import Request\n\nfrom app.serializers import BookSerializer\n\n\nclass BookAPI(GenericAPI):\n input_model = BookSerializer\n\n def post(self, request: Request):\n body: BookSerializer = request.validated_data\n ...\n
Now we have access to the validated data, and we can create our first book:
from panther.app import GenericAPI\nfrom panther.request import Request\n\nfrom app.serializers import BookSerializer\nfrom app.models import Book\n\n\nclass BookAPI(GenericAPI):\n input_model = BookSerializer\n\n def post(self, request: Request):\n body: BookSerializer = request.validated_data\n Book.insert_one(\n name=body.name,\n author=body.author,\n pages_count=body.pages_count,\n )\n ...\n
And finally we return 201 Created
status_code as response of post
:
from panther import status\nfrom panther.app import GenericAPI\nfrom panther.request import Request\nfrom panther.response import Response\n\nfrom app.serializers import BookSerializer\nfrom app.models import Book\n\n\nclass BookAPI(GenericAPI):\n input_model = BookSerializer\n\n def post(self, request: Request):\n body: BookSerializer = request.validated_data\n book = Book.insert_one(\n name=body.name,\n author=body.author,\n pages_count=body.pages_count,\n )\n return Response(data=book, status_code=status.HTTP_201_CREATED)\n
The response.data can be Instance of Models
, dict
, str
, tuple
, list
, str
or None
Panther will return None
if you don't return anything as response.
"},{"location":"class_first_crud/#api-list-of-books","title":"API - List of Books","text":"We just need to add another method for GET
method and return the lists of books:
from panther import status\nfrom panther.app import GenericAPI\nfrom panther.request import Request\nfrom panther.response import Response\n\nfrom app.serializers import BookSerializer\nfrom app.models import Book\n\n\nclass BookAPI(GenericAPI):\n input_model = BookSerializer\n\n def post(self, request: Request):\n ...\n\n def get(self):\n books: list[Book] = Book.find()\n return Response(data=books, status_code=status.HTTP_200_OK)\n
Panther validate input with input_model
, only in POST
, PUT
, PATCH
methods.
"},{"location":"class_first_crud/#filter-response-fields","title":"Filter Response Fields","text":"Assume we don't want to return field author
in response:
Create new serializer in app/serializers.py
:
from pydantic import BaseModel\n\n\nclass BookOutputSerializer(BaseModel):\n name: str\n pages_count: int\n
Add the BookOutputSerializer
as output_model
to your class
from panther import status\nfrom panther.app import GenericAPI\nfrom panther.request import Request\nfrom panther.response import Response\n\nfrom app.serializers import BookSerializer, BookOutputSerializer\nfrom app.models import Book\n\n\nclass BookAPI(GenericAPI):\n input_model = BookSerializer\n output_model = BookOutputSerializer\n\n def post(self, request: Request):\n ...\n\n def get(self):\n books: list[Book] = Book.find()\n return Response(data=books, status_code=status.HTTP_200_OK)\n
Panther use the output_model
, in all methods.
"},{"location":"class_first_crud/#cache-the-response","title":"Cache The Response","text":"For caching the response, we should add cache=True
in API()
. And it will return the cached response every time till cache_exp_time
For setting a custom expiration time for API we need to add cache_exp_time
to API()
:
from datetime import timedelta\n\nfrom panther import status\nfrom panther.app import GenericAPI\nfrom panther.request import Request\nfrom panther.response import Response\n\nfrom app.serializers import BookSerializer, BookOutputSerializer\nfrom app.models import Book\n\n\nclass BookAPI(GenericAPI):\n input_model = BookSerializer\n output_model = BookOutputSerializer\n cache = True\n cache_exp_time = timedelta(seconds=10)\n\n def post(self, request: Request):\n ...\n\n def get(self):\n books: list[Book] = Book.find()\n return Response(data=books, status_code=status.HTTP_200_OK)\n
Panther is going to use the DEFAULT_CACHE_EXP
from core/configs.py
if cache_exp_time
has not been set.
"},{"location":"class_first_crud/#throttle-the-request","title":"Throttle The Request","text":"For setting rate limit for requests, we can add throttling to BookAPI
, it should be the instance of panther.throttling.Throttling
, something like below (in the below example user can't request more than 10 times in a minutes):
from datetime import timedelta\n\nfrom panther import status\nfrom panther.app import GenericAPI\nfrom panther.request import Request\nfrom panther.response import Response\nfrom panther.throttling import Throttling\n\nfrom app.serializers import BookSerializer, BookOutputSerializer\nfrom app.models import Book\n\n\nclass BookAPI(GenericAPI):\n input_model = BookSerializer\n output_model = BookOutputSerializer\n cache = True\n cache_exp_time = timedelta(seconds=10)\n throttling = Throttling(rate=10, duration=timedelta(minutes=1))\n\n def post(self, request: Request):\n ...\n\n def get(self):\n books: list[Book] = Book.find()\n return Response(data=books, status_code=status.HTTP_200_OK)\n
"},{"location":"class_first_crud/#api-retrieve-a-book","title":"API - Retrieve a Book","text":"For retrieve
, update
and delete
API, we are going to
Create another class named SingleBookAPI
in app/apis.py
:
from panther.app import GenericAPI\n\n\nclass SingleBookAPI(GenericAPI):\n ...\n
Add it in app/urls.py
:
from app.apis import BookAPI, SingleBookAPI\n\n\nurls = {\n 'book/': BookAPI,\n 'book/<book_id>/': SingleBookAPI,\n}\n
You should write the Path Variable in <
and >
You should have the parameter with the same name of path variable
in you api
with normal type hints
Panther will convert type of the path variable
to your parameter type, then pass it
Complete the api:
from panther import status\nfrom panther.app import GenericAPI\nfrom panther.response import Response\n\nfrom app.models import Book\n\n\nclass SingleBookAPI(GenericAPI):\n\n def get(self, book_id: int):\n if book := Book.find_one(id=book_id):\n return Response(data=book, status_code=status.HTTP_200_OK)\n else:\n return Response(status_code=status.HTTP_404_NOT_FOUND)\n
"},{"location":"class_first_crud/#api-update-a-book","title":"API - Update a Book","text":""},{"location":"class_first_crud/#api-delete-a-book","title":"API - Delete a Book","text":""},{"location":"configs/","title":"Configs","text":"Panther stores all the configs in the core/configs.py
"},{"location":"configs/#monitoring","title":"MONITORING","text":"Type: bool
(Default: False
)
It should be True
if you want to use panther monitor
command and see the monitoring logs
If True
it will:
Log every request in logs/monitoring.log
"},{"location":"configs/#log_queries","title":"LOG_QUERIES","text":"Type: bool
(Default: False
)
If True
it will:
Calculate every query perf time & Log them in logs/query.log
"},{"location":"configs/#middlewares","title":"MIDDLEWARES","text":"Type: list
(Default: [ ]
)
List of middlewares you want to use
"},{"location":"configs/#authentication","title":"AUTHENTICATION","text":"Type: str | None
(Default: None
)
Every request goes through authentication()
method of this class
Example: AUTHENTICATION = 'panther.authentications.JWTAuthentication'
"},{"location":"configs/#urls","title":"URLs","text":"Type: str
(Required)
It should be the address of your urls
dict
Example: URLS = 'core.configs.urls.url_routing'
"},{"location":"configs/#default_cache_exp","title":"DEFAULT_CACHE_EXP","text":"Type: timedelta| None
(Default: None
)
We use it as default cache_exp_time
you can overwrite it in your @API
too
It is used when you set cache=True
in @API
decorator
Example: DEFAULT_CACHE_EXP = timedelta(seconds=10)
"},{"location":"configs/#throttling","title":"THROTTLING","text":"Type: Throttling | None
(Default: None
)
We use it as default throttling
you can overwrite it in your @API
too
Example: THROTTLING = Throttling(rate=10, duration=timedelta(seconds=10))
"},{"location":"configs/#user_model","title":"USER_MODEL","text":"Type: str | None
(Default: 'panther.db.models.BaseUser'
)
It is used for authentication
Example: USER_MODEL = 'panther.db.models.User'
"},{"location":"configs/#jwtconfig","title":"JWTConfig","text":"Type: dict | None
(Default: JWTConfig = {'key': SECRET_KEY}
)
We use it when you set panther.authentications.JWTAuthentication
as AUTHENTICATION
"},{"location":"configs/#background_tasks","title":"BACKGROUND_TASKS","text":"Type: bool
(Default: False
)
If True
it will:
initialize()
the background_tasks
"},{"location":"configs/#startup","title":"STARTUP","text":"Type: str | None
(Default: None
)
It should be dotted address of your startup
function, this function can be sync
or async
Example: URLS = 'core.configs.startup'
"},{"location":"configs/#shutdown","title":"SHUTDOWN","text":"Type: str | None
(Default: None
)
It should be dotted address of your shutdown
function this function can be sync
or async
Example: URLS = 'core.configs.shutdown'
"},{"location":"configs/#auto_reformat","title":"AUTO_REFORMAT","text":"Type: bool
(Default: False
)
It will reformat your code on every reload (on every change if you run the project with --reload
)
You may want to write your custom ruff.toml
in root of your project.
Reference: https://docs.astral.sh/ruff/formatter/
Example: AUTO_REFORMAT = True
"},{"location":"function_first_crud/","title":"Function Base","text":"We assume you could run the project with Introduction
Now let's write custom API Create
, Retrieve
, Update
and Delete
for a Book
:
"},{"location":"function_first_crud/#structure-requirements","title":"Structure & Requirements","text":""},{"location":"function_first_crud/#create-model","title":"Create Model","text":"Create a model named Book
in app/models.py
:
from panther.db import Model\n\n\nclass Book(Model):\n name: str\n author: str\n pages_count: int\n
"},{"location":"function_first_crud/#create-api-function","title":"Create API Function","text":"Create the book_api()
in app/apis.py
:
from panther.app import API\n\n\n@API()\nasync def book_api():\n ... \n
We are going to complete it later ...
"},{"location":"function_first_crud/#update-urls","title":"Update URLs","text":"Add the book_api
in app/urls.py
:
from app.apis import book_api\n\n\nurls = {\n 'book/': book_api,\n}\n
We assume that the urls
in core/urls.py
pointing to app/urls.py
, like below:
from app.urls import urls as app_urls\n\n\nurls = {\n '/': app_urls,\n}\n
"},{"location":"function_first_crud/#add-database-middleware","title":"Add Database Middleware","text":"Add one database middleware in core/configs.py
MIDDLEWARES
, we are going to add pantherdb
PantherDB is a Simple, FileBase and Document Oriented database:
...\n\nMIDDLEWARES = [\n ('panther.middlewares.db.DatabaseMiddleware', {'url': f'pantherdb://{BASE_DIR}/{DB_NAME}.pdb'}),\n]\n
"},{"location":"function_first_crud/#apis","title":"APIs","text":""},{"location":"function_first_crud/#api-create-a-book","title":"API - Create a Book","text":"Now we are going to create a book on post
request, We need to:
Declare request: Request
in book_api
function:
from panther.app import API\nfrom panther.request import Request\n\n\n@API()\nasync def book_api(request: Request):\n ...\n
Create serializer in app/serializers.py
, we used pydantic
for the validation
of request.data
:
from pydantic import BaseModel\n\n\nclass BookSerializer(BaseModel):\n name: str\n author: str\n pages_count: int\n
Pass the created serializer to our book_api
as input_model
so the incoming data will be validated and cleaned automatically:
from panther.app import API\nfrom panther.request import Request\n\nfrom app.serializers import BookSerializer\n\n\n@API(input_model=BookSerializer)\nasync def book_api(request: Request):\n ...\n
Now we have access to request.data
, We are going to use it like the below for ease of use, so the auto-suggest helps us in development: from panther.app import API\nfrom panther.request import Request\n\nfrom app.serializers import BookSerializer\n\n\n@API(input_model=BookSerializer)\nasync def book_api(request: Request):\n body: BookSerializer = request.validated_data\n ...\n
Now we have access to the validated data, and we can create our first book:
from panther.app import API\nfrom panther.request import Request\n\nfrom app.serializers import BookSerializer\nfrom app.models import Book\n\n\n@API(input_model=BookSerializer)\nasync def book_api(request: Request):\n body: BookSerializer = request.validated_data\n\n Book.insert_one(\n name=body.name,\n author=body.author,\n pages_count=body.pages_count,\n )\n ...\n
But we only want this happens in post
requests, so we add this condition
:
from panther.app import API\nfrom panther.request import Request\n\nfrom app.serializers import BookSerializer\nfrom app.models import Book\n\n\n@API(input_model=BookSerializer)\nasync def book_api(request: Request):\n if request.method == 'POST':\n body: BookSerializer = request.validated_data\n\n Book.create(\n name=body.name,\n author=body.author,\n pages_count=body.pages_count,\n )\n ...\n
And finally we return 201 Created
status_code as response of post
and 501 Not Implemented
for other methods:
from panther import status\nfrom panther.app import API\nfrom panther.request import Request\nfrom panther.response import Response\n\nfrom app.serializers import BookSerializer\nfrom app.models import Book\n\n\n@API(input_model=BookSerializer)\nasync def book_api(request: Request):\n if request.method == 'POST':\n body: BookSerializer = request.validated_data\n\n book: Book = Book.create(\n name=body.name,\n author=body.author,\n pages_count=body.pages_count,\n )\n return Response(data=book, status_code=status.HTTP_201_CREATED)\n\n return Response(status_code=status.HTTP_501_NOT_IMPLEMENTED)\n
The response.data can be Instance of Models
, dict
, str
, tuple
, list
, str
or None
Panther will return None
if you don't return anything as response.
"},{"location":"function_first_crud/#api-list-of-books","title":"API - List of Books","text":"We just need to add another condition on GET
methods and return the lists of books:
from panther import status\nfrom panther.app import API\nfrom panther.request import Request\nfrom panther.response import Response\n\nfrom app.serializers import BookSerializer\nfrom app.models import Book\n\n\n@API(input_model=BookSerializer)\nasync def book_api(request: Request):\n if request.method == 'POST':\n ...\n\n elif request.method == 'GET':\n books: list[Book] = Book.find()\n return Response(data=books, status_code=status.HTTP_200_OK)\n\n return Response(status_code=status.HTTP_501_NOT_IMPLEMENTED)\n
Panther validate input with input_model
, only in POST
, PUT
, PATCH
methods.
"},{"location":"function_first_crud/#filter-response-fields","title":"Filter Response Fields","text":"Assume we don't want to return field author
in response:
Create new serializer in app/serializers.py
:
from pydantic import BaseModel\n\n\nclass BookOutputSerializer(BaseModel):\n name: str\n pages_count: int\n
Add the BookOutputSerializer
as output_model
to your API()
from panther import status\nfrom panther.app import API\nfrom panther.request import Request\nfrom panther.response import Response\n\nfrom app.serializers import BookSerializer, BookOutputSerializer\nfrom app.models import Book\n\n\n@API(input_model=BookSerializer, output_model=BookOutputSerializer)\nasync def book_api(request: Request):\n if request.method == 'POST':\n ...\n\n elif request.method == 'GET':\n books: list[Book] = Book.find()\n return Response(data=books, status_code=status.HTTP_200_OK)\n\n return Response(status_code=status.HTTP_501_NOT_IMPLEMENTED)\n
Panther use the output_model
, in all methods.
"},{"location":"function_first_crud/#cache-the-response","title":"Cache The Response","text":"For caching the response, we should add cache=True
in API()
. And it will return the cached response every time till cache_exp_time
For setting a custom expiration time for API we need to add cache_exp_time
to API()
:
from datetime import timedelta\n\nfrom panther import status\nfrom panther.app import API\nfrom panther.request import Request\nfrom panther.response import Response\n\nfrom app.serializers import BookSerializer, BookOutputSerializer\nfrom app.models import Book\n\n\n@API(input_model=BookSerializer, output_model=BookOutputSerializer, cache=True, cache_exp_time=timedelta(seconds=10))\nasync def book_api(request: Request):\n if request.method == 'POST':\n ...\n\n elif request.method == 'GET':\n books: list[Book] = Book.find()\n return Response(data=books, status_code=status.HTTP_200_OK)\n\n return Response(status_code=status.HTTP_501_NOT_IMPLEMENTED)\n
Panther is going to use the DEFAULT_CACHE_EXP
from core/configs.py
if cache_exp_time
has not been set.
"},{"location":"function_first_crud/#throttle-the-request","title":"Throttle The Request","text":"For setting rate limit for requests, we can add throttling to API()
, it should be the instance of panther.throttling.Throttling
, something like below (in the below example user can't request more than 10 times in a minutes):
from datetime import timedelta\n\nfrom panther import status\nfrom panther.app import API\nfrom panther.request import Request\nfrom panther.response import Response\nfrom panther.throttling import Throttling\n\nfrom app.serializers import BookSerializer, BookOutputSerializer\nfrom app.models import Book\n\n\n@API(\n input_model=BookSerializer, \n output_model=BookOutputSerializer, \n cache=True, \n cache_exp_time=timedelta(seconds=10),\n throttling=Throttling(rate=10, duration=timedelta(minutes=1))\n)\nasync def book_api(request: Request):\n if request.method == 'POST':\n ...\n\n elif request.method == 'GET':\n books: list[Book] = Book.find()\n return Response(data=books, status_code=status.HTTP_200_OK)\n\n return Response(status_code=status.HTTP_501_NOT_IMPLEMENTED)\n
"},{"location":"function_first_crud/#api-retrieve-a-book","title":"API - Retrieve a Book","text":"For retrieve
, update
and delete
API, we are going to
Create another api named single_book_api
in app/apis.py
:
from panther.app import API\nfrom panther.request import Request\n\n\n@API()\nasync def single_book_api(request: Request):\n ...\n
Add it in app/urls.py
:
from app.apis import book_api, single_book_api\n\n\nurls = {\n 'book/': book_api,\n 'book/<book_id>/': single_book_api,\n}\n
You should write the Path Variable in <
and >
You should have the parameter with the same name of path variable
in you api
with normal type hints
Panther will convert type of the path variable
to your parameter type, then pass it
Complete the api:
from panther import status\nfrom panther.app import API\nfrom panther.request import Request\nfrom panther.response import Response\n\nfrom app.models import Book\n\n\n@API()\nasync def single_book_api(request: Request, book_id: int):\n if request.method == 'GET':\n if book := Book.find_one(id=book_id):\n return Response(data=book, status_code=status.HTTP_200_OK)\n else:\n return Response(status_code=status.HTTP_404_NOT_FOUND)\n
"},{"location":"function_first_crud/#api-update-a-book","title":"API - Update a Book","text":""},{"location":"function_first_crud/#api-delete-a-book","title":"API - Delete a Book","text":""},{"location":"log_queries/","title":"Log Queries","text":"Variable: LOG_QUERIES
Type: bool
Default: False
Panther has a log_query
decorator on queries that process the perf_time
of every query
Make sure it is False
on production for better performance
"},{"location":"log_queries/#log-example","title":"Log Example:","text":"INFO: | 2023-03-19 20:37:27 | Query --> User.insert_one() --> 1.6 ms\n
"},{"location":"log_queries/#the-log-query-decorator-is-something-like-this","title":"The Log Query Decorator Is Something Like This","text":"def log_query(func):\n def log(*args, **kwargs):\n if config['log_queries'] is False:\n return func(*args, **kwargs)\n\n start = perf_counter()\n response = func(*args, **kwargs)\n end = perf_counter()\n class_name = ...\n query_logger.info(f'Query --> {class_name}.{func.__name__}() --> {(end - start) * 1_000:.2} ms')\n return response\n return log\n
"},{"location":"middlewares/","title":"Middlewares","text":"Variable: MIDDLEWARES
Type: list
Default: []
Panther has several built-in
middleware:
Database Middleware
Redis Middleware
And you can write your own custom middlewares too
"},{"location":"middlewares/#structure-of-middlewares","title":"Structure of middlewares","text":"MIDDLEWARES
itself is a list
of tuples
which each tuple
is like below:
(Dotted Address of The Middleware Class
, kwargs as dict
)
"},{"location":"middlewares/#database-middleware","title":"Database Middleware","text":"This middleware will create a db
connection which is used in ODM
and you can use it manually too, it gives you a database connection:
from panther.db.connection import db\n
We only support 2 database for now: PantherDB
& MongoDB
Address of Middleware: panther.middlewares.db.DatabaseMiddleware
kwargs:
Example of PantherDB
(Built-in Local Storage
):
MIDDLEWARES = [\n ('panther.middlewares.db.DatabaseMiddleware', {'url': 'pantherdb://project_directory/database.pdb'}),\n]\n
Example of MongoDB
: MIDDLEWARES = [\n ('panther.middlewares.db.DatabaseMiddleware', {'url': 'mongodb://127.0.0.1:27017/example'}),\n]\n
"},{"location":"middlewares/#redis-middleware","title":"Redis Middleware","text":" Address of Middleware: panther.middlewares.redis.RedisMiddleware
kwargs:
{'host': '127.0.0.1', 'port': 6379, ...}\n
Example
MIDDLEWARES = [\n ('panther.middlewares.redis.RedisMiddleware', {'host': '127.0.0.1', 'port': 6379}),\n]\n
"},{"location":"middlewares/#custom-middleware","title":"Custom Middleware","text":""},{"location":"middlewares/#middleware-types","title":"Middleware Types","text":"We have 3 type of Middlewares, make sure that you are inheriting from the correct one: - Base Middleware
: which is used for both websocket
and http
requests - HTTP Middleware
: which is only used for http
requests - Websocket Middleware
: which is only used for websocket
requests
"},{"location":"middlewares/#write-custom-middleware","title":"Write Custom Middleware","text":" Write a class
and inherit from one of the classes below
# For HTTP Requests\nfrom panther.middlewares.base import HTTPMiddleware\n\n# For Websocket Requests\nfrom panther.middlewares.base import WebsocketMiddleware\n\n# For Both HTTP and Websocket Requests\nfrom panther.middlewares.base import BaseMiddleware\n
Then you can write your custom before()
and after()
methods
The methods
should be async
before()
should have request
parameter after()
should have response
parameter overwriting the before()
and after()
are optional The methods
can get kwargs
from their __init__
"},{"location":"middlewares/#custom-http-middleware-example","title":"Custom HTTP Middleware Example","text":" core/middlewares.py
from panther.middlewares.base import HTTPMiddleware\nfrom panther.request import Request\nfrom panther.response import Response\n\n\nclass CustomMiddleware(HTTPMiddleware):\n\n def __init__(self, something):\n self.something = something\n\n async def before(self, request: Request) -> Request:\n print('Before Endpoint', self.something)\n return request\n\n async def after(self, response: Response) -> Response:\n print('After Endpoint', self.something)\n return response\n
core/configs.py
MIDDLEWARES = [\n ('core.middlewares.CustomMiddleware', {'something': 'hello-world'}),\n]\n
"},{"location":"middlewares/#custom-http-websocket-middleware-example","title":"Custom HTTP + Websocket Middleware Example","text":" core/middlewares.py
from panther.middlewares.base import BaseMiddleware\nfrom panther.request import Request\nfrom panther.response import Response\nfrom panther.websocket import GenericWebsocket \n\n\nclass SayHiMiddleware(BaseMiddleware):\n\n def __init__(self, name):\n self.name = name\n\n async def before(self, request: Request | GenericWebsocket) -> Request | GenericWebsocket:\n print('Hello ', self.name)\n return request\n\n async def after(self, response: Response | GenericWebsocket) -> Response | GenericWebsocket:\n print('Goodbye ', self.name)\n return response\n
core/configs.py
MIDDLEWARES = [\n ('core.middlewares.SayHiMiddleware', {'name': 'Ali Rn'}),\n]\n
"},{"location":"monitoring/","title":"Monitoring","text":"Variable: MONITORING
Type: bool
Default: False
Panther has a Monitoring
middleware that process the perf_time
of every request
It will create a monitoring.log
file and log the records
Then you can watch them live with: panther monitor
"},{"location":"monitoring/#log-example","title":"Log Example:","text":"date time | method | path | ip:port | response_time [ms, s] | status\n\n2023-12-11 18:23:42 | GET | /login | 127.0.0.1:55710 | 2.8021 ms | 200\n
"},{"location":"panther_odm/","title":"Panther ODM","text":""},{"location":"panther_odm/#find_one","title":"find_one","text":" Find the first match document Example:
user: User = User.find_one(id=1, name='Ali') \n\nuser: User = User.find_one({'id': 1, 'name': 'Ali'}) \n\nuser: User = User.find_one({'id': 1}, name='Ali') \n
"},{"location":"panther_odm/#find","title":"find","text":" Find all the matches documents Example:
users: list[User] = User.find(id=1, name='Ali') \n\nusers: list[User] = User.find({'id': 1, 'name': 'Ali'}) \n\nusers: list[User] = User.find({'id': 1}, name='Ali') \n
"},{"location":"panther_odm/#all","title":"all","text":""},{"location":"panther_odm/#insert_one","title":"insert_one","text":" Insert only one document into database Example:
User.insert_one(id=1, name='Ali') \n\nUser.insert_one({'id': 1, 'name': 'Ali'}) \n\nUser.insert_one({'id': 1}, name='Ali') \n
"},{"location":"panther_odm/#delete","title":"delete","text":""},{"location":"panther_odm/#delete_one","title":"delete_one","text":""},{"location":"panther_odm/#delete_many","title":"delete_many","text":" Delete all the matches document from database Example:
deleted_count: int = User.delete_many(id=1, name='Ali')\n\ndeleted_count: int = User.delete_many({'id': 1}, name='Ali')\n\ndeleted_count: int = User.delete_many({'id': 1, 'name': 'Ali'})\n
"},{"location":"panther_odm/#update","title":"update","text":""},{"location":"panther_odm/#update_one","title":"update_one","text":" Update the first match document in database You should filter with dictionary
as first parameter
and pass the fields you want to update as kwargs
or another dictionary
as second parameter
Example:
is_updated: bool = User.update_one({'id': 1, 'name': 'Ali'}, name='Saba', age=26)\n\nis_updated: bool = User.update_one({'id': 1, 'name': 'Ali'}, {'name': 'Saba', 'age': 26})\n\nis_updated: bool = User.update_one({'id': 1, 'name': 'Ali'}, {'name': 'Saba'}, age=26)\n
"},{"location":"panther_odm/#update_many","title":"update_many","text":" Update all the matches document in database You should filter with dictionary
as first parameter
and pass the fields you want to update as kwargs
or another dictionary
as second parameter
Example:
updated_count: int = User.update_many({'name': 'Ali'}, name='Saba', age=26)\n\nupdated_count: int = User.update_many({'name': 'Ali'}, {'name': 'Saba', 'age': 26})\n\nupdated_count: int = User.update_many({'name': 'Ali'}, {'name': 'Saba'}, age=26)\n
"},{"location":"panther_odm/#last","title":"last","text":" Find the last match document Example:
user: User = User.last(name='Ali', age=26) \n\nuser: User = User.last({'name': 'Ali', 'age': 26}) \n\nuser: User = User.last({'name': 'Ali'}, age=26) \n
"},{"location":"panther_odm/#count","title":"count","text":""},{"location":"panther_odm/#find_or_insert","title":"find_or_insert","text":""},{"location":"panther_odm/#find_one_or_raise","title":"find_one_or_raise","text":""},{"location":"release_notes/","title":"Release Notes","text":""},{"location":"release_notes/#351","title":"3.5.1","text":"Set default behavior of GenericWebsocket.connect
to ignore the connection (reject
)
"},{"location":"release_notes/#350","title":"3.5.0","text":""},{"location":"release_notes/#340","title":"3.4.0","text":" Support WebsocketMiddleware
"},{"location":"release_notes/#332","title":"3.3.2","text":" Add content-length
to response header "},{"location":"release_notes/#331","title":"3.3.1","text":" Check ruff
installation on startup Fix an issue in routing
"},{"location":"release_notes/#330","title":"3.3.0","text":""},{"location":"release_notes/#324","title":"3.2.4","text":" Add all() query Add tests for pantherdb
, load_configs()
, status.py
, Panel
, multipart
, request headers
Refactor Headers()
class Check uvloop
installation on Panther init
Minor Improvement "},{"location":"release_notes/#321","title":"3.2.1","text":""},{"location":"release_notes/#320","title":"3.2.0","text":" Support Startup
& Shutdown
Events "},{"location":"release_notes/#315","title":"3.1.5","text":" Support Websocket
in the monitoring
Refactor collect_all_models()
"},{"location":"release_notes/#314","title":"3.1.4","text":" Check ws redis connection on the init
Refactor Monitoring
class and usage Improve logging
config Check database connection before query "},{"location":"release_notes/#313","title":"3.1.3","text":" Add Image
base class Add size
to File
base class Improve the way of loading configs
in single-file
structure Improve background_tasks.py
, generate_ws_connection_id()
bpython
removed from being the default python shell Improve load_middlewares()
error handling Print configs
on the run
Add requirements.txt
for development Update roadmap.jpg
, README.md
"},{"location":"release_notes/#312","title":"3.1.2","text":" Add new methods to BackgroundTask
every_seconds()
every_minutes()
every_hours()
every_days()
every_weeks()
at()
"},{"location":"release_notes/#311","title":"3.1.1","text":" Upgrade PantherDB
version Add first()
, last()
queries "},{"location":"release_notes/#310","title":"3.1.0","text":""},{"location":"release_notes/#303","title":"3.0.3","text":" Add find_one_or_raise
query Add last_login
to BaseUser
Add refresh_life_time
to JWTConfig
Add encode_refresh_token()
to JWTAuthentication
Add encrypt_password()
Handle PantherException
Handle RedisConnection
without connection_pool
"},{"location":"release_notes/#302","title":"3.0.2","text":" Added 'utf-8' encoding while opening the file \"README.md\" in setup.py Fixed panther shell not working issue in windows. Added a condition to raise error if no argument is passed to panther command in cli. "},{"location":"release_notes/#301","title":"3.0.1","text":" Assume content-type is 'application/json' if it was empty Fix an issue on creating instance of model when query is done "},{"location":"release_notes/#300","title":"3.0.0","text":" Support Websocket Implement Built-in TestClient Support Single-File Structure Support bytes
as Response.data
Add methods
to API()
Change Request.pure_data
to Request.data
Change Request.data
to Request.validated_data
Change panther.middlewares.db.Middleware
to panther.middlewares.db.DatabaseMiddleware
Change panther.middlewares.redis.Middleware
to panther.middlewares.redis.RedisMiddleware
Fix panther run
command Minor Improvement "},{"location":"release_notes/#242","title":"2.4.2","text":" Don't log content-type when it's not supported "},{"location":"release_notes/#241","title":"2.4.1","text":" Fix an issue in collect_all_models() in Windows "},{"location":"release_notes/#240","title":"2.4.0","text":" Handle Complex Multipart-FormData "},{"location":"release_notes/#233","title":"2.3.3","text":" Fix a bug in response headers "},{"location":"release_notes/#232","title":"2.3.2","text":""},{"location":"release_notes/#231","title":"2.3.1","text":" Handle PlainTextResponse Handle Custom Header in Response Change the way of accepting 'URLs' in configs (relative -> dotted) Fix an issue in collect_all_models() "},{"location":"release_notes/#230","title":"2.3.0","text":""},{"location":"release_notes/#220","title":"2.2.0","text":""},{"location":"release_notes/#216","title":"2.1.6","text":" Fix validation errors on nested inputs "},{"location":"release_notes/#215","title":"2.1.5","text":" Fix response of nested Models in _panel//"},{"location":"release_notes/#214","title":"2.1.4","text":" Add access-control-allow-origin to response header "},{"location":"release_notes/#213","title":"2.1.3","text":" Upgrade greenlet version in requirements for python3.12 "},{"location":"release_notes/#212","title":"2.1.2","text":" Add ruff.toml Add Coverage to workflows Fix a bug for running in Windows "},{"location":"release_notes/#211","title":"2.1.1","text":" Fix a bug in main.py imports "},{"location":"release_notes/#210","title":"2.1.0","text":""},{"location":"release_notes/#200","title":"2.0.0","text":" Supporting class-base APIs "},{"location":"release_notes/#1720","title":"1.7.20","text":" Fix an issue in find_endpoint() "},{"location":"release_notes/#1719","title":"1.7.19","text":" Fix an issue in routing Fix an issue on return complex dict Response "},{"location":"release_notes/#1718","title":"1.7.18","text":" Remove uvloop from requirements for now (we had issue in windows) "},{"location":"release_notes/#1716","title":"1.7.16","text":" Trying to fix requirements for windows Minor improvement in BaseMongoDBQuery "},{"location":"release_notes/#1715","title":"1.7.15","text":" Fix an issue in handling form-data "},{"location":"release_notes/#1714","title":"1.7.14","text":" Add Cache and Throttling doc to FirstCrud Fix an issue in BasePantherDBQuery._merge() "},{"location":"release_notes/#1713","title":"1.7.13","text":" Hotfix validation of _id in Model() "},{"location":"release_notes/#1712","title":"1.7.12","text":""},{"location":"release_notes/#1711","title":"1.7.11","text":""},{"location":"release_notes/#1710","title":"1.7.10","text":" Fix a bug in collect_urls
and rename it to flatten_urls
Add General Tests Compatible with python3.10 (Not Tested) Working on docs "},{"location":"release_notes/#179","title":"1.7.9","text":""},{"location":"release_notes/#178","title":"1.7.8","text":""},{"location":"release_notes/#178_1","title":"1.7.8","text":""},{"location":"release_notes/#177","title":"1.7.7","text":""},{"location":"release_notes/#175","title":"1.7.5","text":" Change the way of raising exception in JWTAuthentication Rename User model to BaseUser Fix template "},{"location":"release_notes/#174","title":"1.7.4","text":""},{"location":"release_notes/#173","title":"1.7.3","text":" Add Throttling Doc Fix some issue in Doc "},{"location":"release_notes/#172","title":"1.7.2","text":" Add Throttling to example Customize install_requires in setup.py Improve monitoring cli command "},{"location":"release_notes/#171","title":"1.7.1","text":" Rename db BaseModel to Model Add more docs "},{"location":"release_notes/#170","title":"1.7.0","text":""},{"location":"release_notes/#161","title":"1.6.1","text":""},{"location":"release_notes/#160","title":"1.6.0","text":""},{"location":"release_notes/#152","title":"1.5.2","text":" Improve Response data serialization Fix a bug in JWTAuthentication "},{"location":"release_notes/#151","title":"1.5.1","text":""},{"location":"release_notes/#150","title":"1.5.0","text":" Refactor Mongodb ODM Minor Improvement "},{"location":"release_notes/#140","title":"1.4.0","text":""},{"location":"release_notes/#132","title":"1.3.2","text":" Add Uvicorn to the setup requirements Update Readme "},{"location":"release_notes/#131","title":"1.3.1","text":" Fix a bug in project creation template Fix a bug in caching "},{"location":"release_notes/#130","title":"1.3.0","text":" Add PantherDB to Panther Remove tinydb "},{"location":"release_notes/#127","title":"1.2.7","text":" Fix a bug while using tinydb "},{"location":"release_notes/#126","title":"1.2.6","text":""},{"location":"release_notes/#125","title":"1.2.5","text":" Fix install_requires issue Add benchmarks to docs "},{"location":"release_notes/#124","title":"1.2.4","text":" Remove Uvicorn From install_requires Working on Docs "},{"location":"release_notes/#123","title":"1.2.3","text":""},{"location":"release_notes/#121","title":"1.2.1","text":" Path Variable Handled Successfully "},{"location":"release_notes/#120","title":"1.2.0","text":" Read multipart/form-data with Regex "},{"location":"release_notes/#119","title":"1.1.9","text":" Refactoring code style with ruff Add asyncio.TaskGroup() "},{"location":"release_notes/#118","title":"1.1.8","text":""},{"location":"release_notes/#117","title":"1.1.7","text":" Add benchmark pictures to doc "},{"location":"release_notes/#115","title":"1.1.5","text":" Clean Readme Clean main.py "},{"location":"release_notes/#114","title":"1.1.4","text":""},{"location":"release_notes/#113","title":"1.1.3","text":""},{"location":"release_notes/#112","title":"1.1.2","text":" Add delete_many query to TinyDB "},{"location":"release_notes/#111","title":"1.1.1","text":""},{"location":"release_notes/#110","title":"1.1.0","text":""},{"location":"release_notes/#109","title":"1.0.9","text":" Handle string exceptions (raise them as detail: error) Little debug on MongoQueries "},{"location":"release_notes/#107","title":"1.0.7","text":" Working on queries Fix a bug in query methods "},{"location":"release_notes/#106","title":"1.0.6","text":""},{"location":"release_notes/#104","title":"1.0.4","text":""},{"location":"release_notes/#102","title":"1.0.2","text":" Add global config Split the BaseModels Worked on MongoQuery Set Mongo as default database while creating project Minor Improvement "},{"location":"release_notes/#101","title":"1.0.1","text":""},{"location":"release_notes/#10","title":"1.0.","text":" Refactor & Complete the CLI "},{"location":"release_notes/#019","title":"0.1.9","text":""},{"location":"release_notes/#018","title":"0.1.8","text":""},{"location":"release_notes/#017","title":"0.1.7","text":""},{"location":"release_notes/#016","title":"0.1.6","text":" Handle Most Types as Data in Response "},{"location":"release_notes/#014","title":"0.1.4","text":""},{"location":"release_notes/#001","title":"0.0.1","text":""},{"location":"single_file/","title":"Single-File","text":"If you want to work with Panther
in a single-file
structure, follow the steps below.
"},{"location":"single_file/#steps","title":"Steps","text":" Write your APIs
as you like
from panther.app import API\n\n@API()\nasync def hello_world_api():\n return {'detail': 'Hello World'}\n
Add your APIs
to a dict
(ex: url_routing
)
from panther.app import API\n\n@API()\nasync def hello_world_api():\n return {'detail': 'Hello World'}\n\nurl_routing = {\n '/': hello_world_api,\n}\n
3. Create an app
and pass your current module name
and urls
to it.
from panther import Panther\nfrom panther.app import API\n\n@API()\nasync def hello_world_api():\n return {'detail': 'Hello World'}\n\nurl_routing = {\n '/': hello_world_api,\n}\n\napp = Panther(__name__, configs=__name__, urls=url_routing)\n
4. Run the project panther run \n
URLs
is a required config unless you pass the urls
to the Panther
When you pass the configs
to the Panther(configs=...)
, Panther is going to load the configs from this file, else it is going to load core/configs.py
file
You can pass the startup
and shutdown
functions to the Panther()
too.
from panther import Panther\nfrom panther.app import API\n\n@API()\nasync def hello_world_api():\n return {'detail': 'Hello World'}\n\nurl_routing = {\n '/': hello_world_api,\n}\n\ndef startup():\n pass\n\ndef shutdown():\n pass\n\napp = Panther(__name__, configs=__name__, urls=url_routing, startup=startup, shutdown=shutdown)\n
"},{"location":"throttling/","title":"Throttling","text":"Variable: THROTTLING
Type: str
In Panther, you can use Throttling
for all APIs at once in core/configs.py
or per API in its @API
decorator
The Throttling
class has 2 field rate
& duration
rate: int
duration: datetime.timedelta
It will return Too Many Request
status_code: 429
if user try to request in the duration
more than rate
And user will baned( getToo Many Request
) for duration
And keep that in mind if you have Throttling
in @API()
, the Throttling
of core/configs.py
will be ignored.
"},{"location":"throttling/#for-all-apis-example","title":"For All APIs Example:","text":"core/configs.py
from datetime import timedelta\n\nfrom panther.throttling import Throttling\n\n\n# User only can request 5 times in every minute\nTHROTTLING = Throttling(rate=5, duration=timedelta(minutes=1))\n
"},{"location":"throttling/#for-single-api-example","title":"For Single API Example:","text":"apis.py
from datetime import timedelta\n\nfrom panther.throttling import Throttling\nfrom panther.app import API\n\n\n# User only can request 5 times in every minute\nInfoThrottling = Throttling(rate=5, duration=timedelta(minutes=1))\n\n\n@API(throttling=InfoThrottling)\nasync def info_api():\n pass\n
"},{"location":"todos/","title":"TODOs","text":""},{"location":"todos/#base","title":"Base","text":" \u2705 Start with Uvicorn \u2705 Fix URL Routing \u2705 Read Configs \u2705 Handle Exceptions \u2705 Add Custom Logger \u2705 Request Class \u2705 Response Class \u2705 Validate Input \u2705 Custom Output Model \u2705 Log Queries \u2705 Add Package Requirements \u2705 Custom Logging \u2705 Caching \u2705 Handle Path Variable \u2705 Handle Simple Multipart-FormData \u2705 Handle Throttling \u2705 Handle ClassBase APIs \u2705 Handle File \u2705 Handle Complex Multipart-FormData \u2705 Handle Testing \u2705 Handle WS \u2610 Handle Cookie \u2610 Generate Swagger For APIs "},{"location":"todos/#database","title":"Database:","text":" \u2705 Structure Of DB Connection \u2705 PantherDB Connection \u2705 MongoDB Connection \u2705 Create Custom BaseModel For All Type Of Databases \u2705 Set PantherDB As Default "},{"location":"todos/#custom-odm","title":"Custom ODM","text":" \u2705 Find One \u2705 Find \u2705 Last \u2705 Count \u2705 Insert One \u2705 Insert Many \u2705 Delete One \u2705 Delete Many \u2705 Delete Itself \u2705 Update One \u2705 Update Many \u2705 Update Itself \u2705 Find or Insert \u2705 Find or Raise \u2705 Save \u2610 Find with Pagination \u2610 Aggregation \u2610 Complex Pipelines \u2610 ... "},{"location":"todos/#middleware","title":"Middleware","text":" \u2705 Add Middlewares To Structure \u2705 Create BaseMiddleware \u2705 Pass Custom Parameters To Middlewares \u2705 Handle Custom Middlewares "},{"location":"todos/#authentication","title":"Authentication","text":" \u2705 JWT Authentication \u2705 Separate Auth For Every API \u2705 Handle Permissions \u2610 Token Storage Authentication \u2610 Cookie Authentication \u2610 Query Param Authentication \u2610 Store JWT After Logout In Redis/ Memory "},{"location":"todos/#cache","title":"Cache","text":" \u2705 Add Redis To Structure \u2705 Create Cache Decorator \u2705 Handle In Memory Caching \u2705 Handle In Redis Caching \u2610 Write Async LRU_Caching With TTL (Replace it with in memory ...) "},{"location":"todos/#cli","title":"CLI","text":" \u2705 Create Project \u2705 Run Project \u2705 Create Project with Options \u2705 Monitoring With Textual \u2705 Monitor Requests, Response & Time \u2610 Create Project With TUI "},{"location":"todos/#documentation","title":"Documentation","text":" \u2705 Create MkDocs For Project \u2705 Benchmarks \u2705 Release Notes \u2705 Features \u2610 Complete The MkDoc "},{"location":"todos/#tests","title":"Tests","text":" \u2705 Start Writing Tests For Panther \u2705 Test Client "},{"location":"urls/","title":"URLs","text":"Variable: URLs
Type: str
Required: True
"},{"location":"urls/#path-variables-are-handled-like-below","title":"Path Variables are handled like below:","text":" <variable_name
> Example: user/<user_id>/blog/<title>/
The endpoint
should have parameters with those names too Example: async def profile_api(user_id: int, title: str):
"},{"location":"urls/#example","title":"Example","text":" core/configs.py URLs = 'core.urls.url_routing\n
core/urls.py from app.urls import app_urls\n\nurl_routing = {\n 'user/': app_urls,\n}\n
app/urls.py
from app.apis import *\n\nurls = {\n 'login/': login_api,\n 'logout/': logout_api,\n 'profile/<user_id>/': profile_api,\n}\n
app/apis.py
...\n\n@API()\nasync def profile_api(user_id: int):\n return User.find_one(id=user_id)\n
"},{"location":"websocket/","title":"WebSocket","text":"Panther supports WebSockets
routing just like APIs
"},{"location":"websocket/#structure-requirements","title":"Structure & Requirements","text":""},{"location":"websocket/#create-websocket-class","title":"Create WebSocket Class","text":"Create the BookWebsocket()
in app/websockets.py
which inherited from GenericWebsocket
:
from panther.websocket import GenericWebsocket\n\n\nclass BookWebsocket(GenericWebsocket):\n async def connect(self):\n await self.accept()\n print(f'{self.connection_id=}')\n\n async def receive(self, data: str | bytes = None):\n # Just Echo The Message\n await self.send(data=data)\n
We are going to discuss it below ...
"},{"location":"websocket/#update-urls","title":"Update URLs","text":"Add the BookWebsocket()
in app/urls.py
:
from app.websockets import BookWebsocket\n\n\nurls = {\n 'ws/book/': BookWebsocket,\n}\n
"},{"location":"websocket/#how-it-works","title":"How It Works?","text":" Client tries to connect to your ws/book/
url with websocket
protocol The connect()
method of your BookWebsocket
is going to call You should validate the connection with self.headers
, self.query_params
or etc Then accept()
the connection with self.accept()
otherwise it is going to be rejected
by default. Now you can see the unique connection_id
which is specified to this user with self.connection_id
, you may want to store it somewhere (db
, cache
, or etc.) If the client sends you any message, you will receive it in receive()
method, the client message can be str
or bytes
. If you want to send anything to the client: If you want to use webscoket
in multi-tread
or multi-instance
backend, you should add RedisMiddleware
in your configs
or it won't work well. [Adding Redis Middleware] If you want to close a connection:
In websocket class scope: You can close connection with self.close()
method which takes 2 args, code
and reason
: from panther import status\nawait self.close(code=status.WS_1000_NORMAL_CLOSURE, reason='I just want to close it')\n
Out of websocket class scope (Not Recommended): You can close it with close_websocket_connection()
from panther.websocket
, it's async
function with takes 3 args, connection_id
, code
and reason
, like below: from panther import status\nfrom panther.websocket import close_websocket_connection\nawait close_websocket_connection(connection_id='7e82d57c9ec0478787b01916910a9f45', code=status.WS_1008_POLICY_VIOLATION, reason='')\n
Path Variables
will be passed to connect()
:
from panther.websocket import GenericWebsocket\n\n class UserWebsocket(GenericWebsocket):\n async def connect(self, user_id: int, room_id: str):\n await self.accept()\n\n url = {\n '/ws/<user_id>/<room_id>/': UserWebsocket \n }\n
WebSocket Echo Example -> Https://GitHub.com/PantherPy/echo_websocket Enjoy. "},{"location":"working_with_db/","title":"Working With Database","text":"Panther create a database connection depends on database middleware you are using on core/configs.py
and you can access to this connection from your models
or direct access from from panther.db.connection import db
Now we are going to create a new API which uses our default database(PantherDB
) and creating a Book
Create Book
model in app/models.py
from panther.db import Model\n\n\nclass Book(Model):\n title: str\n description: str\n pages_count: int\n
Add book
url in app/urls.py
that points to book_api()
...\nfrom app.apis import time_api, book_api\n\n\nurls = {\n '': hello_world,\n 'info/': info,\n 'time/': time_api,\n 'book/': book_api,\n}\n
Create book_api()
in app/apis.py
from panther import status\nfrom panther.app import API\nfrom panther.response import Response\n\n\n@API()\nasync def book_api():\n ...\n return Response(status_code=status.HTTP_201_CREATED) \n
Now we should use the Panther ODM to create a book, it's based on mongo queries, for creation we use insert_one
like this:
from panther import status\nfrom panther.app import API\nfrom panther.response import Response\nfrom app.models import Book\n\n\n@API()\nasync def book_api():\n Book.insert_one(\n title='Python',\n description='Python is good.',\n pages_count=10\n )\n return Response(status_code=status.HTTP_201_CREATED) \n
In next step we are going to explain more about Panther ODM
"}]}
\ No newline at end of file
+{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Introduction","text":""},{"location":"#panther","title":"Panther","text":"Is A Fast & Friendly Web Framework For Building Async APIs With Python 3.10+
"},{"location":"#why-use-panther","title":"Why Use Panther ?","text":" Document-oriented Databases ODM (PantherDB, MongoDB) Built-in Websocket Support Cache APIs (In Memory, In Redis) Built-in Authentication Classes (Customizable) Built-in Permission Classes (Customizable) Handle Custom Middlewares Handle Custom Throttling Visual API Monitoring (In Terminal) "},{"location":"#supported-by","title":"Supported by","text":""},{"location":"#benchmark","title":"Benchmark","text":"Framework Throughput (Request/Second) Blacksheep 5,339 Muffin 5,320 Panther 5,112 Sanic 3,660 FastAPI 3,260 Tornado 2,081 Bottle 2,045 Django 821 Flask 749 More Detail: https://GitHub.com/PantherPy/frameworks-benchmark
"},{"location":"#installation","title":"Installation","text":" 1. Create a Virtual Environment $ python3 -m venv .venv 2. Active The Environment 3. Install Panther"},{"location":"#usage","title":"Usage","text":""},{"location":"#create-project","title":"Create Project","text":"$ panther create\n
"},{"location":"#run-project","title":"Run Project","text":"
$ panther run --reload\n
* Panther uses Uvicorn as ASGI (Asynchronous Server Gateway Interface) but you can run the project with Granian, daphne or any ASGI server too"},{"location":"#monitoring-requests","title":"Monitoring Requests","text":"$ panther monitor \n
"},{"location":"#python-shell","title":"Python Shell","text":"$ panther shell\n
"},{"location":"#single-file-structure-example","title":"Single-File Structure Example","text":" Create main.py
from datetime import datetime, timedelta\n\nfrom panther import version, status, Panther\nfrom panther.app import API\nfrom panther.request import Request\nfrom panther.response import Response\nfrom panther.throttling import Throttling\n\nInfoThrottling = Throttling(rate=5, duration=timedelta(minutes=1))\n\n\n@API()\nasync def hello_world():\n return {'detail': 'Hello World'}\n\n\n@API(cache=True, throttling=InfoThrottling)\nasync def info(request: Request):\n data = {\n 'panther_version': version(),\n 'datetime_now': datetime.now().isoformat(),\n 'user_agent': request.headers.user_agent\n }\n return Response(data=data, status_code=status.HTTP_202_ACCEPTED)\n\n\nurl_routing = {\n '': hello_world,\n 'info': info,\n}\n\napp = Panther(__name__, configs=__name__, urls=url_routing)\n
Run the project:
now you can see these two urls:
http://127.0.0.1:8000/ http://127.0.0.1:8000/info/ Next Step: First CRUD
Real Word Example: Https://GitHub.com/PantherPy/panther-example
"},{"location":"#roadmap","title":"Roadmap","text":""},{"location":"authentications/","title":"Authentications","text":"Variable: AUTHENTICATION
Type: str
Default: None
You can set your Authentication class in core/configs.py
, then Panther will use this class for authentication in every API
, if you set auth=True
in @API()
, and put the user
in request.user
or raise HTTP_401_UNAUTHORIZED
We already have one built-in authentication class which is used JWT
for authentication.
You can write your own authentication class too (we are going to discuss it)
"},{"location":"authentications/#jwtauthentication","title":"JWTAuthentication","text":"This class will
Get the token
from Authorization
header of request with keyword of Bearer
decode
it Find the match user
in USER_MODEL
you have already set JWTAuthentication
is going to use panther.db.models.BaseUser
if you didn't set the USER_MODEL
in your core/configs.py
You can customize these 3 variables for JWTAuthentication
in your core/configs.py
as JWTConfig
like below (JWTConfig
is optional):
...\nfrom datetime import timedelta\nfrom panther.utils import load_env \nfrom pathlib import Path\n\nBASE_DIR = Path(__name__).resolve().parent \nenv = load_env(BASE_DIR / '.env')\n\nSECRET_KEY = env['SECRET_KEY']\n\nJWTConfig = { \n 'key': SECRET_KEY, \n 'algorithm': 'HS256', \n 'life_time': timedelta(days=2), \n}\n
key \u2003\u2003\u2003\u2003--> default is SECRET_KEY
algorithm \u2003 --> default is HS256
life_time\u2003\u2003--> default is timedelta(days=1)
"},{"location":"authentications/#custom-authentication","title":"Custom Authentication","text":" Create a class and inherits it from panther.authentications.BaseAuthentication
Implement authentication(cls, request: Request)
method
Process the request.headers.authorization
or ... Return Instance of USER_MODEL
Or raise panther.exceptions.AuthenticationException
Address it in core/configs.py
AUTHENTICATION = 'project_name.core.authentications.CustomAuthentication'
You can look at the source code of JWTAuthentication for
"},{"location":"background_tasks/","title":"Background Tasks","text":""},{"location":"background_tasks/#intro","title":"Intro","text":"Panther is going to run the background tasks as a thread in the background
"},{"location":"background_tasks/#usage","title":"Usage","text":" Add the BACKGROUND_TASKS = True
in the core/configs.py
Import the background_tasks
from panther.background_tasks
:
from panther.background_tasks import background_tasks\n
Create a task
from panther.background_tasks import background_tasks, BackgroundTask\n\ndef do_something(name: str, age: int):\n pass\n\ntask = BackgroundTask(do_something, name='Ali', age=26)\n
Now you can add your task to the background_tasks
from panther.background_tasks import background_tasks, BackgroundTask\n\ndef do_something(name: str, age: int):\n pass\n\ntask = BackgroundTask(do_something, name='Ali', age=26)\nbackground_tasks.add_task(task)\n
"},{"location":"background_tasks/#options","title":"Options","text":" let's say we want to run the task
below every day on 8:00
o'clock.
from datetime import time\n\nfrom panther.background_tasks import BackgroundTask, background_tasks\n\n\ndef do_something(name: str, age: int):\n pass\n\ntask = BackgroundTask(do_something, name='Ali', age=26).at(time(hour=8))\nbackground_tasks.add_task(task)\n
"},{"location":"background_tasks/#interval","title":"Interval","text":"You can set custom interval
for the task
, let's say we want to run the task
below for 3 times
.
from panther.background_tasks import BackgroundTask, background_tasks\n\n\ndef do_something(name: str, age: int):\n pass\n\ntask = BackgroundTask(do_something, name='Ali', age=26).interval(3)\nbackground_tasks.add_task(task)\n
"},{"location":"background_tasks/#schedule","title":"Schedule","text":"BackgroundTask
has some methods to schedule
the run time, (Default value of them is 1
)
"},{"location":"background_tasks/#time-specification","title":"Time Specification","text":"You can set a custom time
to tasks too
"},{"location":"background_tasks/#notice","title":"Notice","text":" The task
function can be sync
or async
You can pass the arguments to the task as args
and kwargs
def do_something(name: str, age: int):\n pass\n\ntask = BackgroundTask(do_something, name='Ali', age=26)\nor \ntask = BackgroundTask(do_something, 'Ali', age=26)\nor \ntask = BackgroundTask(do_something, 'Ali', 26)\n
Default interval is 1.
The -1 interval means infinite,
The .at()
only useful when you are using .every_days()
or .every_weeks()
"},{"location":"class_first_crud/","title":"Class Base","text":"We assume you could run the project with Introduction
Now let's write custom API Create
, Retrieve
, Update
and Delete
for a Book
:
"},{"location":"class_first_crud/#structure-requirements","title":"Structure & Requirements","text":""},{"location":"class_first_crud/#create-model","title":"Create Model","text":"Create a model named Book
in app/models.py
:
from panther.db import Model\n\n\nclass Book(Model):\n name: str\n author: str\n pages_count: int\n
"},{"location":"class_first_crud/#create-api-class","title":"Create API Class","text":"Create the BookAPI()
in app/apis.py
:
from panther.app import GenericAPI\n\n\nclass BookAPI(GenericAPI):\n ... \n
We are going to complete it later ...
"},{"location":"class_first_crud/#update-urls","title":"Update URLs","text":"Add the BookAPI
in app/urls.py
:
from app.apis import BookAPI\n\n\nurls = {\n 'book/': BookAPI,\n}\n
We assume that the urls
in core/urls.py
pointing to app/urls.py
, like below:
from app.urls import urls as app_urls\n\n\nurls = {\n '/': app_urls,\n}\n
"},{"location":"class_first_crud/#add-database-middleware","title":"Add Database Middleware","text":"Add one database middleware in core/configs.py
MIDDLEWARES
, we are going to add pantherdb
PantherDB is a Simple, FileBase and Document Oriented database:
...\n\nMIDDLEWARES = [\n ('panther.middlewares.db.DatabaseMiddleware', {'url': f'pantherdb://{BASE_DIR}/{DB_NAME}.pdb'}),\n]\n
"},{"location":"class_first_crud/#apis","title":"APIs","text":""},{"location":"class_first_crud/#api-create-a-book","title":"API - Create a Book","text":"Now we are going to create a book on post
request, We need to:
Declare post
method in BookAPI
:
from panther.app import GenericAPI\n\n\nclass BookAPI(GenericAPI):\n\n def post(self):\n ...\n
Declare request: Request
in BookAPI.post()
function:
from panther.app import GenericAPI\nfrom panther.request import Request\n\n\nclass BookAPI(GenericAPI):\n\n def post(self, request: Request):\n ...\n
Create serializer in app/serializers.py
, we used pydantic
for the validation
of request.data
:
from pydantic import BaseModel\n\n\nclass BookSerializer(BaseModel):\n name: str\n author: str\n pages_count: int\n
Pass the created serializer to our BookAPI
as input_model
so the incoming data will be validated and cleaned automatically:
from panther.app import GenericAPI\nfrom panther.request import Request\n\nfrom app.serializers import BookSerializer\n\n\nclass BookAPI(GenericAPI):\n input_model = BookSerializer\n\n def post(self, request: Request):\n ...\n
Now we have access to request.data
, We are going to use it like the below for ease of use, so the auto-suggest helps us in development: from panther.app import GenericAPI\nfrom panther.request import Request\n\nfrom app.serializers import BookSerializer\n\n\nclass BookAPI(GenericAPI):\n input_model = BookSerializer\n\n def post(self, request: Request):\n body: BookSerializer = request.validated_data\n ...\n
Now we have access to the validated data, and we can create our first book:
from panther.app import GenericAPI\nfrom panther.request import Request\n\nfrom app.serializers import BookSerializer\nfrom app.models import Book\n\n\nclass BookAPI(GenericAPI):\n input_model = BookSerializer\n\n def post(self, request: Request):\n body: BookSerializer = request.validated_data\n Book.insert_one(\n name=body.name,\n author=body.author,\n pages_count=body.pages_count,\n )\n ...\n
And finally we return 201 Created
status_code as response of post
:
from panther import status\nfrom panther.app import GenericAPI\nfrom panther.request import Request\nfrom panther.response import Response\n\nfrom app.serializers import BookSerializer\nfrom app.models import Book\n\n\nclass BookAPI(GenericAPI):\n input_model = BookSerializer\n\n def post(self, request: Request):\n body: BookSerializer = request.validated_data\n book = Book.insert_one(\n name=body.name,\n author=body.author,\n pages_count=body.pages_count,\n )\n return Response(data=book, status_code=status.HTTP_201_CREATED)\n
The response.data can be Instance of Models
, dict
, str
, tuple
, list
, str
or None
Panther will return None
if you don't return anything as response.
"},{"location":"class_first_crud/#api-list-of-books","title":"API - List of Books","text":"We just need to add another method for GET
method and return the lists of books:
from panther import status\nfrom panther.app import GenericAPI\nfrom panther.request import Request\nfrom panther.response import Response\n\nfrom app.serializers import BookSerializer\nfrom app.models import Book\n\n\nclass BookAPI(GenericAPI):\n input_model = BookSerializer\n\n def post(self, request: Request):\n ...\n\n def get(self):\n books: list[Book] = Book.find()\n return Response(data=books, status_code=status.HTTP_200_OK)\n
Panther validate input with input_model
, only in POST
, PUT
, PATCH
methods.
"},{"location":"class_first_crud/#filter-response-fields","title":"Filter Response Fields","text":"Assume we don't want to return field author
in response:
Create new serializer in app/serializers.py
:
from pydantic import BaseModel\n\n\nclass BookOutputSerializer(BaseModel):\n name: str\n pages_count: int\n
Add the BookOutputSerializer
as output_model
to your class
from panther import status\nfrom panther.app import GenericAPI\nfrom panther.request import Request\nfrom panther.response import Response\n\nfrom app.serializers import BookSerializer, BookOutputSerializer\nfrom app.models import Book\n\n\nclass BookAPI(GenericAPI):\n input_model = BookSerializer\n output_model = BookOutputSerializer\n\n def post(self, request: Request):\n ...\n\n def get(self):\n books: list[Book] = Book.find()\n return Response(data=books, status_code=status.HTTP_200_OK)\n
Panther use the output_model
, in all methods.
"},{"location":"class_first_crud/#cache-the-response","title":"Cache The Response","text":"For caching the response, we should add cache=True
in API()
. And it will return the cached response every time till cache_exp_time
For setting a custom expiration time for API we need to add cache_exp_time
to API()
:
from datetime import timedelta\n\nfrom panther import status\nfrom panther.app import GenericAPI\nfrom panther.request import Request\nfrom panther.response import Response\n\nfrom app.serializers import BookSerializer, BookOutputSerializer\nfrom app.models import Book\n\n\nclass BookAPI(GenericAPI):\n input_model = BookSerializer\n output_model = BookOutputSerializer\n cache = True\n cache_exp_time = timedelta(seconds=10)\n\n def post(self, request: Request):\n ...\n\n def get(self):\n books: list[Book] = Book.find()\n return Response(data=books, status_code=status.HTTP_200_OK)\n
Panther is going to use the DEFAULT_CACHE_EXP
from core/configs.py
if cache_exp_time
has not been set.
"},{"location":"class_first_crud/#throttle-the-request","title":"Throttle The Request","text":"For setting rate limit for requests, we can add throttling to BookAPI
, it should be the instance of panther.throttling.Throttling
, something like below (in the below example user can't request more than 10 times in a minutes):
from datetime import timedelta\n\nfrom panther import status\nfrom panther.app import GenericAPI\nfrom panther.request import Request\nfrom panther.response import Response\nfrom panther.throttling import Throttling\n\nfrom app.serializers import BookSerializer, BookOutputSerializer\nfrom app.models import Book\n\n\nclass BookAPI(GenericAPI):\n input_model = BookSerializer\n output_model = BookOutputSerializer\n cache = True\n cache_exp_time = timedelta(seconds=10)\n throttling = Throttling(rate=10, duration=timedelta(minutes=1))\n\n def post(self, request: Request):\n ...\n\n def get(self):\n books: list[Book] = Book.find()\n return Response(data=books, status_code=status.HTTP_200_OK)\n
"},{"location":"class_first_crud/#api-retrieve-a-book","title":"API - Retrieve a Book","text":"For retrieve
, update
and delete
API, we are going to
Create another class named SingleBookAPI
in app/apis.py
:
from panther.app import GenericAPI\n\n\nclass SingleBookAPI(GenericAPI):\n ...\n
Add it in app/urls.py
:
from app.apis import BookAPI, SingleBookAPI\n\n\nurls = {\n 'book/': BookAPI,\n 'book/<book_id>/': SingleBookAPI,\n}\n
You should write the Path Variable in <
and >
You should have the parameter with the same name of path variable
in you api
with normal type hints
Panther will convert type of the path variable
to your parameter type, then pass it
Complete the api:
from panther import status\nfrom panther.app import GenericAPI\nfrom panther.response import Response\n\nfrom app.models import Book\n\n\nclass SingleBookAPI(GenericAPI):\n\n def get(self, book_id: int):\n if book := Book.find_one(id=book_id):\n return Response(data=book, status_code=status.HTTP_200_OK)\n else:\n return Response(status_code=status.HTTP_404_NOT_FOUND)\n
"},{"location":"class_first_crud/#api-update-a-book","title":"API - Update a Book","text":""},{"location":"class_first_crud/#api-delete-a-book","title":"API - Delete a Book","text":""},{"location":"configs/","title":"Configs","text":"Panther stores all the configs in the core/configs.py
"},{"location":"configs/#monitoring","title":"MONITORING","text":"Type: bool
(Default: False
)
It should be True
if you want to use panther monitor
command and see the monitoring logs
If True
it will:
Log every request in logs/monitoring.log
"},{"location":"configs/#log_queries","title":"LOG_QUERIES","text":"Type: bool
(Default: False
)
If True
it will:
Calculate every query perf time & Log them in logs/query.log
"},{"location":"configs/#middlewares","title":"MIDDLEWARES","text":"Type: list
(Default: [ ]
)
List of middlewares you want to use
"},{"location":"configs/#authentication","title":"AUTHENTICATION","text":"Type: str | None
(Default: None
)
Every request goes through authentication()
method of this class
Example: AUTHENTICATION = 'panther.authentications.JWTAuthentication'
"},{"location":"configs/#urls","title":"URLs","text":"Type: str
(Required)
It should be the address of your urls
dict
Example: URLS = 'core.configs.urls.url_routing'
"},{"location":"configs/#default_cache_exp","title":"DEFAULT_CACHE_EXP","text":"Type: timedelta| None
(Default: None
)
We use it as default cache_exp_time
you can overwrite it in your @API
too
It is used when you set cache=True
in @API
decorator
Example: DEFAULT_CACHE_EXP = timedelta(seconds=10)
"},{"location":"configs/#throttling","title":"THROTTLING","text":"Type: Throttling | None
(Default: None
)
We use it as default throttling
you can overwrite it in your @API
too
Example: THROTTLING = Throttling(rate=10, duration=timedelta(seconds=10))
"},{"location":"configs/#user_model","title":"USER_MODEL","text":"Type: str | None
(Default: 'panther.db.models.BaseUser'
)
It is used for authentication
Example: USER_MODEL = 'panther.db.models.User'
"},{"location":"configs/#jwtconfig","title":"JWTConfig","text":"Type: dict | None
(Default: JWTConfig = {'key': SECRET_KEY}
)
We use it when you set panther.authentications.JWTAuthentication
as AUTHENTICATION
"},{"location":"configs/#background_tasks","title":"BACKGROUND_TASKS","text":"Type: bool
(Default: False
)
If True
it will:
initialize()
the background_tasks
"},{"location":"configs/#startup","title":"STARTUP","text":"Type: str | None
(Default: None
)
It should be dotted address of your startup
function, this function can be sync
or async
Example: URLS = 'core.configs.startup'
"},{"location":"configs/#shutdown","title":"SHUTDOWN","text":"Type: str | None
(Default: None
)
It should be dotted address of your shutdown
function this function can be sync
or async
Example: URLS = 'core.configs.shutdown'
"},{"location":"configs/#auto_reformat","title":"AUTO_REFORMAT","text":"Type: bool
(Default: False
)
It will reformat your code on every reload (on every change if you run the project with --reload
)
You may want to write your custom ruff.toml
in root of your project.
Reference: https://docs.astral.sh/ruff/formatter/
Example: AUTO_REFORMAT = True
"},{"location":"function_first_crud/","title":"Function Base","text":"We assume you could run the project with Introduction
Now let's write custom API Create
, Retrieve
, Update
and Delete
for a Book
:
"},{"location":"function_first_crud/#structure-requirements","title":"Structure & Requirements","text":""},{"location":"function_first_crud/#create-model","title":"Create Model","text":"Create a model named Book
in app/models.py
:
from panther.db import Model\n\n\nclass Book(Model):\n name: str\n author: str\n pages_count: int\n
"},{"location":"function_first_crud/#create-api-function","title":"Create API Function","text":"Create the book_api()
in app/apis.py
:
from panther.app import API\n\n\n@API()\nasync def book_api():\n ... \n
We are going to complete it later ...
"},{"location":"function_first_crud/#update-urls","title":"Update URLs","text":"Add the book_api
in app/urls.py
:
from app.apis import book_api\n\n\nurls = {\n 'book/': book_api,\n}\n
We assume that the urls
in core/urls.py
pointing to app/urls.py
, like below:
from app.urls import urls as app_urls\n\n\nurls = {\n '/': app_urls,\n}\n
"},{"location":"function_first_crud/#add-database-middleware","title":"Add Database Middleware","text":"Add one database middleware in core/configs.py
MIDDLEWARES
, we are going to add pantherdb
PantherDB is a Simple, FileBase and Document Oriented database:
...\n\nMIDDLEWARES = [\n ('panther.middlewares.db.DatabaseMiddleware', {'url': f'pantherdb://{BASE_DIR}/{DB_NAME}.pdb'}),\n]\n
"},{"location":"function_first_crud/#apis","title":"APIs","text":""},{"location":"function_first_crud/#api-create-a-book","title":"API - Create a Book","text":"Now we are going to create a book on post
request, We need to:
Declare request: Request
in book_api
function:
from panther.app import API\nfrom panther.request import Request\n\n\n@API()\nasync def book_api(request: Request):\n ...\n
Create serializer in app/serializers.py
, we used pydantic
for the validation
of request.data
:
from pydantic import BaseModel\n\n\nclass BookSerializer(BaseModel):\n name: str\n author: str\n pages_count: int\n
Pass the created serializer to our book_api
as input_model
so the incoming data will be validated and cleaned automatically:
from panther.app import API\nfrom panther.request import Request\n\nfrom app.serializers import BookSerializer\n\n\n@API(input_model=BookSerializer)\nasync def book_api(request: Request):\n ...\n
Now we have access to request.data
, We are going to use it like the below for ease of use, so the auto-suggest helps us in development: from panther.app import API\nfrom panther.request import Request\n\nfrom app.serializers import BookSerializer\n\n\n@API(input_model=BookSerializer)\nasync def book_api(request: Request):\n body: BookSerializer = request.validated_data\n ...\n
Now we have access to the validated data, and we can create our first book:
from panther.app import API\nfrom panther.request import Request\n\nfrom app.serializers import BookSerializer\nfrom app.models import Book\n\n\n@API(input_model=BookSerializer)\nasync def book_api(request: Request):\n body: BookSerializer = request.validated_data\n\n Book.insert_one(\n name=body.name,\n author=body.author,\n pages_count=body.pages_count,\n )\n ...\n
But we only want this happens in post
requests, so we add this condition
:
from panther.app import API\nfrom panther.request import Request\n\nfrom app.serializers import BookSerializer\nfrom app.models import Book\n\n\n@API(input_model=BookSerializer)\nasync def book_api(request: Request):\n if request.method == 'POST':\n body: BookSerializer = request.validated_data\n\n Book.create(\n name=body.name,\n author=body.author,\n pages_count=body.pages_count,\n )\n ...\n
And finally we return 201 Created
status_code as response of post
and 501 Not Implemented
for other methods:
from panther import status\nfrom panther.app import API\nfrom panther.request import Request\nfrom panther.response import Response\n\nfrom app.serializers import BookSerializer\nfrom app.models import Book\n\n\n@API(input_model=BookSerializer)\nasync def book_api(request: Request):\n if request.method == 'POST':\n body: BookSerializer = request.validated_data\n\n book: Book = Book.create(\n name=body.name,\n author=body.author,\n pages_count=body.pages_count,\n )\n return Response(data=book, status_code=status.HTTP_201_CREATED)\n\n return Response(status_code=status.HTTP_501_NOT_IMPLEMENTED)\n
The response.data can be Instance of Models
, dict
, str
, tuple
, list
, str
or None
Panther will return None
if you don't return anything as response.
"},{"location":"function_first_crud/#api-list-of-books","title":"API - List of Books","text":"We just need to add another condition on GET
methods and return the lists of books:
from panther import status\nfrom panther.app import API\nfrom panther.request import Request\nfrom panther.response import Response\n\nfrom app.serializers import BookSerializer\nfrom app.models import Book\n\n\n@API(input_model=BookSerializer)\nasync def book_api(request: Request):\n if request.method == 'POST':\n ...\n\n elif request.method == 'GET':\n books: list[Book] = Book.find()\n return Response(data=books, status_code=status.HTTP_200_OK)\n\n return Response(status_code=status.HTTP_501_NOT_IMPLEMENTED)\n
Panther validate input with input_model
, only in POST
, PUT
, PATCH
methods.
"},{"location":"function_first_crud/#filter-response-fields","title":"Filter Response Fields","text":"Assume we don't want to return field author
in response:
Create new serializer in app/serializers.py
:
from pydantic import BaseModel\n\n\nclass BookOutputSerializer(BaseModel):\n name: str\n pages_count: int\n
Add the BookOutputSerializer
as output_model
to your API()
from panther import status\nfrom panther.app import API\nfrom panther.request import Request\nfrom panther.response import Response\n\nfrom app.serializers import BookSerializer, BookOutputSerializer\nfrom app.models import Book\n\n\n@API(input_model=BookSerializer, output_model=BookOutputSerializer)\nasync def book_api(request: Request):\n if request.method == 'POST':\n ...\n\n elif request.method == 'GET':\n books: list[Book] = Book.find()\n return Response(data=books, status_code=status.HTTP_200_OK)\n\n return Response(status_code=status.HTTP_501_NOT_IMPLEMENTED)\n
Panther use the output_model
, in all methods.
"},{"location":"function_first_crud/#cache-the-response","title":"Cache The Response","text":"For caching the response, we should add cache=True
in API()
. And it will return the cached response every time till cache_exp_time
For setting a custom expiration time for API we need to add cache_exp_time
to API()
:
from datetime import timedelta\n\nfrom panther import status\nfrom panther.app import API\nfrom panther.request import Request\nfrom panther.response import Response\n\nfrom app.serializers import BookSerializer, BookOutputSerializer\nfrom app.models import Book\n\n\n@API(input_model=BookSerializer, output_model=BookOutputSerializer, cache=True, cache_exp_time=timedelta(seconds=10))\nasync def book_api(request: Request):\n if request.method == 'POST':\n ...\n\n elif request.method == 'GET':\n books: list[Book] = Book.find()\n return Response(data=books, status_code=status.HTTP_200_OK)\n\n return Response(status_code=status.HTTP_501_NOT_IMPLEMENTED)\n
Panther is going to use the DEFAULT_CACHE_EXP
from core/configs.py
if cache_exp_time
has not been set.
"},{"location":"function_first_crud/#throttle-the-request","title":"Throttle The Request","text":"For setting rate limit for requests, we can add throttling to API()
, it should be the instance of panther.throttling.Throttling
, something like below (in the below example user can't request more than 10 times in a minutes):
from datetime import timedelta\n\nfrom panther import status\nfrom panther.app import API\nfrom panther.request import Request\nfrom panther.response import Response\nfrom panther.throttling import Throttling\n\nfrom app.serializers import BookSerializer, BookOutputSerializer\nfrom app.models import Book\n\n\n@API(\n input_model=BookSerializer, \n output_model=BookOutputSerializer, \n cache=True, \n cache_exp_time=timedelta(seconds=10),\n throttling=Throttling(rate=10, duration=timedelta(minutes=1))\n)\nasync def book_api(request: Request):\n if request.method == 'POST':\n ...\n\n elif request.method == 'GET':\n books: list[Book] = Book.find()\n return Response(data=books, status_code=status.HTTP_200_OK)\n\n return Response(status_code=status.HTTP_501_NOT_IMPLEMENTED)\n
"},{"location":"function_first_crud/#api-retrieve-a-book","title":"API - Retrieve a Book","text":"For retrieve
, update
and delete
API, we are going to
Create another api named single_book_api
in app/apis.py
:
from panther.app import API\nfrom panther.request import Request\n\n\n@API()\nasync def single_book_api(request: Request):\n ...\n
Add it in app/urls.py
:
from app.apis import book_api, single_book_api\n\n\nurls = {\n 'book/': book_api,\n 'book/<book_id>/': single_book_api,\n}\n
You should write the Path Variable in <
and >
You should have the parameter with the same name of path variable
in you api
with normal type hints
Panther will convert type of the path variable
to your parameter type, then pass it
Complete the api:
from panther import status\nfrom panther.app import API\nfrom panther.request import Request\nfrom panther.response import Response\n\nfrom app.models import Book\n\n\n@API()\nasync def single_book_api(request: Request, book_id: int):\n if request.method == 'GET':\n if book := Book.find_one(id=book_id):\n return Response(data=book, status_code=status.HTTP_200_OK)\n else:\n return Response(status_code=status.HTTP_404_NOT_FOUND)\n
"},{"location":"function_first_crud/#api-update-a-book","title":"API - Update a Book","text":""},{"location":"function_first_crud/#api-delete-a-book","title":"API - Delete a Book","text":""},{"location":"log_queries/","title":"Log Queries","text":"Variable: LOG_QUERIES
Type: bool
Default: False
Panther has a log_query
decorator on queries that process the perf_time
of every query
Make sure it is False
on production for better performance
"},{"location":"log_queries/#log-example","title":"Log Example:","text":"INFO: | 2023-03-19 20:37:27 | Query --> User.insert_one() --> 1.6 ms\n
"},{"location":"log_queries/#the-log-query-decorator-is-something-like-this","title":"The Log Query Decorator Is Something Like This","text":"def log_query(func):\n def log(*args, **kwargs):\n if config['log_queries'] is False:\n return func(*args, **kwargs)\n\n start = perf_counter()\n response = func(*args, **kwargs)\n end = perf_counter()\n class_name = ...\n query_logger.info(f'Query --> {class_name}.{func.__name__}() --> {(end - start) * 1_000:.2} ms')\n return response\n return log\n
"},{"location":"middlewares/","title":"Middlewares","text":"Variable: MIDDLEWARES
Type: list
Default: []
Panther has several built-in
middleware:
Database Middleware
Redis Middleware
And you can write your own custom middlewares too
"},{"location":"middlewares/#structure-of-middlewares","title":"Structure of middlewares","text":"MIDDLEWARES
itself is a list
of tuples
which each tuple
is like below:
(Dotted Address of The Middleware Class
, kwargs as dict
)
"},{"location":"middlewares/#database-middleware","title":"Database Middleware","text":"This middleware will create a db
connection which is used in ODM
and you can use it manually too, it gives you a database connection:
from panther.db.connection import db\n
We only support 2 database for now: PantherDB
& MongoDB
Address of Middleware: panther.middlewares.db.DatabaseMiddleware
kwargs:
Example of PantherDB
(Built-in Local Storage
):
MIDDLEWARES = [\n ('panther.middlewares.db.DatabaseMiddleware', {'url': 'pantherdb://project_directory/database.pdb'}),\n]\n
Example of MongoDB
: MIDDLEWARES = [\n ('panther.middlewares.db.DatabaseMiddleware', {'url': 'mongodb://127.0.0.1:27017/example'}),\n]\n
"},{"location":"middlewares/#redis-middleware","title":"Redis Middleware","text":" Address of Middleware: panther.middlewares.redis.RedisMiddleware
kwargs:
{'host': '127.0.0.1', 'port': 6379, ...}\n
Example
MIDDLEWARES = [\n ('panther.middlewares.redis.RedisMiddleware', {'host': '127.0.0.1', 'port': 6379}),\n]\n
"},{"location":"middlewares/#custom-middleware","title":"Custom Middleware","text":""},{"location":"middlewares/#middleware-types","title":"Middleware Types","text":"We have 3 type of Middlewares, make sure that you are inheriting from the correct one: - Base Middleware
: which is used for both websocket
and http
requests - HTTP Middleware
: which is only used for http
requests - Websocket Middleware
: which is only used for websocket
requests
"},{"location":"middlewares/#write-custom-middleware","title":"Write Custom Middleware","text":" Write a class
and inherit from one of the classes below
# For HTTP Requests\nfrom panther.middlewares.base import HTTPMiddleware\n\n# For Websocket Requests\nfrom panther.middlewares.base import WebsocketMiddleware\n\n# For Both HTTP and Websocket Requests\nfrom panther.middlewares.base import BaseMiddleware\n
Then you can write your custom before()
and after()
methods
The methods
should be async
before()
should have request
parameter after()
should have response
parameter overwriting the before()
and after()
are optional The methods
can get kwargs
from their __init__
"},{"location":"middlewares/#custom-http-middleware-example","title":"Custom HTTP Middleware Example","text":" core/middlewares.py
from panther.middlewares.base import HTTPMiddleware\nfrom panther.request import Request\nfrom panther.response import Response\n\n\nclass CustomMiddleware(HTTPMiddleware):\n\n def __init__(self, something):\n self.something = something\n\n async def before(self, request: Request) -> Request:\n print('Before Endpoint', self.something)\n return request\n\n async def after(self, response: Response) -> Response:\n print('After Endpoint', self.something)\n return response\n
core/configs.py
MIDDLEWARES = [\n ('core.middlewares.CustomMiddleware', {'something': 'hello-world'}),\n]\n
"},{"location":"middlewares/#custom-http-websocket-middleware-example","title":"Custom HTTP + Websocket Middleware Example","text":" core/middlewares.py
from panther.middlewares.base import BaseMiddleware\nfrom panther.request import Request\nfrom panther.response import Response\nfrom panther.websocket import GenericWebsocket \n\n\nclass SayHiMiddleware(BaseMiddleware):\n\n def __init__(self, name):\n self.name = name\n\n async def before(self, request: Request | GenericWebsocket) -> Request | GenericWebsocket:\n print('Hello ', self.name)\n return request\n\n async def after(self, response: Response | GenericWebsocket) -> Response | GenericWebsocket:\n print('Goodbye ', self.name)\n return response\n
core/configs.py
MIDDLEWARES = [\n ('core.middlewares.SayHiMiddleware', {'name': 'Ali Rn'}),\n]\n
"},{"location":"monitoring/","title":"Monitoring","text":"Variable: MONITORING
Type: bool
Default: False
Panther has a Monitoring
middleware that process the perf_time
of every request
It will create a monitoring.log
file and log the records
Then you can watch them live with: panther monitor
"},{"location":"monitoring/#log-example","title":"Log Example:","text":"date time | method | path | ip:port | response_time [ms, s] | status\n\n2023-12-11 18:23:42 | GET | /login | 127.0.0.1:55710 | 2.8021 ms | 200\n
"},{"location":"panther_odm/","title":"Panther ODM","text":""},{"location":"panther_odm/#find_one","title":"find_one","text":" Find the first match document Example:
user: User = User.find_one(id=1, name='Ali') \n\nuser: User = User.find_one({'id': 1, 'name': 'Ali'}) \n\nuser: User = User.find_one({'id': 1}, name='Ali') \n
"},{"location":"panther_odm/#find","title":"find","text":" Find all the matches documents Example:
users: list[User] = User.find(id=1, name='Ali') \n\nusers: list[User] = User.find({'id': 1, 'name': 'Ali'}) \n\nusers: list[User] = User.find({'id': 1}, name='Ali') \n
"},{"location":"panther_odm/#all","title":"all","text":""},{"location":"panther_odm/#insert_one","title":"insert_one","text":" Insert only one document into database Example:
User.insert_one(id=1, name='Ali') \n\nUser.insert_one({'id': 1, 'name': 'Ali'}) \n\nUser.insert_one({'id': 1}, name='Ali') \n
"},{"location":"panther_odm/#delete","title":"delete","text":""},{"location":"panther_odm/#delete_one","title":"delete_one","text":""},{"location":"panther_odm/#delete_many","title":"delete_many","text":" Delete all the matches document from database Example:
deleted_count: int = User.delete_many(id=1, name='Ali')\n\ndeleted_count: int = User.delete_many({'id': 1}, name='Ali')\n\ndeleted_count: int = User.delete_many({'id': 1, 'name': 'Ali'})\n
"},{"location":"panther_odm/#update","title":"update","text":""},{"location":"panther_odm/#update_one","title":"update_one","text":" Update the first match document in database You should filter with dictionary
as first parameter
and pass the fields you want to update as kwargs
or another dictionary
as second parameter
Example:
is_updated: bool = User.update_one({'id': 1, 'name': 'Ali'}, name='Saba', age=26)\n\nis_updated: bool = User.update_one({'id': 1, 'name': 'Ali'}, {'name': 'Saba', 'age': 26})\n\nis_updated: bool = User.update_one({'id': 1, 'name': 'Ali'}, {'name': 'Saba'}, age=26)\n
"},{"location":"panther_odm/#update_many","title":"update_many","text":" Update all the matches document in database You should filter with dictionary
as first parameter
and pass the fields you want to update as kwargs
or another dictionary
as second parameter
Example:
updated_count: int = User.update_many({'name': 'Ali'}, name='Saba', age=26)\n\nupdated_count: int = User.update_many({'name': 'Ali'}, {'name': 'Saba', 'age': 26})\n\nupdated_count: int = User.update_many({'name': 'Ali'}, {'name': 'Saba'}, age=26)\n
"},{"location":"panther_odm/#last","title":"last","text":" Find the last match document Example:
user: User = User.last(name='Ali', age=26) \n\nuser: User = User.last({'name': 'Ali', 'age': 26}) \n\nuser: User = User.last({'name': 'Ali'}, age=26) \n
"},{"location":"panther_odm/#count","title":"count","text":""},{"location":"panther_odm/#find_or_insert","title":"find_or_insert","text":""},{"location":"panther_odm/#find_one_or_raise","title":"find_one_or_raise","text":""},{"location":"release_notes/","title":"Release Notes","text":""},{"location":"release_notes/#370","title":"3.7.0","text":""},{"location":"release_notes/#360","title":"3.6.0","text":" Use observable
pattern for loading database middleware and inheritance of the Query
class Remove IDType
from the Model
Change encrypt_password()
method, now uses scrypt
+ md5
"},{"location":"release_notes/#351","title":"3.5.1","text":" Set default behavior of GenericWebsocket.connect
to ignore the connection (reject
) "},{"location":"release_notes/#350","title":"3.5.0","text":""},{"location":"release_notes/#340","title":"3.4.0","text":" Support WebsocketMiddleware
"},{"location":"release_notes/#332","title":"3.3.2","text":" Add content-length
to response header "},{"location":"release_notes/#331","title":"3.3.1","text":" Check ruff
installation on startup Fix an issue in routing
"},{"location":"release_notes/#330","title":"3.3.0","text":""},{"location":"release_notes/#324","title":"3.2.4","text":" Add all() query Add tests for pantherdb
, load_configs()
, status.py
, Panel
, multipart
, request headers
Refactor Headers()
class Check uvloop
installation on Panther init
Minor Improvement "},{"location":"release_notes/#321","title":"3.2.1","text":""},{"location":"release_notes/#320","title":"3.2.0","text":" Support Startup
& Shutdown
Events "},{"location":"release_notes/#315","title":"3.1.5","text":" Support Websocket
in the monitoring
Refactor collect_all_models()
"},{"location":"release_notes/#314","title":"3.1.4","text":" Check ws redis connection on the init
Refactor Monitoring
class and usage Improve logging
config Check database connection before query "},{"location":"release_notes/#313","title":"3.1.3","text":" Add Image
base class Add size
to File
base class Improve the way of loading configs
in single-file
structure Improve background_tasks.py
, generate_ws_connection_id()
bpython
removed from being the default python shell Improve load_middlewares()
error handling Print configs
on the run
Add requirements.txt
for development Update roadmap.jpg
, README.md
"},{"location":"release_notes/#312","title":"3.1.2","text":" Add new methods to BackgroundTask
every_seconds()
every_minutes()
every_hours()
every_days()
every_weeks()
at()
"},{"location":"release_notes/#311","title":"3.1.1","text":" Upgrade PantherDB
version Add first()
, last()
queries "},{"location":"release_notes/#310","title":"3.1.0","text":""},{"location":"release_notes/#303","title":"3.0.3","text":" Add find_one_or_raise
query Add last_login
to BaseUser
Add refresh_life_time
to JWTConfig
Add encode_refresh_token()
to JWTAuthentication
Add encrypt_password()
Handle PantherException
Handle RedisConnection
without connection_pool
"},{"location":"release_notes/#302","title":"3.0.2","text":" Added 'utf-8' encoding while opening the file \"README.md\" in setup.py Fixed panther shell not working issue in windows. Added a condition to raise error if no argument is passed to panther command in cli. "},{"location":"release_notes/#301","title":"3.0.1","text":" Assume content-type is 'application/json' if it was empty Fix an issue on creating instance of model when query is done "},{"location":"release_notes/#300","title":"3.0.0","text":" Support Websocket Implement Built-in TestClient Support Single-File Structure Support bytes
as Response.data
Add methods
to API()
Change Request.pure_data
to Request.data
Change Request.data
to Request.validated_data
Change panther.middlewares.db.Middleware
to panther.middlewares.db.DatabaseMiddleware
Change panther.middlewares.redis.Middleware
to panther.middlewares.redis.RedisMiddleware
Fix panther run
command Minor Improvement "},{"location":"release_notes/#242","title":"2.4.2","text":" Don't log content-type when it's not supported "},{"location":"release_notes/#241","title":"2.4.1","text":" Fix an issue in collect_all_models() in Windows "},{"location":"release_notes/#240","title":"2.4.0","text":" Handle Complex Multipart-FormData "},{"location":"release_notes/#233","title":"2.3.3","text":" Fix a bug in response headers "},{"location":"release_notes/#232","title":"2.3.2","text":""},{"location":"release_notes/#231","title":"2.3.1","text":" Handle PlainTextResponse Handle Custom Header in Response Change the way of accepting 'URLs' in configs (relative -> dotted) Fix an issue in collect_all_models() "},{"location":"release_notes/#230","title":"2.3.0","text":""},{"location":"release_notes/#220","title":"2.2.0","text":""},{"location":"release_notes/#216","title":"2.1.6","text":" Fix validation errors on nested inputs "},{"location":"release_notes/#215","title":"2.1.5","text":" Fix response of nested Models in _panel//"},{"location":"release_notes/#214","title":"2.1.4","text":" Add access-control-allow-origin to response header "},{"location":"release_notes/#213","title":"2.1.3","text":" Upgrade greenlet version in requirements for python3.12 "},{"location":"release_notes/#212","title":"2.1.2","text":" Add ruff.toml Add Coverage to workflows Fix a bug for running in Windows "},{"location":"release_notes/#211","title":"2.1.1","text":" Fix a bug in main.py imports "},{"location":"release_notes/#210","title":"2.1.0","text":""},{"location":"release_notes/#200","title":"2.0.0","text":" Supporting class-base APIs "},{"location":"release_notes/#1720","title":"1.7.20","text":" Fix an issue in find_endpoint() "},{"location":"release_notes/#1719","title":"1.7.19","text":" Fix an issue in routing Fix an issue on return complex dict Response "},{"location":"release_notes/#1718","title":"1.7.18","text":" Remove uvloop from requirements for now (we had issue in windows) "},{"location":"release_notes/#1716","title":"1.7.16","text":" Trying to fix requirements for windows Minor improvement in BaseMongoDBQuery "},{"location":"release_notes/#1715","title":"1.7.15","text":" Fix an issue in handling form-data "},{"location":"release_notes/#1714","title":"1.7.14","text":" Add Cache and Throttling doc to FirstCrud Fix an issue in BasePantherDBQuery._merge() "},{"location":"release_notes/#1713","title":"1.7.13","text":" Hotfix validation of _id in Model() "},{"location":"release_notes/#1712","title":"1.7.12","text":""},{"location":"release_notes/#1711","title":"1.7.11","text":""},{"location":"release_notes/#1710","title":"1.7.10","text":" Fix a bug in collect_urls
and rename it to flatten_urls
Add General Tests Compatible with python3.10 (Not Tested) Working on docs "},{"location":"release_notes/#179","title":"1.7.9","text":""},{"location":"release_notes/#178","title":"1.7.8","text":""},{"location":"release_notes/#178_1","title":"1.7.8","text":""},{"location":"release_notes/#177","title":"1.7.7","text":""},{"location":"release_notes/#175","title":"1.7.5","text":" Change the way of raising exception in JWTAuthentication Rename User model to BaseUser Fix template "},{"location":"release_notes/#174","title":"1.7.4","text":""},{"location":"release_notes/#173","title":"1.7.3","text":" Add Throttling Doc Fix some issue in Doc "},{"location":"release_notes/#172","title":"1.7.2","text":" Add Throttling to example Customize install_requires in setup.py Improve monitoring cli command "},{"location":"release_notes/#171","title":"1.7.1","text":" Rename db BaseModel to Model Add more docs "},{"location":"release_notes/#170","title":"1.7.0","text":""},{"location":"release_notes/#161","title":"1.6.1","text":""},{"location":"release_notes/#160","title":"1.6.0","text":""},{"location":"release_notes/#152","title":"1.5.2","text":" Improve Response data serialization Fix a bug in JWTAuthentication "},{"location":"release_notes/#151","title":"1.5.1","text":""},{"location":"release_notes/#150","title":"1.5.0","text":" Refactor Mongodb ODM Minor Improvement "},{"location":"release_notes/#140","title":"1.4.0","text":""},{"location":"release_notes/#132","title":"1.3.2","text":" Add Uvicorn to the setup requirements Update Readme "},{"location":"release_notes/#131","title":"1.3.1","text":" Fix a bug in project creation template Fix a bug in caching "},{"location":"release_notes/#130","title":"1.3.0","text":" Add PantherDB to Panther Remove tinydb "},{"location":"release_notes/#127","title":"1.2.7","text":" Fix a bug while using tinydb "},{"location":"release_notes/#126","title":"1.2.6","text":""},{"location":"release_notes/#125","title":"1.2.5","text":" Fix install_requires issue Add benchmarks to docs "},{"location":"release_notes/#124","title":"1.2.4","text":" Remove Uvicorn From install_requires Working on Docs "},{"location":"release_notes/#123","title":"1.2.3","text":""},{"location":"release_notes/#121","title":"1.2.1","text":" Path Variable Handled Successfully "},{"location":"release_notes/#120","title":"1.2.0","text":" Read multipart/form-data with Regex "},{"location":"release_notes/#119","title":"1.1.9","text":" Refactoring code style with ruff Add asyncio.TaskGroup() "},{"location":"release_notes/#118","title":"1.1.8","text":""},{"location":"release_notes/#117","title":"1.1.7","text":" Add benchmark pictures to doc "},{"location":"release_notes/#115","title":"1.1.5","text":" Clean Readme Clean main.py "},{"location":"release_notes/#114","title":"1.1.4","text":""},{"location":"release_notes/#113","title":"1.1.3","text":""},{"location":"release_notes/#112","title":"1.1.2","text":" Add delete_many query to TinyDB "},{"location":"release_notes/#111","title":"1.1.1","text":""},{"location":"release_notes/#110","title":"1.1.0","text":""},{"location":"release_notes/#109","title":"1.0.9","text":" Handle string exceptions (raise them as detail: error) Little debug on MongoQueries "},{"location":"release_notes/#107","title":"1.0.7","text":" Working on queries Fix a bug in query methods "},{"location":"release_notes/#106","title":"1.0.6","text":""},{"location":"release_notes/#104","title":"1.0.4","text":""},{"location":"release_notes/#102","title":"1.0.2","text":" Add global config Split the BaseModels Worked on MongoQuery Set Mongo as default database while creating project Minor Improvement "},{"location":"release_notes/#101","title":"1.0.1","text":""},{"location":"release_notes/#10","title":"1.0.","text":" Refactor & Complete the CLI "},{"location":"release_notes/#019","title":"0.1.9","text":""},{"location":"release_notes/#018","title":"0.1.8","text":""},{"location":"release_notes/#017","title":"0.1.7","text":""},{"location":"release_notes/#016","title":"0.1.6","text":" Handle Most Types as Data in Response "},{"location":"release_notes/#014","title":"0.1.4","text":""},{"location":"release_notes/#001","title":"0.0.1","text":""},{"location":"serializer/","title":"Serializer","text":"You can write your serializer
in 2 style:
"},{"location":"serializer/#style-1-pydantic","title":"Style 1 (Pydantic)","text":"Write a normal pydantic
class and use it as serializer:
from pydantic import BaseModel\nfrom pydantic import Field\n\nfrom panther.app import API\nfrom panther.request import Request\nfrom panther.response import Response\n\n\nclass UserSerializer(BaseModel):\n username: str\n password: str\n first_name: str = Field(default='', min_length=2)\n last_name: str = Field(default='', min_length=4)\n\n\n@API(input_model=UserSerializer)\nasync def serializer_example(request: Request):\n return Response(data=request.validated_data)\n
"},{"location":"serializer/#style-2-model-serializer","title":"Style 2 (Model Serializer)","text":"Use panther ModelSerializer
to write your serializer which will use your model
fields as its fields, and you can say which fields are required
from pydantic import Field\n\nfrom panther import status\nfrom panther.app import API\nfrom panther.db import Model\nfrom panther.request import Request\nfrom panther.response import Response\nfrom panther.serializer import ModelSerializer\n\n\nclass User(Model):\n username: str\n password: str\n first_name: str = Field(default='', min_length=2)\n last_name: str = Field(default='', min_length=4)\n\n\nclass UserModelSerializer(metaclass=ModelSerializer, model=User):\n fields = ['username', 'first_name', 'last_name']\n required_fields = ['first_name']\n\n\n@API(input_model=UserModelSerializer)\nasync def model_serializer_example(request: Request):\n return Response(data=request.validated_data, status_code=status.HTTP_202_ACCEPTED)\n
"},{"location":"serializer/#notes","title":"Notes:","text":" In the example above UserModelSerializer
only accepts the values of fields
attribute
In default the UserModelSerializer.fields
are same as User.fields
but you can change their default and make them required with required_fields
attribute
If you want uses required_fields
you have to put them in fields
too.
fields
attribute is required
when you are using ModelSerializer
as metaclass
model=
is required when you are using ModelSerializer
as metaclass
You have to use ModelSerializer
as metaclass
(not as a parent)
Panther is going to create a pydantic
model as your UserModelSerializer
in the startup
"},{"location":"single_file/","title":"Single-File","text":"If you want to work with Panther
in a single-file
structure, follow the steps below.
"},{"location":"single_file/#steps","title":"Steps","text":" Write your APIs
as you like
from panther.app import API\n\n@API()\nasync def hello_world_api():\n return {'detail': 'Hello World'}\n
Add your APIs
to a dict
(ex: url_routing
)
from panther.app import API\n\n@API()\nasync def hello_world_api():\n return {'detail': 'Hello World'}\n\nurl_routing = {\n '/': hello_world_api,\n}\n
3. Create an app
and pass your current module name
and urls
to it.
from panther import Panther\nfrom panther.app import API\n\n@API()\nasync def hello_world_api():\n return {'detail': 'Hello World'}\n\nurl_routing = {\n '/': hello_world_api,\n}\n\napp = Panther(__name__, configs=__name__, urls=url_routing)\n
4. Run the project panther run \n
URLs
is a required config unless you pass the urls
to the Panther
When you pass the configs
to the Panther(configs=...)
, Panther is going to load the configs from this file, else it is going to load core/configs.py
file
You can pass the startup
and shutdown
functions to the Panther()
too.
from panther import Panther\nfrom panther.app import API\n\n@API()\nasync def hello_world_api():\n return {'detail': 'Hello World'}\n\nurl_routing = {\n '/': hello_world_api,\n}\n\ndef startup():\n pass\n\ndef shutdown():\n pass\n\napp = Panther(__name__, configs=__name__, urls=url_routing, startup=startup, shutdown=shutdown)\n
"},{"location":"throttling/","title":"Throttling","text":"Variable: THROTTLING
Type: str
In Panther, you can use Throttling
for all APIs at once in core/configs.py
or per API in its @API
decorator
The Throttling
class has 2 field rate
& duration
rate: int
duration: datetime.timedelta
It will return Too Many Request
status_code: 429
if user try to request in the duration
more than rate
And user will baned( getToo Many Request
) for duration
And keep that in mind if you have Throttling
in @API()
, the Throttling
of core/configs.py
will be ignored.
"},{"location":"throttling/#for-all-apis-example","title":"For All APIs Example:","text":"core/configs.py
from datetime import timedelta\n\nfrom panther.throttling import Throttling\n\n\n# User only can request 5 times in every minute\nTHROTTLING = Throttling(rate=5, duration=timedelta(minutes=1))\n
"},{"location":"throttling/#for-single-api-example","title":"For Single API Example:","text":"apis.py
from datetime import timedelta\n\nfrom panther.throttling import Throttling\nfrom panther.app import API\n\n\n# User only can request 5 times in every minute\nInfoThrottling = Throttling(rate=5, duration=timedelta(minutes=1))\n\n\n@API(throttling=InfoThrottling)\nasync def info_api():\n pass\n
"},{"location":"todos/","title":"TODOs","text":""},{"location":"todos/#base","title":"Base","text":" \u2705 Start with Uvicorn \u2705 Fix URL Routing \u2705 Read Configs \u2705 Handle Exceptions \u2705 Add Custom Logger \u2705 Request Class \u2705 Response Class \u2705 Validate Input \u2705 Custom Output Model \u2705 Log Queries \u2705 Add Package Requirements \u2705 Custom Logging \u2705 Caching \u2705 Handle Path Variable \u2705 Handle Simple Multipart-FormData \u2705 Handle Throttling \u2705 Handle ClassBase APIs \u2705 Handle File \u2705 Handle Complex Multipart-FormData \u2705 Handle Testing \u2705 Handle WS \u2610 Handle Cookie \u2610 Generate Swagger For APIs "},{"location":"todos/#database","title":"Database:","text":" \u2705 Structure Of DB Connection \u2705 PantherDB Connection \u2705 MongoDB Connection \u2705 Create Custom BaseModel For All Type Of Databases \u2705 Set PantherDB As Default "},{"location":"todos/#custom-odm","title":"Custom ODM","text":" \u2705 Find One \u2705 Find \u2705 Last \u2705 Count \u2705 Insert One \u2705 Insert Many \u2705 Delete One \u2705 Delete Many \u2705 Delete Itself \u2705 Update One \u2705 Update Many \u2705 Update Itself \u2705 Find or Insert \u2705 Find or Raise \u2705 Save \u2610 Find with Pagination \u2610 Aggregation \u2610 Complex Pipelines \u2610 ... "},{"location":"todos/#middleware","title":"Middleware","text":" \u2705 Add Middlewares To Structure \u2705 Create BaseMiddleware \u2705 Pass Custom Parameters To Middlewares \u2705 Handle Custom Middlewares "},{"location":"todos/#authentication","title":"Authentication","text":" \u2705 JWT Authentication \u2705 Separate Auth For Every API \u2705 Handle Permissions \u2610 Token Storage Authentication \u2610 Cookie Authentication \u2610 Query Param Authentication \u2610 Store JWT After Logout In Redis/ Memory "},{"location":"todos/#cache","title":"Cache","text":" \u2705 Add Redis To Structure \u2705 Create Cache Decorator \u2705 Handle In Memory Caching \u2705 Handle In Redis Caching \u2610 Write Async LRU_Caching With TTL (Replace it with in memory ...) "},{"location":"todos/#cli","title":"CLI","text":" \u2705 Create Project \u2705 Run Project \u2705 Create Project with Options \u2705 Monitoring With Textual \u2705 Monitor Requests, Response & Time \u2610 Create Project With TUI "},{"location":"todos/#documentation","title":"Documentation","text":" \u2705 Create MkDocs For Project \u2705 Benchmarks \u2705 Release Notes \u2705 Features \u2610 Complete The MkDoc "},{"location":"todos/#tests","title":"Tests","text":" \u2705 Start Writing Tests For Panther \u2705 Test Client "},{"location":"urls/","title":"URLs","text":"Variable: URLs
Type: str
Required: True
"},{"location":"urls/#path-variables-are-handled-like-below","title":"Path Variables are handled like below:","text":" <variable_name
> Example: user/<user_id>/blog/<title>/
The endpoint
should have parameters with those names too Example: async def profile_api(user_id: int, title: str):
"},{"location":"urls/#example","title":"Example","text":" core/configs.py URLs = 'core.urls.url_routing\n
core/urls.py from app.urls import app_urls\n\nurl_routing = {\n 'user/': app_urls,\n}\n
app/urls.py
from app.apis import *\n\nurls = {\n 'login/': login_api,\n 'logout/': logout_api,\n 'profile/<user_id>/': profile_api,\n}\n
app/apis.py
...\n\n@API()\nasync def profile_api(user_id: int):\n return User.find_one(id=user_id)\n
"},{"location":"websocket/","title":"WebSocket","text":"Panther supports WebSockets
routing just like APIs
"},{"location":"websocket/#structure-requirements","title":"Structure & Requirements","text":""},{"location":"websocket/#create-websocket-class","title":"Create WebSocket Class","text":"Create the BookWebsocket()
in app/websockets.py
which inherited from GenericWebsocket
:
from panther.websocket import GenericWebsocket\n\n\nclass BookWebsocket(GenericWebsocket):\n async def connect(self):\n await self.accept()\n print(f'{self.connection_id=}')\n\n async def receive(self, data: str | bytes = None):\n # Just Echo The Message\n await self.send(data=data)\n
We are going to discuss it below ...
"},{"location":"websocket/#update-urls","title":"Update URLs","text":"Add the BookWebsocket()
in app/urls.py
:
from app.websockets import BookWebsocket\n\n\nurls = {\n 'ws/book/': BookWebsocket,\n}\n
"},{"location":"websocket/#how-it-works","title":"How It Works?","text":" Client tries to connect to your ws/book/
url with websocket
protocol The connect()
method of your BookWebsocket
is going to call You should validate the connection with self.headers
, self.query_params
or etc Then accept()
the connection with self.accept()
otherwise it is going to be rejected
by default. Now you can see the unique connection_id
which is specified to this user with self.connection_id
, you may want to store it somewhere (db
, cache
, or etc.) If the client sends you any message, you will receive it in receive()
method, the client message can be str
or bytes
. If you want to send anything to the client: If you want to use webscoket
in multi-tread
or multi-instance
backend, you should add RedisMiddleware
in your configs
or it won't work well. [Adding Redis Middleware] If you want to close a connection:
In websocket class scope: You can close connection with self.close()
method which takes 2 args, code
and reason
: from panther import status\nawait self.close(code=status.WS_1000_NORMAL_CLOSURE, reason='I just want to close it')\n
Out of websocket class scope (Not Recommended): You can close it with close_websocket_connection()
from panther.websocket
, it's async
function with takes 3 args, connection_id
, code
and reason
, like below: from panther import status\nfrom panther.websocket import close_websocket_connection\nawait close_websocket_connection(connection_id='7e82d57c9ec0478787b01916910a9f45', code=status.WS_1008_POLICY_VIOLATION, reason='')\n
Path Variables
will be passed to connect()
:
from panther.websocket import GenericWebsocket\n\n class UserWebsocket(GenericWebsocket):\n async def connect(self, user_id: int, room_id: str):\n await self.accept()\n\n url = {\n '/ws/<user_id>/<room_id>/': UserWebsocket \n }\n
WebSocket Echo Example -> Https://GitHub.com/PantherPy/echo_websocket Enjoy. "},{"location":"working_with_db/","title":"Working With Database","text":"Panther create a database connection depends on database middleware you are using on core/configs.py
and you can access to this connection from your models
or direct access from from panther.db.connection import db
Now we are going to create a new API which uses our default database(PantherDB
) and creating a Book
Create Book
model in app/models.py
from panther.db import Model\n\n\nclass Book(Model):\n title: str\n description: str\n pages_count: int\n
Add book
url in app/urls.py
that points to book_api()
...\nfrom app.apis import time_api, book_api\n\n\nurls = {\n '': hello_world,\n 'info/': info,\n 'time/': time_api,\n 'book/': book_api,\n}\n
Create book_api()
in app/apis.py
from panther import status\nfrom panther.app import API\nfrom panther.response import Response\n\n\n@API()\nasync def book_api():\n ...\n return Response(status_code=status.HTTP_201_CREATED) \n
Now we should use the Panther ODM to create a book, it's based on mongo queries, for creation we use insert_one
like this:
from panther import status\nfrom panther.app import API\nfrom panther.response import Response\nfrom app.models import Book\n\n\n@API()\nasync def book_api():\n Book.insert_one(\n title='Python',\n description='Python is good.',\n pages_count=10\n )\n return Response(status_code=status.HTTP_201_CREATED) \n
In next step we are going to explain more about Panther ODM
"}]}
\ No newline at end of file
diff --git a/serializer/index.html b/serializer/index.html
new file mode 100644
index 0000000..e47ff43
--- /dev/null
+++ b/serializer/index.html
@@ -0,0 +1,930 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Serializer - Panther
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Serializer
+
+You can write your serializer
in 2 style:
+Style 1 (Pydantic)
+Write a normal pydantic
class and use it as serializer:
+from pydantic import BaseModel
+from pydantic import Field
+
+from panther.app import API
+from panther.request import Request
+from panther.response import Response
+
+
+class UserSerializer ( BaseModel ):
+ username : str
+ password : str
+ first_name : str = Field ( default = '' , min_length = 2 )
+ last_name : str = Field ( default = '' , min_length = 4 )
+
+
+@API ( input_model = UserSerializer )
+async def serializer_example ( request : Request ):
+ return Response ( data = request . validated_data )
+
+Style 2 (Model Serializer)
+Use panther ModelSerializer
to write your serializer which will use your model
fields as its fields, and you can say which fields are required
+from pydantic import Field
+
+from panther import status
+from panther.app import API
+from panther.db import Model
+from panther.request import Request
+from panther.response import Response
+from panther.serializer import ModelSerializer
+
+
+class User ( Model ):
+ username : str
+ password : str
+ first_name : str = Field ( default = '' , min_length = 2 )
+ last_name : str = Field ( default = '' , min_length = 4 )
+
+
+class UserModelSerializer ( metaclass = ModelSerializer , model = User ):
+ fields = [ 'username' , 'first_name' , 'last_name' ]
+ required_fields = [ 'first_name' ]
+
+
+@API ( input_model = UserModelSerializer )
+async def model_serializer_example ( request : Request ):
+ return Response ( data = request . validated_data , status_code = status . HTTP_202_ACCEPTED )
+
+Notes:
+
+
+In the example above UserModelSerializer
only accepts the values of fields
attribute
+
+
+In default the UserModelSerializer.fields
are same as User.fields
but you can change their default and make them required with required_fields
attribute
+
+
+If you want uses required_fields
you have to put them in fields
too.
+
+
+fields
attribute is required
when you are using ModelSerializer
as metaclass
+
+
+model=
is required when you are using ModelSerializer
as metaclass
+
+
+You have to use ModelSerializer
as metaclass
(not as a parent)
+
+
+Panther is going to create a pydantic
model as your UserModelSerializer
in the startup
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/single_file/index.html b/single_file/index.html
index dabb870..0cc9b3e 100644
--- a/single_file/index.html
+++ b/single_file/index.html
@@ -420,6 +420,26 @@
+
+
+
+
+
+ Serializer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sitemap.xml b/sitemap.xml
index f4fffd6..7786bc6 100644
--- a/sitemap.xml
+++ b/sitemap.xml
@@ -2,97 +2,102 @@
https://pantherpy.github.io/
- 2024-01-04
+ 2024-01-23
daily
https://pantherpy.github.io/authentications/
- 2024-01-04
+ 2024-01-23
daily
https://pantherpy.github.io/background_tasks/
- 2024-01-04
+ 2024-01-23
daily
https://pantherpy.github.io/caching/
- 2024-01-04
+ 2024-01-23
daily
https://pantherpy.github.io/class_first_crud/
- 2024-01-04
+ 2024-01-23
daily
https://pantherpy.github.io/configs/
- 2024-01-04
+ 2024-01-23
daily
https://pantherpy.github.io/function_first_crud/
- 2024-01-04
+ 2024-01-23
daily
https://pantherpy.github.io/log_queries/
- 2024-01-04
+ 2024-01-23
daily
https://pantherpy.github.io/middlewares/
- 2024-01-04
+ 2024-01-23
daily
https://pantherpy.github.io/monitoring/
- 2024-01-04
+ 2024-01-23
daily
https://pantherpy.github.io/panther_odm/
- 2024-01-04
+ 2024-01-23
daily
https://pantherpy.github.io/release_notes/
- 2024-01-04
+ 2024-01-23
+ daily
+
+
+ https://pantherpy.github.io/serializer/
+ 2024-01-23
daily
https://pantherpy.github.io/single_file/
- 2024-01-04
+ 2024-01-23
daily
https://pantherpy.github.io/throttling/
- 2024-01-04
+ 2024-01-23
daily
https://pantherpy.github.io/todos/
- 2024-01-04
+ 2024-01-23
daily
https://pantherpy.github.io/urls/
- 2024-01-04
+ 2024-01-23
daily
https://pantherpy.github.io/user_model/
- 2024-01-04
+ 2024-01-23
daily
https://pantherpy.github.io/websocket/
- 2024-01-04
+ 2024-01-23
daily
https://pantherpy.github.io/working_with_db/
- 2024-01-04
+ 2024-01-23
daily
\ No newline at end of file
diff --git a/sitemap.xml.gz b/sitemap.xml.gz
index dd9df6f..9956e96 100644
Binary files a/sitemap.xml.gz and b/sitemap.xml.gz differ
diff --git a/throttling/index.html b/throttling/index.html
index 4bcfcb7..cc30e49 100644
--- a/throttling/index.html
+++ b/throttling/index.html
@@ -420,6 +420,26 @@
+
+
+
+
+
+ Serializer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/todos/index.html b/todos/index.html
index 66a9c8c..5d21049 100644
--- a/todos/index.html
+++ b/todos/index.html
@@ -418,6 +418,26 @@
+
+
+
+
+
+ Serializer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/urls/index.html b/urls/index.html
index 25cf738..b4b1778 100644
--- a/urls/index.html
+++ b/urls/index.html
@@ -420,6 +420,26 @@
+
+
+
+
+
+ Serializer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/user_model/index.html b/user_model/index.html
index d938a6a..e076438 100644
--- a/user_model/index.html
+++ b/user_model/index.html
@@ -411,6 +411,26 @@
+
+
+
+
+
+ Serializer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/websocket/index.html b/websocket/index.html
index a64dfed..94b30f8 100644
--- a/websocket/index.html
+++ b/websocket/index.html
@@ -13,7 +13,7 @@
-
+
@@ -418,6 +418,26 @@
+
+
+
+
+
+
+
+ Serializer
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/working_with_db/index.html b/working_with_db/index.html
index 0c6a38e..f5a65a0 100644
--- a/working_with_db/index.html
+++ b/working_with_db/index.html
@@ -423,6 +423,26 @@
+
+
+
+
+
+ Serializer
+
+
+
+
+
+
+
+
+
+
+
+
+
+