├── tests ├── unit │ ├── __init__.py │ └── test_unit.py └── integration │ ├── __init__.py │ └── test_integration.py ├── backendpy ├── cli │ └── __init__.py ├── utils │ ├── __init__.py │ ├── bytes.py │ ├── json.py │ └── http.py ├── data_handler │ ├── __init__.py │ ├── data.py │ └── filters.py ├── middleware │ ├── defaults │ │ ├── __init__.py │ │ └── cors.py │ ├── __init__.py │ └── middleware.py ├── __init__.py ├── unittest.py ├── initializer.py ├── logging.py ├── app.py ├── config.py ├── templating.py ├── hook.py ├── db.py ├── exception.py └── error.py ├── .github └── CODEOWNERS ├── pyproject.toml ├── docs ├── projects.rst ├── apps.rst ├── requests.rst ├── index.rst ├── logging.rst ├── Makefile ├── locale │ └── fa │ │ └── LC_MESSAGES │ │ ├── projects.po │ │ ├── apps.po │ │ ├── generated │ │ ├── backendpy.po │ │ ├── backendpy.Backendpy.po │ │ └── backendpy.app.App.po │ │ ├── api.po │ │ ├── cli.po │ │ ├── index.po │ │ ├── testing.po │ │ ├── logging.po │ │ ├── deployment.po │ │ ├── initialization_scripts.po │ │ ├── run.po │ │ ├── management.po │ │ ├── environments.po │ │ ├── application_structure.po │ │ ├── templates.po │ │ ├── exceptions.po │ │ ├── predefined_errors.po │ │ ├── requests.po │ │ ├── responses.po │ │ ├── configurations.po │ │ ├── introduction.po │ │ ├── hooks.po │ │ ├── installation.po │ │ └── project_creation.po ├── make.bat ├── run.rst ├── deployment.rst ├── management.rst ├── environments.rst ├── installation.rst ├── introduction.rst ├── exceptions.rst ├── application_structure.rst ├── testing.rst ├── templates.rst ├── conf.py ├── configurations.rst ├── predefined_errors.rst ├── initialization_scripts.rst ├── project_creation.rst ├── responses.rst ├── hooks.rst ├── middlewares.rst ├── routes.rst └── database.rst ├── LICENSE ├── setup.cfg ├── .gitignore └── README.md /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backendpy/cli/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backendpy/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backendpy/data_handler/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backendpy/middleware/defaults/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backendpy/__init__.py: -------------------------------------------------------------------------------- 1 | from .asgi import Backendpy -------------------------------------------------------------------------------- /backendpy/middleware/__init__.py: -------------------------------------------------------------------------------- 1 | from .middleware import Middleware 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hamdollahi @saeedhamdollahi @savangco 2 | *.py @hamdollahi -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel", 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | -------------------------------------------------------------------------------- /tests/unit/test_unit.py: -------------------------------------------------------------------------------- 1 | from backendpy.unittest import AsyncTestCase 2 | 3 | 4 | class MyTestCase(AsyncTestCase): 5 | 6 | def test_something(self): 7 | self.assertEqual(True, False) 8 | -------------------------------------------------------------------------------- /docs/projects.rst: -------------------------------------------------------------------------------- 1 | Start a project 2 | =============== 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | project_creation 8 | run 9 | configurations 10 | environments 11 | management -------------------------------------------------------------------------------- /tests/integration/test_integration.py: -------------------------------------------------------------------------------- 1 | from backendpy.unittest import AsyncTestCase 2 | 3 | 4 | class MyTestCase(AsyncTestCase): 5 | 6 | def setUp(self): 7 | pass 8 | 9 | def test_something(self): 10 | self.assertEqual(True, False) 11 | -------------------------------------------------------------------------------- /backendpy/utils/bytes.py: -------------------------------------------------------------------------------- 1 | from functools import singledispatch 2 | 3 | 4 | @singledispatch 5 | def to_bytes(content): 6 | return str(content).encode('utf-8') 7 | 8 | 9 | @to_bytes.register(str) 10 | def _(content): 11 | return content.encode('utf-8') 12 | 13 | 14 | @to_bytes.register(bytes) 15 | def _(content): 16 | return content 17 | -------------------------------------------------------------------------------- /docs/apps.rst: -------------------------------------------------------------------------------- 1 | Application development 2 | ======================= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | application_structure 8 | routes 9 | requests 10 | responses 11 | exceptions 12 | predefined_errors 13 | data_handlers 14 | hooks 15 | middlewares 16 | database 17 | templates 18 | logging 19 | testing 20 | initialization_scripts 21 | -------------------------------------------------------------------------------- /docs/requests.rst: -------------------------------------------------------------------------------- 1 | Requests 2 | ======== 3 | HTTP requests, after being received by the framework, become a :class:`~backendpy.request.Request` 4 | object and are sent to the handler functions as a parameter called ``request``. 5 | 6 | .. code-block:: python 7 | :caption: project/apps/hello/handlers.py 8 | 9 | async def hello_world(request): 10 | ... 11 | 12 | Request object contains the following fields: 13 | 14 | .. autoclass:: backendpy.request.Request 15 | :noindex: 16 | 17 | -------------------------------------------------------------------------------- /backendpy/utils/json.py: -------------------------------------------------------------------------------- 1 | from functools import singledispatch 2 | from json import dumps # ujson is faster but it is not safe in dumps 3 | from typing import AnyStr 4 | 5 | try: 6 | from ujson import loads 7 | except ImportError: 8 | from json import loads 9 | 10 | 11 | @singledispatch 12 | def to_json(content) -> str: 13 | return dumps(content) 14 | 15 | 16 | @to_json.register(str) 17 | def _(content): 18 | return content 19 | 20 | 21 | @to_json.register(bytes) 22 | def _(content): 23 | return content 24 | 25 | 26 | def from_json(content: AnyStr) -> dict: 27 | return loads(content) 28 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Backendpy documentation master file, created by 2 | sphinx-quickstart on Fri Feb 11 19:38:29 2022. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Overview 7 | ======== 8 | 9 | .. toctree:: 10 | :numbered: 11 | :maxdepth: 2 12 | :caption: Contents: 13 | :name: mastertoc: 14 | 15 | introduction 16 | installation 17 | projects 18 | apps 19 | deployment 20 | 21 | 22 | Indices and tables 23 | ================== 24 | 25 | * :ref:`genindex` 26 | * :ref:`modindex` 27 | * :ref:`search` 28 | -------------------------------------------------------------------------------- /docs/logging.rst: -------------------------------------------------------------------------------- 1 | Logging 2 | ======= 3 | The Backendpy framework provides a logging class that uses Python standard logging module and differs in the color 4 | display of the logs in the command line, which increases the readability of the logs in this environment. 5 | 6 | This module has ``get_logger()`` function with the following specifications: 7 | 8 | .. autofunction:: backendpy.logging::get_logger 9 | :noindex: 10 | 11 | Example: 12 | 13 | .. code-block:: python 14 | 15 | from backendpy import logging 16 | 17 | LOGGER = logging.get_logger(__name__) 18 | 19 | LOGGER.debug("Example debug log") 20 | LOGGER.error("Example error log") 21 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/projects.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-06-04 17:23+0430\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=utf-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: Babel 2.9.1\n" 17 | 18 | #: ../../projects.rst:2 eb4559ea6d4442aeaa352764ec2d3edb 19 | msgid "Start a project" 20 | msgstr "‫شروع یک پروژه‬" 21 | 22 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/apps.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-06-04 17:23+0430\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=utf-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: Babel 2.9.1\n" 17 | 18 | #: ../../apps.rst:2 5e6f26e2b9a749a2b5556014d81f3057 19 | msgid "Application development" 20 | msgstr "‫توسعه‌ی اپلیکیشن‬" 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/generated/backendpy.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: Backendpy \n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-02-13 01:16+0330\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=utf-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Generated-By: Babel 2.9.1\n" 18 | 19 | #: ../../generated/backendpy.rst:2 66469839e589497eae9ff50c002efadd 20 | msgid "backendpy" 21 | msgstr "" 22 | 23 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.https://www.sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/api.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-08-27 04:04+0430\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=utf-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: Babel 2.9.1\n" 17 | 18 | #: ../../api.rst:2 7058543441e4404397a077c6c14bfbd2 19 | msgid "API references" 20 | msgstr "‫مرجع API‬" 21 | 22 | #: ../../api.rst:4 438c18043ada447d98ae129d629d1362 23 | msgid "Todo" 24 | msgstr "" 25 | 26 | #: ../../api.rst:6 8cd60fcd6ccd4d2b8c56e5ca1766fc07 27 | msgid "TODO" 28 | msgstr "" 29 | 30 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/cli.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-06-04 17:23+0430\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=utf-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: Babel 2.9.1\n" 17 | 18 | #: ../../cli.rst:2 cab6ff1cc2a44ba28339ea6bd5b436ba 19 | msgid "CLI commands references" 20 | msgstr "‫مرجع دستورات CLI‬" 21 | 22 | #: ../../cli.rst:4 4b380c87c7fc4b698fb2fc06728d4d21 23 | msgid "Todo" 24 | msgstr "" 25 | 26 | #: ../../cli.rst:6 9bc6e3ca0995426c80e0003d63f4a6cf 27 | msgid "TODO" 28 | msgstr "" 29 | 30 | -------------------------------------------------------------------------------- /docs/run.rst: -------------------------------------------------------------------------------- 1 | Run 2 | === 3 | You can use different ASGI servers such as Uvicorn, Hypercorn and Daphne to run the project. 4 | For this purpose, you must first install your desired server (see the :doc:`installation` section). 5 | 6 | Then enter the project path and use the following commands: 7 | 8 | .. code-block:: console 9 | :caption: For Uvicorn 10 | 11 | $ uvicorn main:bp 12 | 13 | .. code-block:: console 14 | :caption: For Hypercorn 15 | 16 | $ hypercorn main:bp 17 | 18 | .. code-block:: console 19 | :caption: For Daphne 20 | 21 | $ daphne main:bp 22 | 23 | .. note:: 24 | In these examples, we assume that the name of the main module of the project is "main.py" and the instance 25 | name of the Backendpy class inside it is "bp". These names are optional. 26 | 27 | The server is now accessible (depending on the host and port running on it) for example at http://127.0.0.1:8000. 28 | 29 | For more information on the options of each server, refer to their documentation. 30 | -------------------------------------------------------------------------------- /docs/deployment.rst: -------------------------------------------------------------------------------- 1 | Project deployment 2 | ================== 3 | A project based on the Backendpy framework is a standard ASGI application and can use a variety of methods and tools 4 | to deploy and operate these types of applications. 5 | 6 | Web servers such as Uvicorn, Hypercorn and Daphne can be used for this purpose. 7 | Also, the features of a web server such as Gunicorn can be used in combination with previous web servers. Or even use 8 | them behind the Nginx web server (as a proxy layer) and take advantage of all the features of this web server. 9 | To use each of these web servers, refer to their documentation. 10 | 11 | Example of using Uvicorn: 12 | 13 | .. code-block:: console 14 | 15 | uvicorn main:bp --host '127.0.0.1' --port 8000 16 | 17 | Example of using Uvicorn with Gunicorn: 18 | 19 | .. code-block:: console 20 | 21 | gunicorn main:bp -w 4 -k uvicorn.workers.UvicornWorker 22 | 23 | These commands can be defined and managed as a service in the operating system. 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /backendpy/unittest.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import functools 3 | import unittest 4 | 5 | 6 | class AsyncTestCase(unittest.TestCase): 7 | """Subclass of :class:`unittest.TestCase` with added async functionality.""" 8 | 9 | def __init__(self, methodName: str = 'runTest'): 10 | self._event_loop = asyncio.get_event_loop() 11 | super().__init__(methodName=methodName) 12 | 13 | def async_test(self, coro): 14 | @functools.wraps(coro) 15 | def wrapper(*args, **kwargs): 16 | self._event_loop.run_until_complete(coro(*args, **kwargs)) 17 | return wrapper 18 | 19 | def __getattribute__(self, item): 20 | attr = object.__getattribute__(self, item) 21 | if asyncio.iscoroutinefunction(attr): 22 | return self.async_test(attr) 23 | return attr 24 | 25 | 26 | class TestCase(unittest.TestCase): 27 | """Subclass of :class:`unittest.TestCase`.""" 28 | 29 | def __init__(self, methodName: str = 'runTest'): 30 | super().__init__(methodName=methodName) 31 | 32 | -------------------------------------------------------------------------------- /docs/management.rst: -------------------------------------------------------------------------------- 1 | Management 2 | ========== 3 | 4 | The following commands can be used by the system administrator: 5 | 6 | Creating Database 7 | ----------------- 8 | If you are using the default ORM, you can create the database and all data model tables within the project 9 | automatically by the following command (after entering the project path in the command line): 10 | 11 | .. code-block:: console 12 | 13 | $ backendpy create_db 14 | 15 | 16 | Initialization 17 | -------------- 18 | Some applications require the initial storage of data in the database, the creation of files, and so on, 19 | before they can be used. For example, before using some systems, information such as user roles, admin 20 | account, etc. related to the users application must be stored in the database. 21 | 22 | By running the following command in the project path, Backendpy framework will execute the initialization scripts of 23 | all the apps enabled on the project and will also take the required input data on the command line: 24 | 25 | .. code-block:: console 26 | 27 | $ backendpy init_project 28 | 29 | -------------------------------------------------------------------------------- /docs/environments.rst: -------------------------------------------------------------------------------- 1 | Environments 2 | ============ 3 | In Backendpy framework, it is possible to use different environments for project execution (such as development, 4 | production, etc.). 5 | 6 | Each environment has a different config file and can have different database settings, media paths, and so on. 7 | 8 | In a development team, different parts of the team can use their own environments and settings to execute and work 9 | on the project. 10 | Also, if needed on the main server, the project can be executed with another configuration in parallel, on another 11 | host or port. 12 | 13 | To do this, you need to define the ``BACKENDPY_ENV`` variable in the os with the desired name for the environment. 14 | For example, to define an environment called "dev", we use the following command: 15 | 16 | .. code-block:: console 17 | 18 | $ export BACKENDPY_ENV=dev 19 | 20 | You must also create and configure a separate configuration file named ``config.dev.ini``. 21 | 22 | Now if the server runs (in the process that the environmental variable is exported) or when other backendpy 23 | management commands are executed, this configuration will be used instead of the original configuration. 24 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/index.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-02-16 22:44+0330\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=utf-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: Babel 2.9.1\n" 17 | 18 | #: ../../index.rst:9 5890c985d75844dfa3b1dc1cc94d7361 19 | msgid "Contents:" 20 | msgstr "‫فهرست:‬" 21 | 22 | #: ../../index.rst:7 a129b2332686417f8f1c13c724117dcd 23 | msgid "Overview" 24 | msgstr "‫مرور کلی‬" 25 | 26 | #: ../../index.rst:26 aaab60aec4df458e8ac40a8fb5cab16a 27 | msgid "Indices and tables" 28 | msgstr "‫شاخص‌ها و جدول‌ها‬" 29 | 30 | #: ../../index.rst:28 b45a32c8235b42aa8ee8ef5e47586f3c 31 | msgid ":ref:`genindex`" 32 | msgstr "" 33 | 34 | #: ../../index.rst:29 f5103a6a2e82414d89a34ed2247b9648 35 | msgid ":ref:`modindex`" 36 | msgstr "" 37 | 38 | #: ../../index.rst:30 7fc26e9abde7491494c94a6366e3deb4 39 | msgid ":ref:`search`" 40 | msgstr "" 41 | 42 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Requirements 5 | ------------ 6 | Python 3.8+ 7 | 8 | Using pip 9 | --------- 10 | To install the Backendpy framework using pip: 11 | 12 | .. code-block:: console 13 | 14 | $ pip3 install backendpy 15 | 16 | If you also want to use the optional framework features (such as database, templating, etc.), you can use the 17 | following command to install the framework with additional dependencies: 18 | 19 | .. code-block:: console 20 | 21 | $ pip3 install backendpy[full] 22 | 23 | If you only need one of these features, you can install the required dependencies separately. The list of these 24 | requirements is as follows: 25 | 26 | .. list-table:: Optional requirements 27 | :widths: 15 15 70 28 | :header-rows: 1 29 | 30 | * - Name 31 | - Version 32 | - Usage 33 | * - asyncpg 34 | - >=0.25.0 35 | - If using default ORM 36 | * - sqlalchemy 37 | - >=2.0.13 38 | - If using default ORM 39 | * - jinja2 40 | - >=3.0.0 41 | - If using default Templating 42 | * - pillow 43 | - >=9.0.0 44 | - If using the ModifyImage filter of the backendpy.data_handler.filters 45 | * - ujson 46 | - >=5.1.0 47 | - If installed, ujson.loads will be used instead of json.loads, which is faster 48 | 49 | You also need to install an ASGI server such as Uvicorn, Hypercorn or Daphne: 50 | 51 | .. code-block:: console 52 | 53 | $ pip3 install uvicorn 54 | -------------------------------------------------------------------------------- /docs/introduction.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | **Backendpy** is an open-source framework for building the back-end of web projects with the Python programming 4 | language. 5 | 6 | Why Backendpy? 7 | -------------- 8 | This framework does not deprive developers of their freedom by restricting them to pre-defined structures, nor 9 | does it leave some repetitive and time-consuming tasks to the developer. 10 | 11 | Some of the features of Backendpy are: 12 | 13 | * Asynchronous programming (ASGI-based projects) 14 | * Application-based architecture and the ability to install third-party applications in a project 15 | * Support of middlewares for different layers such as Application, Handler, Request or Response 16 | * Supports events and hooks 17 | * Data handler classes, including validators and filters to automatically apply to request input data 18 | * Supports a variety of responses including JSON, HTML, file and… with various settings such as stream, gzip and… 19 | * Router with the ability to define urls as Python decorator or as separate files 20 | * Application-specific error codes 21 | * Optional default database layer by the Sqlalchemy async ORM with management of sessions for the scope of each request 22 | * Optional default templating layer by the Jinja template engine 23 | * ... 24 | 25 | License 26 | ------- 27 | The Backendpy framework licensed under the BSD 3-Clause terms. 28 | The source code is available at https://github.com/savangco/backendpy. 29 | 30 | .. note:: 31 | This project is under active development. 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, EndFramework 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /docs/exceptions.rst: -------------------------------------------------------------------------------- 1 | Exceptions 2 | ========== 3 | Backendpy exceptions are the type of responses used to return HTTP errors and can also be raised. 4 | 5 | .. note:: 6 | As mentioned, Backendpy exceptions are the type of :class:`~backendpy.response.Response` and their 7 | content is returned as a response and displayed to the user. Therefore, these exceptions should be used 8 | only for errors that must be displayed to users, and any kind of internal system error should be created 9 | with normal Python exceptions, in which case, the :class:`~backendpy.exception.ServerError` response is 10 | displayed to the user with a public message and does not contain sensitive system information that may 11 | be contained in the internal exception message. 12 | 13 | The list of default exception response classes are as follows: 14 | 15 | .. autoclass:: backendpy.exception.ExceptionResponse 16 | :noindex: 17 | 18 | .. autoclass:: backendpy.exception.BadRequest 19 | :noindex: 20 | 21 | Example usage: 22 | 23 | .. code-block:: python 24 | :caption: project/apps/hello/handlers.py 25 | 26 | from backendpy.router import Routes 27 | from backendpy.exception import BadRequest 28 | 29 | routes = Routes() 30 | 31 | @routes.post('/login') 32 | async def login(request): 33 | raise BadRequest({'message': 'Login failed!'}) 34 | 35 | .. autoclass:: backendpy.exception.Unauthorized 36 | :noindex: 37 | 38 | .. autoclass:: backendpy.exception.Forbidden 39 | :noindex: 40 | 41 | .. autoclass:: backendpy.exception.NotFound 42 | :noindex: 43 | 44 | .. autoclass:: backendpy.exception.ServerError 45 | :noindex: 46 | -------------------------------------------------------------------------------- /backendpy/initializer.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import os 3 | import sys 4 | from inspect import iscoroutinefunction 5 | 6 | from .app import App 7 | from .config import get_config 8 | from .logging import get_logger 9 | 10 | LOGGER = get_logger(__name__) 11 | 12 | 13 | class Init: 14 | def __init__(self, project_path): 15 | self.config = get_config(project_path) 16 | sys.path.append(os.path.dirname(self.config["environment"]["project_path"])) 17 | 18 | async def __call__(self): 19 | try: 20 | for package_name in self.config['apps']['active']: 21 | try: 22 | app = getattr(importlib.import_module(f'{package_name}.main'), 'app') 23 | if isinstance(app, App): 24 | if app.init_func: 25 | if iscoroutinefunction(app.init_func): 26 | await app.init_func(self.config) 27 | else: 28 | LOGGER.error(f'"{package_name}" Initialization error: ' 29 | f'The "init_func" must be a coroutine function') 30 | else: 31 | LOGGER.error(f'"{package_name}" Initialization error: ' 32 | f'App instance error') 33 | except (ImportError, AttributeError) as e: 34 | LOGGER.error(f'"{package_name}" Initialization error: ' 35 | f'App instance import error ({e})') 36 | except Exception as e: 37 | LOGGER.error(f'Failed to run initializations: {e}') 38 | -------------------------------------------------------------------------------- /docs/application_structure.rst: -------------------------------------------------------------------------------- 1 | Applications structure 2 | ====================== 3 | In section :doc:`project_creation`, we talked about how to create and activate a basic application in 4 | a Backendpy-based project. In this section, we describe the complete components of an application. 5 | 6 | As mentioned earlier, a Backendpy-based application does not have a predefined structure or constraint. 7 | In fact, the developer is free to implement the desired architecture for the application and finally import 8 | and configure all the components inside the main module of the application. 9 | 10 | The main module of an application must contain an instance of the :class:`~backendpy.app.App` class 11 | that is defined inside a variable called ``app``. 12 | Below is an example of defining an app with all its possible parameters (which are used to assign 13 | components to an application): 14 | 15 | .. code-block:: python 16 | :caption: project/apps/hello/main.py 17 | 18 | from backendpy.app import App 19 | from .controllers.handlers import routes 20 | from .controllers.hooks import hooks 21 | from .controllers.errors import errors 22 | from .controllers.init import init_func 23 | 24 | app = App( 25 | routes=[routes], 26 | hooks=[hooks], 27 | errors=[errors], 28 | models=['project.apps.hello.db.models'], 29 | template_dirs=['templates'], 30 | init_func=init_func) 31 | 32 | An :class:`~backendpy.app.App` class has the following parameters: 33 | 34 | .. autoclass:: backendpy.app.App 35 | :noindex: 36 | 37 | In the following, we will describe each of these components of the application, as well as other 38 | items that can be used in applications. 39 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/testing.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-06-04 17:23+0430\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=utf-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: Babel 2.9.1\n" 17 | 18 | #: ../../testing.rst:2 e8d8a64837994b6db33ef3c54236d397 19 | msgid "Testing" 20 | msgstr "‫تست کردن‌‬" 21 | 22 | #: ../../testing.rst:3 d4ef7a40abc74a6e80d48b2c0ae5aadb 23 | msgid "" 24 | "Because of the use of the async architecture in the Backendpy framework, " 25 | "we will need to run the tests as async for most sections in our " 26 | "applications. Hence, the Backendpy framework provides the " 27 | ":class:`~backendpy.unittest.AsyncTestCase` class, which is the subclass " 28 | "of :class:`unittest.TestCase`, to which async execution has been added." 29 | msgstr "" 30 | 31 | #: ../../testing.rst:7 7e33965339714375b8791570695707ee 32 | msgid "Example of testing a database query:" 33 | msgstr "" 34 | 35 | #: ../../testing.rst:9 cfea334b62b048638f19877b0ccbde4e 36 | msgid "project/apps/hello/tests/test_db.py" 37 | msgstr "" 38 | 39 | #: ../../testing.rst:40 c24fda825a524ad0b849c9f23ec86d05 40 | msgid "API test example:" 41 | msgstr "" 42 | 43 | #: ../../testing.rst:42 85a760865de747e28123b8578556e4a3 44 | msgid "project/apps/hello/tests/test_api.py" 45 | msgstr "" 46 | 47 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = backendpy 3 | version = v0.2.8-alpha.1 4 | author = EndFramework 5 | author_email = jalil.hamdollahi@gmail.com 6 | description = Async (ASGI) Python web framework 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | keywords = Backendpy, Web, Framework, Python, Async, ASGI 10 | license = BSD 3-Clause License 11 | url = https://github.com/EndFramework/backendpy 12 | project_urls = 13 | Bug Tracker = https://github.com/EndFramework/backendpy/issues 14 | Documentation = https://backendpy.readthedocs.io 15 | Source Code = https://github.com/EndFramework/backendpy 16 | classifiers = 17 | Development Status :: 3 - Alpha 18 | Environment :: Web Environment 19 | Intended Audience :: Developers 20 | License :: OSI Approved :: BSD License 21 | Programming Language :: Python :: 3 22 | Programming Language :: Python :: 3.8 23 | Programming Language :: Python :: 3.9 24 | Programming Language :: Python :: 3.10 25 | Programming Language :: Python :: 3.11 26 | Programming Language :: Python :: 3.12 27 | Programming Language :: Python :: 3.13 28 | Topic :: Internet :: WWW/HTTP 29 | Topic :: Internet :: WWW/HTTP :: Dynamic Content 30 | Topic :: Software Development 31 | Topic :: Software Development :: Libraries :: Application Frameworks 32 | Topic :: Software Development :: Libraries :: Python Modules 33 | 34 | [options] 35 | packages = find: 36 | python_requires = >=3.8 37 | install_requires = 38 | aiofiles >= 0.8.0 39 | 40 | [options.entry_points] 41 | console_scripts = 42 | backendpy = backendpy.cli.admin:main 43 | 44 | [options.extras_require] 45 | full = ujson>=5.1.0; asyncpg>=0.25.0; SQLAlchemy>=1.4.31; jinja2>=3.0.3; Pillow>=9.0.0 46 | -------------------------------------------------------------------------------- /backendpy/logging.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import logging as _logging 3 | from typing import Optional 4 | 5 | NOTSET = _logging.NOTSET 6 | DEBUG = _logging.DEBUG 7 | INFO = _logging.INFO 8 | WARNING = _logging.WARNING 9 | ERROR = _logging.ERROR 10 | CRITICAL = _logging.CRITICAL 11 | 12 | 13 | class Logger(_logging.Logger): 14 | """Subclass of :class:`logging.Logger` that produces colored logs.""" 15 | 16 | PINK = '\033[95m' 17 | BLUE = '\033[94m' 18 | GREEN = '\033[92m' 19 | ORANGE = '\033[93m' 20 | RED = '\033[91m' 21 | ENDC = '\033[0m' 22 | 23 | def debug(self, msg, *args, **kwargs): 24 | super().debug(f"{self.BLUE}{msg}{self.ENDC}", *args, **kwargs) 25 | 26 | def info(self, msg, *args, **kwargs): 27 | super().info(f"{self.GREEN}{msg}{self.ENDC}", *args, **kwargs) 28 | 29 | def warning(self, msg, *args, **kwargs): 30 | super().warning(f"{self.ORANGE}{msg}{self.ENDC}", *args, **kwargs) 31 | 32 | def error(self, msg, *args, **kwargs): 33 | super().error(f"{self.RED}{msg}{self.ENDC}", *args, **kwargs) 34 | 35 | def exception(self, msg, *args, exc_info=True, **kwargs): 36 | super().exception(f"{self.RED}{msg}{self.ENDC}", *args, exc_info=exc_info, **kwargs) 37 | 38 | def critical(self, msg, *args, **kwargs): 39 | super().critical(f"{self.RED}{msg}{self.ENDC}", *args, **kwargs) 40 | 41 | 42 | def get_logger( 43 | name: str, 44 | level: Optional[int | str] = None) -> Logger: 45 | """ 46 | Return a logger using :class:`backendpy.logging.Logger` class. 47 | 48 | :param name: Logger name 49 | :param level: Logging level 50 | """ 51 | _logging.setLoggerClass(Logger) 52 | _logging.basicConfig(level=level if level is not None else DEBUG) 53 | return _logging.getLogger(name) 54 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/logging.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-06-04 17:49+0430\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=utf-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: Babel 2.9.1\n" 17 | 18 | #: ../../logging.rst:2 7dee54268aef42a59b4c0e5cbcd1f028 19 | msgid "Logging" 20 | msgstr "‫Logها‬" 21 | 22 | #: ../../logging.rst:3 5e9fe01a79a043c8a7171d00ec0eade8 23 | msgid "" 24 | "The Backendpy framework provides a logging class that uses Python " 25 | "standard logging module and differs in the color display of the logs in " 26 | "the command line, which increases the readability of the logs in this " 27 | "environment." 28 | msgstr "" 29 | 30 | #: ../../logging.rst:6 3d94a6f107a14e889300963b85c26571 31 | msgid "" 32 | "This module has ``get_logger()`` function with the following " 33 | "specifications:" 34 | msgstr "" 35 | 36 | #: 8dece1ec68664a408b13c5c6d605ec83 backendpy.logging.get_logger:1 of 37 | msgid "Return a logger using :class:`backendpy.logging.Logger` class." 38 | msgstr "" 39 | 40 | #: 357cc9db4e06403b9a6b801a4760691f backendpy.logging.get_logger of 41 | msgid "Parameters" 42 | msgstr "" 43 | 44 | #: backendpy.logging.get_logger:3 f57bd9bd563949fc87b763f559d6f56e of 45 | msgid "Logger name" 46 | msgstr "" 47 | 48 | #: backendpy.logging.get_logger:4 ec9d6ad0ae594dff853ae0367aa4231d of 49 | msgid "Logging level" 50 | msgstr "" 51 | 52 | #: ../../logging.rst:11 cd2ad6a5f49d46ddb81c559fd8b4b6a6 53 | msgid "Example:" 54 | msgstr "" 55 | 56 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/deployment.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-06-04 17:23+0430\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=utf-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: Babel 2.9.1\n" 17 | 18 | #: ../../deployment.rst:2 803c34e9019047b7b7f41b2e6eb90961 19 | msgid "Project deployment" 20 | msgstr "‫به‌کاراندازی پروژه‬" 21 | 22 | #: ../../deployment.rst:3 194bcd8d9f924316a61e69cd6801afbf 23 | msgid "" 24 | "A project based on the Backendpy framework is a standard ASGI application" 25 | " and can use a variety of methods and tools to deploy and operate these " 26 | "types of applications." 27 | msgstr "" 28 | 29 | #: ../../deployment.rst:6 26bd44f7b025436596ba5e79880006ac 30 | msgid "" 31 | "Web servers such as Uvicorn, Hypercorn and Daphne can be used for this " 32 | "purpose. Also, the features of a web server such as Gunicorn can be used " 33 | "in combination with previous web servers. Or even use them behind the " 34 | "Nginx web server (as a proxy layer) and take advantage of all the " 35 | "features of this web server. To use each of these web servers, refer to " 36 | "their documentation." 37 | msgstr "" 38 | 39 | #: ../../deployment.rst:11 4f13322a4601420d88d9938f83da1b43 40 | msgid "Example of using Uvicorn:" 41 | msgstr "" 42 | 43 | #: ../../deployment.rst:17 2b50f084c359490484852fd12a075a43 44 | msgid "Example of using Uvicorn with Gunicorn:" 45 | msgstr "" 46 | 47 | #: ../../deployment.rst:23 e12946db6f8e413f802afe940f45f204 48 | msgid "" 49 | "These commands can be defined and managed as a service in the operating " 50 | "system." 51 | msgstr "" 52 | -------------------------------------------------------------------------------- /backendpy/app.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Iterable, Mapping 4 | from typing import Any, Optional 5 | 6 | from .error import ErrorList 7 | from .hook import Hooks 8 | from .router import Routes 9 | 10 | 11 | class App: 12 | """ 13 | A class used to define Backendpy internal application. 14 | 15 | :ivar routes: Iterable of instances of the Routes class 16 | :ivar hooks: Iterable of instances of the Hooks class (or None) 17 | :ivar models: Iterable of module paths that contain database models (or None) 18 | :ivar template_dirs: Iterable of paths (within the application directory) 19 | from which templates will be searched (or None) 20 | :ivar errors: Iterable of instances of the ErrorList class (or None) 21 | :ivar init_func: The initialization function of the application (or None) 22 | """ 23 | 24 | def __init__( 25 | self, 26 | routes: Iterable[Routes], 27 | hooks: Optional[Iterable[Hooks]] = None, 28 | models: Optional[Iterable[str]] = None, 29 | template_dirs: Optional[Iterable[str]] = None, 30 | errors: Optional[Iterable[ErrorList]] = None, 31 | init_func: Optional[callable[[Mapping], Any]] = None): 32 | """ 33 | Initialize application instance 34 | 35 | :param routes: Iterable of instances of the Routes class 36 | :param hooks: Iterable of instances of the Hooks class (or None) 37 | :param models: Iterable of module paths that contain database models (or None) 38 | :param template_dirs: Iterable of paths (within the application directory) 39 | from which templates will be searched (or None) 40 | :param errors: Iterable of instances of the ErrorList class (or None) 41 | :param init_func: The initialization function of the application (or None) 42 | """ 43 | self.routes = routes 44 | self.hooks = hooks 45 | self.models = models 46 | self.template_dirs = template_dirs 47 | self.errors = errors 48 | self.init_func = init_func 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Scrapy stuff: 59 | .scrapy 60 | 61 | # Sphinx documentation 62 | docs/_build/ 63 | 64 | # PyBuilder 65 | target/ 66 | 67 | # Jupyter Notebook 68 | .ipynb_checkpoints 69 | 70 | # IPython 71 | profile_default/ 72 | ipython_config.py 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # pipenv 78 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 79 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 80 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 81 | # install all needed dependencies. 82 | #Pipfile.lock 83 | 84 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 85 | __pypackages__/ 86 | 87 | # Celery stuff 88 | celerybeat-schedule 89 | celerybeat.pid 90 | 91 | # SageMath parsed files 92 | *.sage.py 93 | 94 | # Environments 95 | .env 96 | .venv 97 | env/ 98 | venv/ 99 | ENV/ 100 | env.bak/ 101 | venv.bak/ 102 | 103 | # Spyder project settings 104 | .spyderproject 105 | .spyproject 106 | 107 | # Rope project settings 108 | .ropeproject 109 | 110 | # mkdocs documentation 111 | /site 112 | 113 | # mypy 114 | .mypy_cache/ 115 | .dmypy.json 116 | dmypy.json 117 | 118 | # Pyre type checker 119 | .pyre/ 120 | 121 | # Pycharm 122 | .idea/ 123 | 124 | # log 125 | *.log 126 | -------------------------------------------------------------------------------- /docs/testing.rst: -------------------------------------------------------------------------------- 1 | Testing 2 | ======= 3 | Because of the use of the async architecture in the Backendpy framework, we will need to run the tests as async for 4 | most sections in our applications. Hence, the Backendpy framework provides the :class:`~backendpy.unittest.AsyncTestCase` 5 | class, which is the subclass of :class:`unittest.TestCase`, to which async execution has been added. 6 | 7 | Example of testing a database query: 8 | 9 | .. code-block:: python 10 | :caption: project/apps/hello/tests/test_db.py 11 | 12 | import unittest 13 | from asyncio import current_task 14 | from backendpy.unittest import AsyncTestCase 15 | from backendpy.db import get_db_engine, get_db_session 16 | from ..db import queries 17 | 18 | 19 | DATABASE = {'host': 'localhost', 'port': '5432', 'name': 'your_db_name', 20 | 'username': 'your_db_user', 'password': 'your_db_password'} 21 | 22 | class QueriesTestCase(AsyncTestCase): 23 | 24 | def setUp(self) -> None: 25 | self.db_engine = get_db_engine(DATABASE, echo=True) 26 | self.db_session = get_db_session(self.db_engine, scope_func=current_task) 27 | 28 | async def tearDown(self) -> None: 29 | await self.db_session.remove() 30 | await self.db_engine.dispose() 31 | 32 | async def test_get_users(self): 33 | result = await queries.get_users(self.db_session()) 34 | self.assertNotEqual(result, False) 35 | 36 | if __name__ == '__main__': 37 | unittest.main() 38 | 39 | 40 | API test example: 41 | 42 | .. code-block:: python 43 | :caption: project/apps/hello/tests/test_api.py 44 | 45 | import unittest 46 | from backendpy.unittest import TestCase 47 | from backendpy.utils import http 48 | 49 | class MyTestCase(TestCase): 50 | 51 | def setUp(self) -> None: 52 | self.client = http.Client('127.0.0.1', 8000) 53 | 54 | def test_user_creation(self): 55 | with self.client as session: 56 | data = {'first_name': 'Jalil', 57 | 'last_name': 'Hamdollahi Oskouei', 58 | 'username': 'my_user', 59 | 'password': 'my_pass'} 60 | result = session.post('/users', json=data) 61 | self.assertEqual(result.status, 200) 62 | self.assertNotEqual(result.data, '') 63 | 64 | if __name__ == '__main__': 65 | unittest.main() 66 | -------------------------------------------------------------------------------- /docs/templates.rst: -------------------------------------------------------------------------------- 1 | Templates 2 | ========= 3 | The need to render templates on the server side is used in some projects. External template engines that support async 4 | can be used for this purpose. You can also use the framework helper layer for this, which is a layer for using the 5 | Jinja2 template engine package. 6 | The Backendpy framework facilitates the use of templates with this template engine and adapts it to its architecture 7 | with things like template files async reading from predefined application template paths. 8 | 9 | An example of rendering a web page template and returning it as a response is as follows. 10 | 11 | First we need to specify the application template dirs inside the application ``main.py`` module with the 12 | ``template_dirs`` parameter of the App class: 13 | 14 | .. code-block:: python 15 | :caption: project/apps/hello/main.py 16 | 17 | from backendpy.app import App 18 | 19 | app = App( 20 | ... 21 | template_dirs=['templates'], 22 | ...) 23 | 24 | Then we create the desired templates in the defined path: 25 | 26 | .. code-block:: html 27 | :caption: project/apps/hello/templates/home.html 28 | 29 | 30 | 31 | 32 | Backendpy 33 | 34 | 35 |

{{ message }}

36 | 37 | 38 | 39 | Refer to the Jinja2 package documentation to learn the templates syntax. 40 | 41 | Finally, we use these template inside a handler: 42 | 43 | .. code-block:: python 44 | :caption: project/apps/hello/controllers/handlers.py 45 | 46 | from backendpy.router import Routes 47 | from backendpy.response import HTML 48 | from backendpy.templating import Template 49 | 50 | routes = Routes() 51 | 52 | @routes.get('/home') 53 | async def home(request): 54 | context = {'message': 'Hello World!'} 55 | return HTML(await Template('home.html').render(context)) 56 | 57 | In this example code, we first initialize :class:`~backendpy.templating.Template` class with the template name and 58 | then with the ``render`` method we render the context values in it (note that this method must also be called async) 59 | and then we return the final content with :class:`~backendpy.response.HTML` response. 60 | 61 | Also here you just need to enter the template name and the framework will automatically search for this name in the 62 | application template dirs. 63 | 64 | Details of the :class:`~backendpy.templating.Template` class are as follows: 65 | 66 | .. autoclass:: backendpy.templating.Template 67 | :noindex: 68 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('.')) 16 | sys.path.insert(0, os.path.abspath('../../backendpy')) 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'Backendpy' 21 | copyright = '2022, Savang Co.' 22 | author = 'Jalil Hamdollahi Oskouei' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = '0.1.7a1' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.doctest', 35 | 'sphinx.ext.autodoc', 36 | 'sphinx.ext.autosummary', 37 | 'sphinx.ext.todo', 38 | ] 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # The language for content autogenerated by Sphinx. Refer to documentation 44 | # for a list of supported languages. 45 | # 46 | # This is also used if you do content translation via gettext catalogs. 47 | # Usually you set "language" from the command line for these cases. 48 | language = 'en' 49 | 50 | # List of patterns, relative to source directory, that match files and 51 | # directories to ignore when looking for source files. 52 | # This pattern also affects html_static_path and html_extra_path. 53 | exclude_patterns = [] 54 | 55 | # Locale 56 | locale_dirs = ['locale/'] 57 | gettext_compact = False 58 | gettext_uuid = True 59 | 60 | # Epub 61 | epub_show_urls = 'footnote' 62 | 63 | # 64 | todo_include_todos = True 65 | 66 | # -- Options for HTML output ------------------------------------------------- 67 | 68 | # The theme to use for HTML and HTML Help pages. See the documentation for 69 | # a list of builtin themes. 70 | # 71 | html_theme = 'sphinx_rtd_theme' 72 | 73 | # Add any paths that contain custom static files (such as style sheets) here, 74 | # relative to this directory. They are copied after the builtin static files, 75 | # so a file named "default.css" will overwrite the builtin "default.css". 76 | html_static_path = ['_static'] 77 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/initialization_scripts.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-06-04 17:23+0430\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=utf-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: Babel 2.9.1\n" 17 | 18 | #: ../../initialization_scripts.rst:2 6b7203d632134c86a70c831cff3179a2 19 | msgid "Initialization scripts" 20 | msgstr "‫اسکریپت‌های مقداردهی اولیه‬" 21 | 22 | #: ../../initialization_scripts.rst:3 0bbdc05928b7443c8aadd655ff90601e 23 | msgid "" 24 | "Some applications require basic data in order to run. For example, the " 25 | "users application, after installation, also needs to enter the " 26 | "administrator account information to be able to manage other accounts and" 27 | " the entire system. To record this raw data, we can use initialization " 28 | "scripts that are executable at the command line when a project is " 29 | "deployed." 30 | msgstr "" 31 | 32 | #: ../../initialization_scripts.rst:8 a7d289e3fd594b439e7240cddccdb166 33 | msgid "" 34 | "From the ``init_func`` parameter of any application we can assign an " 35 | "initialization function to it:" 36 | msgstr "" 37 | 38 | #: ../../initialization_scripts.rst:10 4f2a7bc06c624406a6268f3ec8a61444 39 | msgid "project/apps/hello/main.py" 40 | msgstr "" 41 | 42 | #: ../../initialization_scripts.rst:21 866128c56a724b52bc523a4e107fc475 43 | msgid "And an init function could be like this:" 44 | msgstr "" 45 | 46 | #: ../../initialization_scripts.rst:23 0e7f0304f236478d854b604a30f8f36c 47 | msgid "project/apps/hello/controllers/init.py" 48 | msgstr "" 49 | 50 | #: ../../initialization_scripts.rst:61 02e9735e918d42bc8991cbe8001c5da7 51 | msgid "" 52 | "As can be seen, the init functions receive ``config`` parameter, which " 53 | "can be used to access project configurations such as database information" 54 | " and so on." 55 | msgstr "" 56 | 57 | #: ../../initialization_scripts.rst:64 9c587dc531d0487dbab5df55ce6afb81 58 | msgid "" 59 | "The project manager can perform the initialization by executing the " 60 | "following command on the command line in the project dir:" 61 | msgstr "" 62 | 63 | #: ../../initialization_scripts.rst:71 db7e35d85e2b43c8a829f50e5677f119 64 | msgid "" 65 | "By executing this command, the Backendpy framework executes the " 66 | "initialization scripts of all applications activated in the project " 67 | "configuration sequentially." 68 | msgstr "" 69 | 70 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/run.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-02-17 18:21+0330\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=utf-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: Babel 2.9.1\n" 17 | 18 | #: ../../run.rst:2 4ffa537048b341f3b9898a502cd5cb81 19 | msgid "Run" 20 | msgstr "‫اجرا‬" 21 | 22 | #: ../../run.rst:4 40c5c8b98a9b4ace85dc539d5d16cebe 23 | msgid "" 24 | "You can use different ASGI servers such as Uvicorn, Hypercorn and Daphne " 25 | "to run the project. For this purpose, you must first install your desired" 26 | " server (see the :doc:`installation` section)." 27 | msgstr "" 28 | "‫برای اجرای یک پروژه می‌توانید از سرورهای مختلف ASGI مانند Uvicorn، " 29 | "Hypercorn و Daphne استفاده کنید. برای این منظور ابتدا باید سرور مورد نظر " 30 | "خود را (طبق بخش :doc:`installation`) نصب کنید.‬" 31 | 32 | #: ../../run.rst:7 3c7079eb83414bdf900135074e5dec29 33 | msgid "Then enter the project path and use the following commands:" 34 | msgstr "‫سپس وارد مسیر پروژه شده و از دستورات زیر استفاده کنید:‬" 35 | 36 | #: ../../run.rst:9 56645810000f44fabfed3836b087497f 37 | msgid "For Uvicorn" 38 | msgstr "‫برای Uvicorn‬" 39 | 40 | #: ../../run.rst:14 206c2f23e7354683ac5cf97de03cefb7 41 | msgid "For Hypercorn" 42 | msgstr "‫برای Hypercorn‬" 43 | 44 | #: ../../run.rst:19 3d6edbb5eb284adca60e3ae24bd6b632 45 | msgid "For Daphne" 46 | msgstr "‫برای Daphne‬" 47 | 48 | #: ../../run.rst:25 35cd22ddb7d84ae1802f15d36c553cc8 49 | msgid "" 50 | "In these examples, we assume that the name of the main module of the " 51 | "project is \"main.py\" and the instance name of the Backendpy class " 52 | "inside it is \"bp\". These names are optional." 53 | msgstr "" 54 | "‫در این مثال‌ها فرض کرده‌ایم که نام ماژول اصلی پروژه main.py و نام " 55 | "نمونه‌ی کلاس Backendpy داخل آن bp است. این نام‌ها اختیاری‌اند.‬" 56 | 57 | #: ../../run.rst:28 f111a9ac237849bcb8c1189c054e6d5b 58 | msgid "" 59 | "The server is now accessible (depending on the host and port running on " 60 | "it) for example at http://127.0.0.1:8000." 61 | msgstr "" 62 | "‫اکنون سرور (بسته به host و port مورد استفاده) به عنوان مثال در آدرس " 63 | "http://127.0.0.1:8000 قابل دسترس است.‬" 64 | 65 | #: ../../run.rst:30 bff75c61876f41899a03ee560d28342c 66 | msgid "" 67 | "For more information on the options of each server, refer to their " 68 | "documentation." 69 | msgstr "" 70 | "‫برای اطلاعات بیشتر در مورد نحوه‌ی استفاده از هرکدام از سرورها، به " 71 | "مستندات آن‌ها رجوع کنید.‬" 72 | 73 | -------------------------------------------------------------------------------- /backendpy/config.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import configparser 4 | import os 5 | 6 | from .logging import get_logger 7 | 8 | LOGGER = get_logger(__name__) 9 | 10 | 11 | def get_config(project_path: str, error_logs: bool = False) -> dict[str, dict[str, str | tuple[str]]]: 12 | """ 13 | Reads the project settings from the INI file, applies some defaults, and returns the config as a dict object. 14 | 15 | :param project_path: Project root path where the config file is located 16 | :param error_logs: Whether or not to log the config errors (default is False) 17 | :return: A dict that contains configs 18 | """ 19 | 20 | # Load from config file 21 | config_parser = configparser.ConfigParser() 22 | env_name = os.getenv('BACKENDPY_ENV', None) 23 | file_name = f'config.{env_name}.ini' if env_name is not None else 'config.ini' 24 | config_path = os.path.join(project_path, file_name) 25 | if os.path.exists(config_path): 26 | config_parser.read(config_path) 27 | elif error_logs: 28 | LOGGER.warning(f"Project {file_name} file does not exist") 29 | 30 | # Convert config to dict 31 | config = _to_dict(config_parser) 32 | 33 | # Add default sections if does not exists 34 | config.setdefault('environment', {}) 35 | config.setdefault('networking', {}) 36 | config.setdefault('logging', {}) 37 | config.setdefault('database', {}) 38 | config.setdefault('apps', {}) 39 | config.setdefault('middlewares', {}) 40 | 41 | # Add default configs if does not exists 42 | if type(config['apps'].get('active')) is not tuple: 43 | config['apps']['active'] = () 44 | if type(config['middlewares'].get('active')) is not tuple: 45 | config['middlewares']['active'] = () 46 | if type(config['networking'].get('allowed_hosts')) is not tuple: 47 | config['networking']['allowed_hosts'] = () 48 | 49 | # Set environment configs 50 | config['environment']['project_path'] = str(project_path) 51 | config['environment']['project_name'] = os.path.basename(config['environment']['project_path']) 52 | 53 | # Check configs 54 | if config['environment'].get('media_path') and \ 55 | not os.path.isdir(config['environment']['media_path']): 56 | raise LookupError("Project media path does not exist") 57 | 58 | return config 59 | 60 | 61 | def _parse_list(string: str) -> tuple[str, ...]: 62 | """Parse the list from multi-line formatted string and convert it to a tuple type.""" 63 | return tuple(i for i in string.split('\n') if i) 64 | 65 | 66 | def _to_dict(cfg: configparser.ConfigParser) -> dict: 67 | """Convert config to dict""" 68 | r = dict() 69 | for section in cfg.sections(): 70 | r[section] = dict() 71 | for k, v in cfg.items(section): 72 | r[section][k] = _parse_list(v) if '\n' in v else v 73 | return r 74 | -------------------------------------------------------------------------------- /docs/configurations.rst: -------------------------------------------------------------------------------- 1 | Configurations 2 | ============== 3 | In Backendpy projects, all the settings of a project are defined in the ``config.ini`` file, which is located in the 4 | root path of each project and next to the main module of the project. 5 | This config file is defined in INI format, which includes sections and options. 6 | The basic list of framework configs and example of their definition is as follows: 7 | 8 | .. code-block:: 9 | :caption: project/config.ini 10 | 11 | ; Backendpy Configurations 12 | 13 | [networking] 14 | allowed_hosts = 15 | 127.0.0.1:8000 16 | localhost:8000 17 | stream_size = 32768 18 | 19 | [environment] 20 | media_path = /foo/bar 21 | 22 | [apps] 23 | active = 24 | backendpy_accounts 25 | myproject.apps.myapp 26 | 27 | [middlewares] 28 | active = 29 | backendpy_accounts.middleware.auth.AuthMiddleware 30 | 31 | [database] 32 | host = localhost 33 | port = 5432 34 | name = 35 | username = 36 | password = 37 | 38 | In ini format, ``;`` is used for comments, ``[]`` is used to define sections, ``key = value`` is used to define values 39 | and the lines are used for list values. 40 | 41 | * **networking** section contains values related to the server and the network. 42 | 43 | * **environment** section contains values such as the path to the media files and etc. 44 | 45 | * **apps** section contains a list of the project active applications. 46 | 47 | * **middlewares** section contains a list of the project active middlewares. 48 | 49 | * **database** section, if using the default ORM, will include the settings related to it. 50 | 51 | Also other custom settings may be required by any of the active apps, which must also be specified in this file. 52 | For example, an account application might have settings like this: 53 | 54 | .. code-block:: 55 | 56 | [accounts] 57 | aes_key = 11111111111111111111111111111111 58 | auth_tokens_secret = 2222222222222222222222222222 59 | 60 | .. note:: 61 | 62 | To protect sensitive information in the config file, such as passwords, private keys, etc., be sure to restrict 63 | access to this file. For example, set the permission to 600. 64 | 65 | 66 | In order to access the project configs inside the code, you can use the ``config`` attribute of the project 67 | :class:`~backendpy.Backendpy` class instance which contains this configs in dictionary format: 68 | 69 | .. code-block:: python 70 | :caption: project/main.py 71 | 72 | from backendpy import Backendpy 73 | 74 | bp = Backendpy() 75 | 76 | print(bp.config['database']['host']) 77 | 78 | And similarly inside the request handlers: 79 | 80 | .. code-block:: python 81 | :caption: project/apps/hello/handlers.py 82 | 83 | async def hello_world(request): 84 | print(request.app.config['database']['host']) 85 | ... 86 | 87 | -------------------------------------------------------------------------------- /docs/predefined_errors.rst: -------------------------------------------------------------------------------- 1 | Predefined errors 2 | ================= 3 | To return a response with error content, you can manually return the content of that error with :doc:`exceptions` 4 | or create helper classes and assign specific templates to them. 5 | But if the number and variety of these errors is large, it is better to assign a code to each error in addition to 6 | a fixed format for errors. 7 | 8 | In the Backendpy framework, a special class for managing error responses is provided to help better and easier manage 9 | errors and their code and to prevent the error code of one application from interfering with other applications: 10 | 11 | .. autoclass:: backendpy.error.Error 12 | :noindex: 13 | 14 | In order to use :class:`~backendpy.error.Error` response class, you must first define a list of errors for the 15 | application, including codes and their corresponding messages. 16 | The list of errors in an application is defined by an instance of the :class:`~backendpy.error.ErrorList` class, 17 | which itself contains instances of the :class:`~backendpy.error.ErrorCode` class. 18 | 19 | .. autoclass:: backendpy.error.ErrorList 20 | :noindex: 21 | 22 | .. autoclass:: backendpy.error.ErrorCode 23 | :noindex: 24 | 25 | For example, inside a custom module: 26 | 27 | .. code-block:: python 28 | :caption: project/apps/hello/controllers/errors.py 29 | 30 | from backendpy.response import Status 31 | from backendpy.error import ErrorCode, ErrorList 32 | 33 | errors = ErrorList( 34 | ErrorCode(1100, "Authorization error", Status.UNAUTHORIZED), 35 | ErrorCode(1101, "User '{}' does not exists", Status.BAD_REQUEST) 36 | ) 37 | 38 | After defining the list of errors, we must add this list to the application. For this purpose, as mentioned before, 39 | inside the ``main.py`` module of the application, we set the ``errors`` parameter with our own error list 40 | (instance of :class:`~backendpy.error.ErrorList` class). 41 | 42 | Also note that the errors parameter is a list type, and more than one ErrorList can be assigned to each app, 43 | each list being specific to a different part of the app. 44 | 45 | .. code-block:: python 46 | :caption: project/apps/hello/main.py 47 | 48 | from backendpy.app import App 49 | from .controllers.errors import errors 50 | 51 | app = App( 52 | ... 53 | errors=[errors], 54 | ...) 55 | 56 | And finally, examples of returning the :class:`~backendpy.error.Error` response: 57 | 58 | .. code-block:: python 59 | :caption: project/apps/hello/controllers/handlers.py 60 | 61 | from backendpy.router import Routes 62 | from backendpy.error import Error 63 | 64 | routes = Routes() 65 | 66 | @routes.get('/example-error') 67 | async def example_error(request): 68 | raise Error(1100) 69 | 70 | @routes.post('/login') 71 | async def login(request): 72 | raise Error(1101, 'jalil') 73 | -------------------------------------------------------------------------------- /docs/initialization_scripts.rst: -------------------------------------------------------------------------------- 1 | Initialization scripts 2 | ====================== 3 | Some applications require basic data in order to run. For example, the users application, after installation, also 4 | needs to enter the administrator account information to be able to manage other accounts and the entire system. 5 | To record this raw data, we can use initialization scripts that are executable at the command line when a project is 6 | deployed. 7 | 8 | From the ``init_func`` parameter of any application we can assign an initialization function to it: 9 | 10 | .. code-block:: python 11 | :caption: project/apps/hello/main.py 12 | 13 | from backendpy.app import App 14 | from .controllers.init import init_data 15 | 16 | app = App( 17 | ... 18 | init_func=init_data, 19 | ...) 20 | 21 | And an init function could be like this: 22 | 23 | .. code-block:: python 24 | :caption: project/apps/hello/controllers/init.py 25 | 26 | from asyncio import current_task 27 | from backendpy.db import get_db_engine, get_db_session 28 | from backendpy.logging import logging 29 | from ..db import queries 30 | 31 | LOGGER = logging.getLogger(__name__) 32 | 33 | async def init_data(config): 34 | # Get db session 35 | db_engine = get_db_engine(config['database'], echo=False) 36 | db_session = get_db_session(db_engine, current_task) 37 | 38 | try: 39 | # ... 40 | # Create admin user 41 | if await queries.get_users(db_session): 42 | LOGGER.warning('Users created already [SKIPPED]') 43 | else: 44 | try: 45 | user_data = { 46 | 'first_name': input('Enter first name:\n'), 47 | 'last_name': input('Enter last name:\n'), 48 | 'username': input('Enter admin username:\n'), 49 | 'password': input('Enter admin password:\n')} 50 | except Exception as e: 51 | raise Exception(f'Input error:\n{e}') 52 | 53 | if await queries.set_user(db_session, user_data): 54 | LOGGER.info('Admin user created successfully') 55 | else: 56 | raise Exception('Admin user creation error') 57 | finally: 58 | await db_session.remove() 59 | await db_engine.dispose() 60 | 61 | As can be seen, the init functions receive ``config`` parameter, which can be used to access project configurations 62 | such as database information and so on. 63 | 64 | The project manager can perform the initialization by executing the following command on the command line in the 65 | project dir: 66 | 67 | .. code-block:: console 68 | 69 | $ backendpy init_project 70 | 71 | By executing this command, the Backendpy framework executes the initialization scripts of all applications activated 72 | in the project configuration sequentially. 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Backendpy 3 | Python web framework for building the backend of your project! 4 | 5 | Some features: 6 | * Asynchronous programming (ASGI-based projects) 7 | * Application-based architecture and the ability to install third-party applications in a project 8 | * Support of middlewares for different layers such as Application, Handler, Request or Response 9 | * Supports events and hooks 10 | * Data handler classes, including validators and filters to automatically apply to request input data 11 | * Supports a variety of responses including JSON, HTML, file and… with various settings such as stream, gzip and… 12 | * Router with the ability to define urls as Python decorator or as separate files 13 | * Application-specific error codes 14 | * Optional default database layer by the Sqlalchemy async ORM with management of sessions for the scope of each request 15 | * Optional default templating layer by the Jinja template engine 16 | * … 17 | 18 | ### Requirements 19 | Python 3.8+ 20 | 21 | ### Documentation 22 | Documentation is available at https://backendpy.readthedocs.io. 23 | 24 | 25 | ### Quick Start 26 | 27 | #### Installation 28 | ```shell 29 | $ pip3 install backendpy 30 | ``` 31 | Or use the following command to install optional additional libraries: 32 | ```shell 33 | $ pip3 install backendpy[full] 34 | ``` 35 | You also need to install an ASGI server such as Uvicorn, Hypercorn or Daphne: 36 | ```shell 37 | $ pip3 install uvicorn 38 | ``` 39 | 40 | #### Create Project 41 | 42 | *project/main.py* 43 | ```python 44 | from backendpy import Backendpy 45 | 46 | bp = Backendpy() 47 | ``` 48 | 49 | #### Create Application 50 | 51 | *project/apps/hello/main.py* 52 | ```python 53 | from backendpy.app import App 54 | from .handlers import routes 55 | 56 | app = App( 57 | routes=[routes]) 58 | ``` 59 | *project/apps/hello/handlers.py* 60 | ```python 61 | from backendpy.router import Routes 62 | from backendpy.response import Text 63 | 64 | routes = Routes() 65 | 66 | @routes.get('/hello-world') 67 | async def hello(request): 68 | return Text('Hello World!') 69 | ``` 70 | 71 | #### Activate Application 72 | 73 | *project/config.ini* 74 | ```ini 75 | [networking] 76 | allowed_hosts = 77 | 127.0.0.1:8000 78 | 79 | [apps] 80 | active = 81 | project.apps.hello 82 | ``` 83 | 84 | #### Run Project 85 | 86 | Inside the project root path: 87 | ```shell 88 | $ uvicorn main:bp 89 | ``` 90 | 91 | #### Command line 92 | The basic structure of a project mentioned above can also be created by commands: 93 | ```shell 94 | $ backendpy create_project --name myproject 95 | ``` 96 | To create a project with more complete sample components: 97 | ```shell 98 | $ backendpy create_project --name myproject --full 99 | ``` 100 | Or to create an application: 101 | ```shell 102 | $ backendpy create_app --name myapp 103 | ``` 104 | ```shell 105 | $ backendpy create_app --name myapp --full 106 | ``` 107 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/management.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-02-17 18:12+0330\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=utf-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: Babel 2.9.1\n" 17 | 18 | #: ../../management.rst:2 3243d28e060f40b18f03721b28f7601b 19 | msgid "Management" 20 | msgstr "‫مدیریت‬" 21 | 22 | #: ../../management.rst:4 993cdd8c9bd148a99a50c9f8058774ca 23 | msgid "The following commands can be used by the system administrator:" 24 | msgstr "‫دستورات زیر می‌تواند توسط مدیر سیستم مورد استفاده قرار گیرند:‬" 25 | 26 | #: ../../management.rst:7 f00935b70a674ea58f76783ca3392058 27 | msgid "Creating Database" 28 | msgstr "‫ایجاد پایگاه داده‬" 29 | 30 | #: ../../management.rst:8 d6e49e6b74d74ecbacea56ad759ccd7f 31 | msgid "" 32 | "If you are using the default ORM, you can create the database and all " 33 | "data model tables within the project automatically by the following " 34 | "command (after entering the project path in the command line):" 35 | msgstr "" 36 | "‫اگر از ORM پیش‌فرض استفاده می‌کنید، می‌توانید با دستور زیر (پس از وارد " 37 | "شدن به مسیر پروژه در خط فرمان) پایگاه داده و تمام جداول مدل‌های داده‌ی " 38 | "پروژه را به طور خودکار ایجاد کنید:‬" 39 | 40 | #: ../../management.rst:17 0f3ea164d4374e099e30a4fa4763814c 41 | msgid "Initialization" 42 | msgstr "‫مقداردهی اولیه‬" 43 | 44 | #: ../../management.rst:18 17af1f20c393452983d86a727033fe88 45 | msgid "" 46 | "Some applications require the initial storage of data in the database, " 47 | "the creation of files, and so on, before they can be used. For example, " 48 | "before using some systems, information such as user roles, admin account," 49 | " etc. related to the users application must be stored in the database." 50 | msgstr "" 51 | "‫برخی از اپلیکیشن‌ها قبل از اینکه بتوان از آن‌ها استفاده کرد، به " 52 | "ذخیره‌سازی اولیه‌ی داده‌هایی در پایگاه‌داده، ایجاد فایل‌ها و … نیاز دارند. " 53 | "به عنوان مثال، قبل از استفاده از برخی از سیستم‌ها، ابتدا باید اطلاعاتی " 54 | "مانند گروه‌های کاربری، حساب مدیر و … مربوط به اپلیکیشن کاربران، " 55 | "در پایگاه داده ذخیره شده باشند.‬" 56 | 57 | #: ../../management.rst:22 2aba9b1e04c44aebb59e39d5a82b75c3 58 | msgid "" 59 | "By running the following command in the project path, Backendpy framework" 60 | " will execute the initialization scripts of all the apps enabled on the " 61 | "project and will also take the required input data on the command line:" 62 | msgstr "" 63 | "‫با اجرای دستور زیر در مسیر پروژه، فریم ورک Backendpy اسکریپت‌های " 64 | "مقداردهی اولیه‌ی کلیه‌ی اپلیکیشن‌های فعال در پروژه را اجرا می‌کند و " 65 | "همچنین در صورت نیاز داده‌های ورودی را در خط‌فرمان دریافت می‌کند:‬" 66 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/generated/backendpy.Backendpy.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: Backendpy \n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-06-04 17:59+0430\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 2.9.1\n" 19 | 20 | #: ../../generated/backendpy.Backendpy.rst:2 6626a95eaf8a497c81581580a237e698 21 | msgid "backendpy.Backendpy" 22 | msgstr "" 23 | 24 | #: 879225692050417da47872dc939f7734 backendpy.asgi.Backendpy:1 of 25 | msgid "The Backendpy ASGI handler" 26 | msgstr "" 27 | 28 | #: ../../generated/backendpy.Backendpy.rst:19::1 29 | #: 43d76b11cea94dea9562890961409b12 a0f17386b53f49179f6618b3d28e331c 30 | #: backendpy.asgi.Backendpy.__init__:1 of 31 | msgid "Initialize Backendpy class instance." 32 | msgstr "" 33 | 34 | #: ../../generated/backendpy.Backendpy.rst:13 f69fd860899e462ea5816d7ec37b40cd 35 | msgid "Methods" 36 | msgstr "" 37 | 38 | #: ../../generated/backendpy.Backendpy.rst:19::1 39 | #: a008de2a333c443cae697be65e172247 40 | msgid ":py:obj:`__init__ `\\ \\(\\)" 41 | msgstr "" 42 | 43 | #: ../../generated/backendpy.Backendpy.rst:19::1 44 | #: 3f8a4042f88b481fa73174d8df31f76f 45 | msgid ":py:obj:`event `\\ \\(name\\)" 46 | msgstr "" 47 | 48 | #: ../../generated/backendpy.Backendpy.rst:19::1 49 | #: 6449b58fdd62439cab718d88c07a19d5 50 | msgid "Register an event hook with python decorator." 51 | msgstr "" 52 | 53 | #: ../../generated/backendpy.Backendpy.rst:19::1 54 | #: 5d0725df415b4e57bab249f74d394736 55 | msgid "" 56 | ":py:obj:`execute_event `\\ " 57 | "\\(name\\[\\, args\\]\\)" 58 | msgstr "" 59 | 60 | #: ../../generated/backendpy.Backendpy.rst:19::1 61 | #: 0e0c2be52aa44ccd93b66ae213e832a3 62 | msgid "Trigger all hooks related to the event." 63 | msgstr "" 64 | 65 | #: ../../generated/backendpy.Backendpy.rst:19::1 66 | #: 029a1bcc91b8426b8d0ee6b6d28a3999 67 | msgid "" 68 | ":py:obj:`get_current_request `\\" 69 | " \\(\\)" 70 | msgstr "" 71 | 72 | #: ../../generated/backendpy.Backendpy.rst:19::1 73 | #: 7bd62a7b1f2f45c1bf4bb0cd628e2a72 74 | msgid "Return the current request object." 75 | msgstr "" 76 | 77 | #~ msgid "The Backendpy ASGI application class" 78 | #~ msgstr "" 79 | 80 | #~ msgid "" 81 | #~ ":py:obj:`event `\\ " 82 | #~ "\\(\\*args\\, \\*\\*kwargs\\)" 83 | #~ msgstr "" 84 | 85 | #~ msgid "" 86 | #~ ":py:obj:`execute_event `\\" 87 | #~ " \\(\\*args\\, \\*\\*kwargs\\)" 88 | #~ msgstr "" 89 | 90 | #~ msgid ":py:obj:`uri `\\ \\(\\*args\\, \\*\\*kwargs\\)" 91 | #~ msgstr "" 92 | 93 | -------------------------------------------------------------------------------- /backendpy/templating.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import inspect 4 | import os 5 | from collections.abc import Mapping, Iterable 6 | from typing import Optional, Any 7 | 8 | import aiofiles 9 | 10 | from .utils.bytes import to_bytes 11 | 12 | try: 13 | import jinja2 14 | except ImportError: 15 | pass 16 | 17 | 18 | class Template: 19 | """A class for reading and rendering templates (requires jinja2 package to be installed).""" 20 | 21 | template_dirs = dict() 22 | 23 | def __init__( 24 | self, 25 | path: Optional[str | bytes] = None, 26 | source: Optional[str] = None): 27 | """ 28 | Initialize template instance. 29 | 30 | :param path: The name or path of the template file inside the application templates dir 31 | :param source: Source code of the template (This parameter is used when the template file 32 | path is not specified and we want to use source code instead of file) 33 | """ 34 | if path: 35 | self._path = path 36 | self._source = None 37 | elif source: 38 | self._source = source 39 | else: 40 | raise ValueError('Template path or source is not specified') 41 | self._template = None 42 | 43 | def _get_caller_app_template_dirs(self) -> list[str]: 44 | call_path = inspect.stack()[2].filename 45 | for app_path, app_template_dirs in self.template_dirs.items(): 46 | if call_path.startswith(app_path): 47 | return app_template_dirs 48 | else: 49 | raise FileNotFoundError('Template dir not found') 50 | 51 | @staticmethod 52 | async def _read_template( 53 | template_paths: Iterable[str], 54 | path: str | bytes) -> str: 55 | for template_path in template_paths: 56 | try: 57 | async with aiofiles.open(os.path.join(to_bytes(template_path), to_bytes(path)), mode='r') as template: 58 | return str(await template.read()) 59 | except FileNotFoundError: 60 | pass 61 | else: 62 | raise FileNotFoundError('Template file not found') 63 | 64 | async def render( 65 | self, 66 | context: Optional[Mapping[str, Any]] = None, 67 | **kwargs) -> str: 68 | """ 69 | Render the context inside the template. 70 | 71 | :param context: Context values (in the structure of a mapping) that the template 72 | variables will take 73 | :param kwargs: Context values (taken as a keyword arguments) that the template 74 | variables will take 75 | :return: Rendered template in string format 76 | """ 77 | _context = kwargs 78 | if context and isinstance(context, Mapping): 79 | _context.update(context) 80 | 81 | if self._template is None: 82 | if self._source is None: 83 | self._source = await self._read_template(self._get_caller_app_template_dirs(), self._path) 84 | self._template = jinja2.Template(source=self._source, enable_async=True) 85 | 86 | return await self._template.render_async(_context) 87 | -------------------------------------------------------------------------------- /backendpy/hook.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping, Iterable 4 | from inspect import iscoroutinefunction 5 | from typing import Optional, Any 6 | 7 | 8 | class Hooks: 9 | """Hook registry class""" 10 | 11 | def __init__(self) -> None: 12 | self._items: dict[str, list[callable]] = dict() 13 | 14 | @property 15 | def items(self) -> dict[str, list[callable]]: 16 | """Get registered event hooks.""" 17 | return self._items 18 | 19 | def register(self, event_name: str, func: callable) -> None: 20 | """Register an event hook.""" 21 | if not iscoroutinefunction(func): 22 | raise TypeError('The "func" parameter must be an asynchronous function.') 23 | self._register(event_name, func) 24 | 25 | def register_batch(self, items: Mapping[str, Iterable[callable]]) -> None: 26 | """Register multiple event hooks at once.""" 27 | for event_name, funcs in items.items(): 28 | for func in funcs: 29 | self.register(event_name, func) 30 | 31 | def event(self, name: str) -> callable: 32 | """Register an event hook with python decorator. 33 | 34 | :param name: The name of an event 35 | """ 36 | def decorator_register(func: callable) -> None: 37 | self.register(name, func) 38 | return decorator_register 39 | 40 | def merge(self, other: Hooks) -> None: 41 | """Merge items from another Hooks class instance with this instance.""" 42 | if not isinstance(other, self.__class__): 43 | raise TypeError(f"{type(self.__class__)} and {type(other)} cannot be merged.") 44 | for event_name, funcs in other.items.items(): 45 | for func in funcs: 46 | self._register(event_name, func) 47 | 48 | def _register(self, event_name: str, func: callable) -> None: 49 | if event_name not in self._items: 50 | self._items[event_name]: list[callable] = list() 51 | self._items[event_name].append(func) 52 | 53 | def __getitem__(self, name: str) -> list[callable]: 54 | return self._items[name] 55 | 56 | def __contains__(self, name: str) -> bool: 57 | return name in self._items 58 | 59 | def __add__(self, other: Hooks) -> Hooks: 60 | """Concatenate items from two instances of the Hooks class and return a new instance.""" 61 | if not isinstance(other, self.__class__): 62 | raise TypeError(f"{type(self.__class__)} and {type(other)} cannot be concatenated.") 63 | new = self.__class__() 64 | new.register_batch(self._items) 65 | new.register_batch(other.items) 66 | return new 67 | 68 | 69 | class HookRunner: 70 | """Class for registering and triggering project hooks.""" 71 | 72 | def __init__(self) -> None: 73 | self.hooks = Hooks() 74 | 75 | async def trigger(self, name: str, args: Optional[Mapping[str, Any]] = None) -> None: 76 | """Trigger all hooks related to the event. 77 | 78 | :param name: The name of an event 79 | :param args: A dictionary-like object containing arguments passed to the hook function. 80 | """ 81 | if name in self.hooks: 82 | for func in self.hooks[name]: 83 | if args is not None: 84 | await func(**args) 85 | else: 86 | await func() 87 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/environments.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-02-17 17:55+0330\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=utf-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: Babel 2.9.1\n" 17 | 18 | #: ../../environments.rst:2 2fa81538dbc043e2a7eb5f0306f98b94 19 | msgid "Environments" 20 | msgstr "‫محیط‌ها‬" 21 | 22 | #: ../../environments.rst:4 6ed94f5423cb4f8d970622860e3e9859 23 | msgid "" 24 | "In Backendpy framework, it is possible to use different environments for " 25 | "project execution (such as development, production, etc.)." 26 | msgstr "" 27 | "‫در چارچوب Backendpy امکان استفاده از محیط‌های متفاوت برای اجرای پروژه " 28 | "(مثل Development، Production و …) وجود دارد.‬" 29 | 30 | #: ../../environments.rst:7 3b84f949ce9e418da3579525ff37c4d9 31 | msgid "" 32 | "Each environment has a different config file and can have different " 33 | "database settings, media paths, and so on." 34 | msgstr "" 35 | "‫هرکدام از محیط‌ها دارای فایل تنظیمات متفاوتی است و می‌تواند دارای " 36 | "تنظیمات پایگاه داده‌ی متفاوت،‌ مسیر media متفاوت و … باشد.‬" 37 | 38 | #: ../../environments.rst:9 c021f29ebf3942bca82919c8259541a3 39 | msgid "" 40 | "In a development team, different parts of the team can use their own " 41 | "environments and settings to execute and work on the project. Also, if " 42 | "needed on the main server, the project can be executed with another " 43 | "configuration in parallel, on another host or port." 44 | msgstr "" 45 | "‫در یک تیم توسعه، بخش‌های مختلف تیم می‌توانند از محیط‌ها و تنظیمات " 46 | "اختصاصی خود برای اجرا و کار روی پروژه استفاده کنند. همچنین درصورت نیاز در" 47 | " سرور اصلی می‌توان پروژه را با تنظیمات دیگری و به‌طور موازی روی host یا " 48 | "port دیگری اجرا کرد.‬" 49 | 50 | #: ../../environments.rst:14 ffa96117775a4754a277b9d51131b9d4 51 | msgid "" 52 | "To do this, you need to define the ``BACKENDPY_ENV`` variable in the os " 53 | "with the desired name for the environment. For example, to define an " 54 | "environment called \"dev\", we use the following command:" 55 | msgstr "" 56 | "‫برای این منظور باید متغیر ``BACKENDPY_ENV`` را در سیستم‌عامل ،با نام " 57 | "دلخواه برای محیط، تعریف کرد. برای مثال برای تعریف محیطی با نام dev از " 58 | "دستور زیر استفاده می‌کنیم:‬" 59 | 60 | #: ../../environments.rst:21 06efb630294e40feb5cd0c5c9a3b25da 61 | msgid "" 62 | "You must also create and configure a separate configuration file named " 63 | "``config.dev.ini``." 64 | msgstr "" 65 | "‫همچنین باید فایل تنظیمات جداگانه‌ای با نام ``config.dev.ini`` را ایجاد و" 66 | " تنظیم کنیم.‬" 67 | 68 | #: ../../environments.rst:23 8ece0d7b73964e47852f2e085f402f66 69 | msgid "" 70 | "Now if the server runs (in the process that the environmental variable is" 71 | " exported) or when other backendpy management commands are executed, this" 72 | " configuration will be used instead of the original configuration." 73 | msgstr "" 74 | "‫اکنون با اجرای سرور (در processای که متغیر محیطی در آن تعریف شده باشد) و" 75 | " همچنین هنگام اجرای سایر دستورات مدیریتی خط‌فرمان backendpy، این تنظیمات " 76 | "به جای تنظیمات اصلی، مورد استفاده قرار خواهد گرفت.‬" 77 | 78 | -------------------------------------------------------------------------------- /docs/project_creation.rst: -------------------------------------------------------------------------------- 1 | Create a project 2 | ================ 3 | 4 | Basic structure 5 | --------------- 6 | A Backendpy-based project does not have a mandatory, predetermined structure, and it is the programmer who 7 | decides how to structure his project according to his needs. 8 | 9 | The programmer only needs to create a Python module with a custom name (for example "main.py") and set the 10 | instance of :class:`~backendpy.Backendpy` class (which is an ASGI application) inside it. 11 | 12 | .. code-block:: python 13 | :caption: project/main.py 14 | 15 | from backendpy import Backendpy 16 | 17 | bp = Backendpy() 18 | 19 | Also for project settings, the ``config.ini`` file must be created in the same path next to the module. 20 | Check out the :doc:`configurations` section for more information. 21 | 22 | Applications 23 | ------------ 24 | Backendpy projects are developed by components called Applications. 25 | It is also possible to connect third-party apps to the project. 26 | 27 | To create an application, first create a package containing the ``main.py`` module in the desired path within 28 | the project (or any other path that can be imported). 29 | 30 | Then inside the main.py module of an application we need to set an instance of the :class:`~backendpy.app.App` class. 31 | All parts and settings of an application are assigned by the parameters of the App class. 32 | 33 | For example, in the "/apps" path inside the project, we create a package called "hello" and main.py file as follows: 34 | 35 | .. code-block:: python 36 | :caption: project/apps/hello/main.py 37 | 38 | from backendpy.app import App 39 | from .handlers import routes 40 | 41 | app = App( 42 | routes=[routes]) 43 | 44 | .. code-block:: python 45 | :caption: project/apps/hello/handlers.py 46 | 47 | from backendpy.router import Routes 48 | from backendpy.response import Text 49 | 50 | routes = Routes() 51 | 52 | @routes.get('/hello-world') 53 | async def hello_world(request): 54 | return Text('Hello World!') 55 | 56 | As you can see, we have created another optional module called handlers.py and then introduced the routes 57 | defined in it to the App class instance. 58 | The complete list of App class parameters is described in section :doc:`application_structure`. 59 | 60 | Only the items that are introduced to the App class are important to the framework, and the internal structuring 61 | of the applications is completely optional. 62 | 63 | Our application is now ready and you just need to enable it in the project config.ini file as follows: 64 | 65 | .. code-block:: 66 | :caption: project/config.ini 67 | 68 | [apps] 69 | active = 70 | project.apps.hello 71 | 72 | To run the project, see the :doc:`run` section. 73 | 74 | Refer to the :doc:`apps` section to learn how to develop applications. 75 | 76 | Command line 77 | ------------ 78 | The ``backendpy`` command can also be used to create projects and apps. 79 | To do this, first enter the desired path and then use the following commands: 80 | 81 | Project creation 82 | ```````````````` 83 | 84 | .. code-block:: console 85 | 86 | $ backendpy create_project --name myproject 87 | 88 | To create a project with more complete sample components: 89 | 90 | .. code-block:: console 91 | 92 | $ backendpy create_project --name myproject --full 93 | 94 | App creation 95 | ```````````` 96 | 97 | .. code-block:: console 98 | 99 | $ backendpy create_app --name myapp 100 | 101 | .. code-block:: console 102 | 103 | $ backendpy create_app --name myapp --full 104 | 105 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/application_structure.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-06-04 17:49+0430\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=utf-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: Babel 2.9.1\n" 17 | 18 | #: ../../application_structure.rst:2 460905e2e37d4be19358f82fc9e73244 19 | msgid "Applications structure" 20 | msgstr "‫ساختار اپلیکیشن‌ها‬" 21 | 22 | #: ../../application_structure.rst:3 823e027c2c45484b918334a09fdc38a0 23 | msgid "" 24 | "In section :doc:`project_creation`, we talked about how to create and " 25 | "activate a basic application in a Backendpy-based project. In this " 26 | "section, we describe the complete components of an application." 27 | msgstr "" 28 | 29 | #: ../../application_structure.rst:6 e6ec51bc03f844beb99337538f6b2c6e 30 | msgid "" 31 | "As mentioned earlier, a Backendpy-based application does not have a " 32 | "predefined structure or constraint. In fact, the developer is free to " 33 | "implement the desired architecture for the application and finally import" 34 | " and configure all the components inside the main module of the " 35 | "application." 36 | msgstr "" 37 | 38 | #: ../../application_structure.rst:10 cbc3d2525d254f19ab9f7765f8ac5fd2 39 | msgid "" 40 | "The main module of an application must contain an instance of the " 41 | ":class:`~backendpy.app.App` class that is defined inside a variable " 42 | "called ``app``. Below is an example of defining an app with all its " 43 | "possible parameters (which are used to assign components to an " 44 | "application):" 45 | msgstr "" 46 | 47 | #: ../../application_structure.rst:15 f9f6630ed96144fbab3263f6e00ae85b 48 | msgid "project/apps/hello/main.py" 49 | msgstr "" 50 | 51 | #: ../../application_structure.rst:32 f0a9184e29de441f98b24c62b2994249 52 | msgid "An :class:`~backendpy.app.App` class has the following parameters:" 53 | msgstr "" 54 | 55 | #: a3ed2a9241a14a199f407a2478b31ac5 backendpy.app.App:1 of 56 | msgid "A class used to define Backendpy internal application." 57 | msgstr "" 58 | 59 | #: 7910ca5a394c464b971982da976e68ba backendpy.app.App of 60 | msgid "Variables" 61 | msgstr "" 62 | 63 | #: backendpy.app.App:3 fa9df37a758e437d9b3424a4855347f2 of 64 | msgid "Iterable of instances of the Routes class" 65 | msgstr "" 66 | 67 | #: 38355b2e72d148daa7f4464286ff2617 backendpy.app.App:4 of 68 | msgid "Iterable of instances of the Hooks class (or None)" 69 | msgstr "" 70 | 71 | #: b13045b10f8b4d59ba69e73b051bdaf2 backendpy.app.App:5 of 72 | msgid "Iterable of module paths that contain database models (or None)" 73 | msgstr "" 74 | 75 | #: 3d988a16aefd46cf87e37d81a3aaa587 backendpy.app.App:6 of 76 | msgid "" 77 | "Iterable of paths (within the application directory) from which templates" 78 | " will be searched (or None)" 79 | msgstr "" 80 | 81 | #: 865f5dd10a5e43cd89b66e036f40906b backendpy.app.App:8 of 82 | msgid "Iterable of instances of the ErrorList class (or None)" 83 | msgstr "" 84 | 85 | #: 1a027a43bf4f4950b893dbd09d9cd1ea backendpy.app.App:9 of 86 | msgid "The initialization function of the application (or None)" 87 | msgstr "" 88 | 89 | #: ../../application_structure.rst:37 a51d3d69cd5d450294d96c3ce232dc7a 90 | msgid "" 91 | "In the following, we will describe each of these components of the " 92 | "application, as well as other items that can be used in applications." 93 | msgstr "" 94 | 95 | -------------------------------------------------------------------------------- /docs/responses.rst: -------------------------------------------------------------------------------- 1 | Responses 2 | ========= 3 | To respond to a request, we use instances of the :class:`~backendpy.response.Response` class or its subclasses 4 | inside the handler function. 5 | Default Backendpy responses include :class:`~backendpy.response.Text`, :class:`~backendpy.response.HTML`, 6 | :class:`~backendpy.response.JSON`, :class:`~backendpy.response.Binary`, :class:`~backendpy.response.File`, 7 | and :class:`~backendpy.response.Redirect`, but you can also create your own custom response types by extending 8 | the :class:`~backendpy.response.Response` class. 9 | 10 | The details of each of the default response classes are as follows: 11 | 12 | .. autoclass:: backendpy.response.Response 13 | :noindex: 14 | 15 | .. autoclass:: backendpy.response.Text 16 | :noindex: 17 | 18 | Example usage: 19 | 20 | .. code-block:: python 21 | :caption: project/apps/hello/handlers.py 22 | 23 | from backendpy.router import Routes 24 | from backendpy.response import Text 25 | 26 | routes = Routes() 27 | 28 | @routes.get('/hello-world') 29 | async def hello_world(request): 30 | return Text('Hello World!') 31 | 32 | 33 | .. autoclass:: backendpy.response.JSON 34 | :noindex: 35 | 36 | Example usage: 37 | 38 | .. code-block:: python 39 | :caption: project/apps/hello/handlers.py 40 | 41 | from backendpy.router import Routes 42 | from backendpy.response import JSON 43 | 44 | routes = Routes() 45 | 46 | @routes.get('/hello-world') 47 | async def hello_world(request): 48 | return JSON({'message': 'Hello World!'}) 49 | 50 | 51 | .. autoclass:: backendpy.response.HTML 52 | :noindex: 53 | 54 | Example usage: 55 | 56 | .. code-block:: python 57 | :caption: project/apps/hello/handlers.py 58 | 59 | from backendpy.router import Routes 60 | from backendpy.response import HTML 61 | 62 | routes = Routes() 63 | 64 | @routes.get('/hello-world') 65 | async def hello_world(request): 66 | return HTML('Hello World!') 67 | 68 | 69 | .. autoclass:: backendpy.response.Binary 70 | :noindex: 71 | 72 | .. autoclass:: backendpy.response.File 73 | :noindex: 74 | 75 | Example usage: 76 | 77 | .. code-block:: python 78 | :caption: project/apps/hello/handlers.py 79 | 80 | import os 81 | from backendpy.router import Routes 82 | from backendpy.response import File 83 | 84 | routes = Routes() 85 | 86 | @routes.get('/hello-world') 87 | async def hello_world(request): 88 | return File(os.path.join('images', 'logo.jpg')) 89 | 90 | 91 | .. autoclass:: backendpy.response.Redirect 92 | :noindex: 93 | 94 | There is another type of response to quickly return a success response in predefined json format, 95 | which is as follows: 96 | 97 | .. autoclass:: backendpy.response.Success 98 | :noindex: 99 | 100 | Example usage: 101 | 102 | .. code-block:: python 103 | :caption: project/apps/hello/handlers.py 104 | 105 | from backendpy.router import Routes 106 | from backendpy.response import Success 107 | 108 | routes = Routes() 109 | 110 | @routes.get('/hello-world') 111 | async def hello_world(request): 112 | return Success() 113 | 114 | @routes.post('/login') 115 | async def login(request): 116 | return Success('Successful login!') 117 | 118 | .. note:: 119 | The json format used in the :class:`~backendpy.response.Success` response is similar to the 120 | :class:`~backendpy.error.Error` response, and these two types of responses can be used together in a project. 121 | Refer to the :doc:`predefined_errors` section for information on how to use the :class:`~backendpy.error.Error` 122 | response. -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/templates.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-06-04 17:59+0430\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=utf-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: Babel 2.9.1\n" 17 | 18 | #: ../../templates.rst:2 47ca62915bb14e10a27b8517851f9272 19 | msgid "Templates" 20 | msgstr "‫قالب‌ها‬" 21 | 22 | #: ../../templates.rst:3 15ccd62cca2844c1b1b73a87ce8fd4bc 23 | msgid "" 24 | "The need to render templates on the server side is used in some projects." 25 | " External template engines that support async can be used for this " 26 | "purpose. You can also use the framework helper layer for this, which is a" 27 | " layer for using the Jinja2 template engine package. The Backendpy " 28 | "framework facilitates the use of templates with this template engine and " 29 | "adapts it to its architecture with things like template files async " 30 | "reading from predefined application template paths." 31 | msgstr "" 32 | 33 | #: ../../templates.rst:9 8b582f83951b4aa48e66ace09f7d5bf2 34 | msgid "" 35 | "An example of rendering a web page template and returning it as a " 36 | "response is as follows." 37 | msgstr "" 38 | 39 | #: ../../templates.rst:11 2b4d2fc78fca470693ef2630edf8afdf 40 | msgid "" 41 | "First we need to specify the application template dirs inside the " 42 | "application ``main.py`` module with the ``template_dirs`` parameter of " 43 | "the App class:" 44 | msgstr "" 45 | 46 | #: ../../templates.rst:14 145c33b63f1743d8a2f79751f8ae6dd6 47 | msgid "project/apps/hello/main.py" 48 | msgstr "" 49 | 50 | #: ../../templates.rst:24 fe2179bd3f7842a3a6e7fd3fd02a57b0 51 | msgid "Then we create the desired templates in the defined path:" 52 | msgstr "" 53 | 54 | #: ../../templates.rst:26 4c956b1563e54f6ab1295ba47389c8ed 55 | msgid "project/apps/hello/templates/home.html" 56 | msgstr "" 57 | 58 | #: ../../templates.rst:39 dde32cdde7b04ca7848d163ef66f4e2b 59 | msgid "Refer to the Jinja2 package documentation to learn the templates syntax." 60 | msgstr "" 61 | 62 | #: ../../templates.rst:41 194964d88ae346e2a18ccd8a1f57936e 63 | msgid "Finally, we use these template inside a handler:" 64 | msgstr "" 65 | 66 | #: ../../templates.rst:43 4ae8ce640d0c419ca3d6a13a67706e5e 67 | msgid "project/apps/hello/controllers/handlers.py" 68 | msgstr "" 69 | 70 | #: ../../templates.rst:57 bfef38d1ad4b4921b200053e35a97b86 71 | msgid "" 72 | "In this example code, we first initialize " 73 | ":class:`~backendpy.templating.Template` class with the template name and " 74 | "then with the ``render`` method we render the context values in it (note " 75 | "that this method must also be called async) and then we return the final " 76 | "content with :class:`~backendpy.response.HTML` response." 77 | msgstr "" 78 | 79 | #: ../../templates.rst:61 c891fde0b3da44e79c188b415dd0d90a 80 | msgid "" 81 | "Also here you just need to enter the template name and the framework will" 82 | " automatically search for this name in the application template dirs." 83 | msgstr "" 84 | 85 | #: ../../templates.rst:64 3e57b03be2e8498aaa0d80cea37efb99 86 | msgid "" 87 | "Details of the :class:`~backendpy.templating.Template` class are as " 88 | "follows:" 89 | msgstr "" 90 | 91 | #: b07cbf9b1097478bac20bca122c6e877 backendpy.templating.Template:1 of 92 | msgid "" 93 | "A class for reading and rendering templates (requires jinja2 package to " 94 | "be installed)." 95 | msgstr "" 96 | 97 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/generated/backendpy.app.App.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: Backendpy \n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-06-04 17:49+0430\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=utf-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Generated-By: Babel 2.9.1\n" 19 | 20 | #: ../../generated/backendpy.app.App.rst:2 5027887e5af749b8a3f8795934d3aac8 21 | msgid "backendpy.app.App" 22 | msgstr "" 23 | 24 | #: 4f25c9347cb9463bbdcff3a58036b910 backendpy.app.App:1 of 25 | msgid "A class used to define Backendpy internal application." 26 | msgstr "" 27 | 28 | #: backendpy.app.App f1307d6ceae346ea929f1402c73ea56b of 29 | msgid "Variables" 30 | msgstr "" 31 | 32 | #: 99edb44c69f24ba9ba89f60769ba130b backendpy.app.App:3 33 | #: backendpy.app.App.__init__:3 c1adcd8dbe7b452996b12b5c891e7232 of 34 | msgid "Iterable of instances of the Routes class" 35 | msgstr "" 36 | 37 | #: backendpy.app.App:4 backendpy.app.App.__init__:4 38 | #: c4a88bd54c394cf89a7175f100ff35ef d333637d5919439eb82b27d598f7c099 of 39 | msgid "Iterable of instances of the Hooks class (or None)" 40 | msgstr "" 41 | 42 | #: 36baf0b6b40d435789e13ce384daf955 8f50728e1bac4c10a5a0563ecd4bc177 43 | #: backendpy.app.App:5 backendpy.app.App.__init__:5 of 44 | msgid "Iterable of module paths that contain database models (or None)" 45 | msgstr "" 46 | 47 | #: 90eb0b638223478b86fec13f9d72aa69 backendpy.app.App:6 48 | #: backendpy.app.App.__init__:6 ca248d88e03041d2b366698f823f78f3 of 49 | msgid "" 50 | "Iterable of paths (within the application directory) from which templates" 51 | " will be searched (or None)" 52 | msgstr "" 53 | 54 | #: 039e450fb5fb4d0eb0324e1e4c35944d 7dc4d458b18744ff80f6eefe665daf8f 55 | #: backendpy.app.App:8 backendpy.app.App.__init__:8 of 56 | msgid "Iterable of instances of the ErrorList class (or None)" 57 | msgstr "" 58 | 59 | #: 88bb24c157b7434dbe904f070afd84ca backendpy.app.App:9 60 | #: backendpy.app.App.__init__:9 d2de78d3ff45487fab9f057720174e10 of 61 | msgid "The initialization function of the application (or None)" 62 | msgstr "" 63 | 64 | #: ../../generated/backendpy.app.App.rst:16::1 65 | #: 650efeba43e94f1fa3bb610f760b269d backendpy.app.App.__init__:1 66 | #: e700d41e6a274611bfe1f1ca47c73422 of 67 | msgid "Initialize application instance" 68 | msgstr "" 69 | 70 | #: backendpy.app.App.__init__ bd319da3b7a94ff9b0eef47ea9ac3a9f of 71 | msgid "Parameters" 72 | msgstr "" 73 | 74 | #: ../../generated/backendpy.app.App.rst:13 eaf3318bbe774aac92e95e53f20e8d9c 75 | msgid "Methods" 76 | msgstr "" 77 | 78 | #: ../../generated/backendpy.app.App.rst:16::1 79 | #: 8a843fbbbbb249ea8f981d9a32b3fc19 80 | msgid "" 81 | ":py:obj:`__init__ `\\ \\(routes\\[\\, " 82 | "hooks\\, models\\, ...\\]\\)" 83 | msgstr "" 84 | 85 | #~ msgid "A class used to represent Backendpy internal application" 86 | #~ msgstr "" 87 | 88 | #~ msgid "List of instances of the Routes class" 89 | #~ msgstr "" 90 | 91 | #~ msgid "List of instances of the Hooks class (or None)" 92 | #~ msgstr "" 93 | 94 | #~ msgid "List of module paths that contain database models (or None)" 95 | #~ msgstr "" 96 | 97 | #~ msgid "" 98 | #~ "List of paths (within the application" 99 | #~ " directory) from which templates will " 100 | #~ "be searched (or None)" 101 | #~ msgstr "" 102 | 103 | #~ msgid "List of instances of the ErrorList class (or None)" 104 | #~ msgstr "" 105 | 106 | -------------------------------------------------------------------------------- /backendpy/data_handler/data.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import inspect 4 | from collections.abc import Mapping 5 | from copy import deepcopy 6 | from typing import Optional, Any 7 | 8 | from .fields import Field 9 | from .fields import TYPE_JSON_FIELD, TYPE_FORM_FIELD, TYPE_PARAM, TYPE_URL_VAR, TYPE_FILE, TYPE_CONTENT, TYPE_HEADER 10 | from ..request import Request 11 | 12 | 13 | class Data: 14 | """The base class that will be inherited to create data handler classes.""" 15 | 16 | def __init__( 17 | self, 18 | default: Optional[Mapping[str, Any]] = None, 19 | auto_blank_to_null: Optional[bool] = True): 20 | """ 21 | Initialize data handler instance. 22 | 23 | :param request: :class:`~backendpy.request.Request` class instance 24 | :param default: Optional default values for the data handler fields 25 | """ 26 | self._fields = {i[0]: deepcopy(i[1]) for i in inspect.getmembers(self) if isinstance(i[1], Field)} 27 | self._default_data = default if type(default) is dict else \ 28 | (default.__dict__ if hasattr(default, "__dict__") else {}) 29 | self.auto_blank_to_null = auto_blank_to_null 30 | 31 | async def get_cleaned_data(self, request: Request) \ 32 | -> tuple[dict[str, Optional[Any]], 33 | dict[str, str | list[str]]]: 34 | """Return the processed data of the data handler class and related error messages.""" 35 | data = dict() 36 | cleaned_data = dict() 37 | errors = dict() 38 | for name, field in self._fields.items(): 39 | k = field.data_name if field.data_name else name 40 | if field.type == TYPE_JSON_FIELD \ 41 | and request.body.json is not None \ 42 | and k in request.body.json: 43 | data[name] = request.body.json[k] 44 | elif field.type == TYPE_FORM_FIELD \ 45 | and request.body.form is not None \ 46 | and k in request.body.form: 47 | data[name] = request.body.form[k] 48 | elif field.type == TYPE_PARAM \ 49 | and request.params is not None \ 50 | and k in request.params: 51 | data[name] = request.params[k] 52 | elif field.type == TYPE_URL_VAR \ 53 | and request.url_vars is not None \ 54 | and k in request.url_vars: 55 | data[name] = request.url_vars[k] 56 | elif field.type == TYPE_FILE \ 57 | and request.body.files is not None \ 58 | and k in request.body.files: 59 | data[name] = request.body.files[k]['content'] 60 | elif field.type == TYPE_CONTENT \ 61 | and request.body.content is not None: 62 | data[name] = request.body.content 63 | elif field.type == TYPE_HEADER \ 64 | and k in request.headers: 65 | data[name] = request.headers[k] 66 | elif name in self._default_data: 67 | data[name] = self._default_data[name] 68 | for name, field in self._fields.items(): 69 | if name in data: 70 | await field.set_value( 71 | value=data[name] if ((data[name] != '' and data[name] != b'') 72 | or not self.auto_blank_to_null) else None, 73 | meta={'name': name, 74 | 'received_data': data, 75 | 'request': request}) 76 | if field.errors: 77 | errors[name] = field.errors 78 | cleaned_data[name] = field.value 79 | elif field.value is not None: 80 | # Set default value if the field value is not sent 81 | cleaned_data[name] = field.value 82 | elif field.required: 83 | errors[name] = 'Required' 84 | return cleaned_data, errors 85 | -------------------------------------------------------------------------------- /docs/hooks.rst: -------------------------------------------------------------------------------- 1 | Hooks 2 | ===== 3 | Sometimes it is necessary to perform a specific operation following an event. For these types of needs, we can use 4 | Backendpy hooks feature. 5 | For example, when we want to write an email management application that sends an email after certain events in other 6 | applications, such as the registration or login of users. 7 | 8 | With this feature, we can both define new events with special labels within our application as points so that others 9 | can write their own code for these events to run, or we can assign codes to execute when triggering other events on 10 | the system. 11 | 12 | Event Definition 13 | ---------------- 14 | To define event points, we use the ``execute_event`` method of the :class:`~backendpy.Backendpy` class instance 15 | inside any space we have access to this instance. (For example, inside the handler of a request, we access the 16 | project :class:`~backendpy.Backendpy` instance via ``request.app``). 17 | 18 | Example of defining user creation event: 19 | 20 | .. code-block:: python 21 | 22 | @routes.post('/users', data_handler=UserCreationData) 23 | async def user_creation(request): 24 | ... 25 | await request.app.execute_event('user_created') 26 | ... 27 | 28 | If the event also contains arguments, we send them in the second parameter in the form of a dictionary: 29 | 30 | .. code-block:: python 31 | 32 | @routes.post('/users', data_handler=UserCreationData) 33 | async def user_creation(request): 34 | ... 35 | await request.app.execute_event('user_created', {'username': username}) 36 | ... 37 | 38 | 39 | Default Events 40 | .............. 41 | In addition to the events that developers can add to the project, the default events are also provided in the 42 | framework as follows: 43 | 44 | .. list-table:: Framework Default Events 45 | :widths: 30 70 46 | :header-rows: 1 47 | 48 | * - Label 49 | - Description 50 | * - ``startup`` 51 | - After successfully starting the ASGI server 52 | * - ``shutdown`` 53 | - After the ASGI server shuts down 54 | * - ``request_start`` 55 | - At the start of a request 56 | * - ``request_end`` 57 | - After the response to a request is returned 58 | 59 | 60 | Hook Definition 61 | --------------- 62 | To define the code that is executed in events, we use the :class:`~backendpy.hook.Hooks` class and its ``event`` 63 | decorator: 64 | 65 | .. code-block:: python 66 | :caption: project/apps/hello/controllers/hooks.py 67 | 68 | from backendpy.hook import Hooks 69 | from backendpy.logging import get_logger 70 | 71 | LOGGER = get_logger(__name__) 72 | hooks = Hooks() 73 | 74 | @hooks.event('startup') 75 | async def example(): 76 | LOGGER.debug("Server starting") 77 | 78 | @hooks.event('user_created') 79 | async def example2(username): 80 | LOGGER.debug(f"User '{username}' creation") 81 | 82 | As can be seen, if an argument is sent to a hook, these arguments are received in the parameters of the hook functions, 83 | otherwise they have no parameter. 84 | 85 | Here we have written the hooks inside a custom module. To connect these hooks to the application, like the other 86 | components, we use the ``main.py`` module of the application: 87 | 88 | .. code-block:: python 89 | :caption: project/apps/hello/main.py 90 | 91 | from backendpy.app import App 92 | from .controllers.hooks import hooks 93 | 94 | app = App( 95 | ... 96 | hooks=[hooks], 97 | ...) 98 | 99 | Another way to use hooks is to attach them directly to a project (instead of an application), which can be used for 100 | special purposes such as managing database connections, which are part of the project-level settings: 101 | 102 | .. code-block:: python 103 | :caption: project/main.py 104 | 105 | from backendpy import Backendpy 106 | 107 | bp = Backendpy() 108 | 109 | @bp.event('startup') 110 | async def on_startup(): 111 | LOGGER.debug("Server starting") 112 | -------------------------------------------------------------------------------- /backendpy/middleware/middleware.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | from ..request import Request 4 | from ..response import Response 5 | from ..exception import ExceptionResponse 6 | 7 | 8 | class Middleware: 9 | 10 | @staticmethod 11 | def process_application(application): 12 | """ 13 | Take instance of :class:`~backendpy.Backendpy` class and return modified version of it. 14 | :param application: :class:`~backendpy.Backendpy` class instance (Received from the middlewares queue) 15 | :return: Modified :class:`~backendpy.Backendpy` class instance 16 | """ 17 | return application 18 | 19 | @staticmethod 20 | async def process_request(request: Request): 21 | """ 22 | Take a :class:`~backendpy.request.Request` object before it reaches the handler layer and return a processed 23 | or modified version of it or interrupt the normal execution of the request with raise an exception response 24 | or return a direct response in the second index of return tuple. 25 | :param request: :class:`~backendpy.request.Request` class instance (Received from the middlewares queue) 26 | :return: A pair of the modified :class:`~backendpy.request.Request` instance and None or optional direct 27 | :class:`~backendpy.response.Response` instance. 28 | """ 29 | return request, None 30 | 31 | @staticmethod 32 | async def process_handler(request: Request, handler): 33 | """ 34 | Take a request handler (which is an async function) before executing it and return a modified version of it 35 | or interrupt the execution of the request with raise an exception response. 36 | :param request: :class:`~backendpy.request.Request` class instance (Received from the middlewares queue) 37 | :param handler: Async handler function (Received from the middlewares queue) 38 | :return: Modified handler function 39 | """ 40 | return handler 41 | 42 | @staticmethod 43 | async def process_response(request: Request, response: Response): 44 | """ 45 | Capture the :class:`~backendpy.response.Response` object before sending it to the client and return a 46 | processed, modified, or replaced Response object or interrupt the execution of the request with raise an 47 | exception response. 48 | :param request: :class:`~backendpy.request.Request` class instance (Received from the middlewares queue) 49 | :param response: :class:`~backendpy.response.Response` class instance (Received from the middlewares queue) 50 | :return: Modified :class:`~backendpy.response.Response` class instance 51 | """ 52 | return response 53 | 54 | 55 | class MiddlewareProcessor: 56 | 57 | def __init__(self, paths=None): 58 | self._middlewares_paths = paths if paths else [] 59 | self._middlewares = [] 60 | 61 | @property 62 | def middlewares(self): 63 | if not self._middlewares: 64 | for m in self._middlewares_paths: 65 | module_name, class_name = m.rsplit('.', 1) 66 | self._middlewares.append(getattr(importlib.import_module(module_name), class_name)()) 67 | return self._middlewares 68 | 69 | def run_process_application(self, application): 70 | for middleware in self.middlewares: 71 | application = middleware.process_application(application) 72 | return application 73 | 74 | async def run_process_request(self, request): 75 | for middleware in self.middlewares: 76 | try: 77 | request, direct_response = await middleware.process_request(request) 78 | if direct_response: 79 | return request, direct_response 80 | except ExceptionResponse as e: 81 | return request, e 82 | return request, None 83 | 84 | async def run_process_handler(self, request, handler): 85 | for middleware in self.middlewares: 86 | handler = await middleware.process_handler(request, handler) 87 | return handler 88 | 89 | async def run_process_response(self, request, response): 90 | for middleware in reversed(self.middlewares): 91 | response = await middleware.process_response(request, response) 92 | return response 93 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/exceptions.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-06-04 17:49+0430\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=utf-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: Babel 2.9.1\n" 17 | 18 | #: ../../exceptions.rst:2 a6b5edf9fd284534b847d87bbc181458 19 | msgid "Exceptions" 20 | msgstr "‫Exceptionها‬" 21 | 22 | #: ../../exceptions.rst:3 05976afa856b43b1b853bb87f37f0591 23 | msgid "" 24 | "Backendpy exceptions are the type of responses used to return HTTP errors" 25 | " and can also be raised." 26 | msgstr "" 27 | 28 | #: ../../exceptions.rst:6 3497de16591f409f816a1136bea59d67 29 | msgid "" 30 | "As mentioned, Backendpy exceptions are the type of " 31 | ":class:`~backendpy.response.Response` and their content is returned as a " 32 | "response and displayed to the user. Therefore, these exceptions should be" 33 | " used only for errors that must be displayed to users, and any kind of " 34 | "internal system error should be created with normal Python exceptions, in" 35 | " which case, the :class:`~backendpy.exception.ServerError` response is " 36 | "displayed to the user with a public message and does not contain " 37 | "sensitive system information that may be contained in the internal " 38 | "exception message." 39 | msgstr "" 40 | 41 | #: ../../exceptions.rst:13 31544bfe4147415f8528fd60ece4bd38 42 | msgid "The list of default exception response classes are as follows:" 43 | msgstr "" 44 | 45 | #: b96dc35fe2cb494b9c707317f025b0fc backendpy.exception.ExceptionResponse:1 of 46 | msgid "" 47 | "Base exception response class that its status code and other parameters " 48 | "must be set manually. Also, by expanding this class, you can create all " 49 | "kinds of error responses." 50 | msgstr "" 51 | 52 | #: ad50f8049b354befab024f7654dc6173 backendpy.exception.ExceptionResponse of 53 | msgid "Variables" 54 | msgstr "" 55 | 56 | #: 37af197eb2244028906660bb40b42de0 backendpy.exception.ExceptionResponse:4 of 57 | msgid "The HTTP response body" 58 | msgstr "" 59 | 60 | #: 04386075a1f44ddea31e02c83bc97663 backendpy.exception.ExceptionResponse:5 of 61 | msgid "The HTTP response status" 62 | msgstr "" 63 | 64 | #: 526ecbfb0a174e8799b176734d4bdc80 backendpy.exception.ExceptionResponse:6 of 65 | msgid "The HTTP response headers" 66 | msgstr "" 67 | 68 | #: backendpy.exception.ExceptionResponse:7 f5def77c2b8247669b7bf22ec6284c20 of 69 | msgid "The HTTP response content type" 70 | msgstr "" 71 | 72 | #: backendpy.exception.ExceptionResponse:8 f0ffb6f601de479fa48ab145e3d745af of 73 | msgid "Determines whether or not to compress (gzip) the response" 74 | msgstr "" 75 | 76 | #: 435ec779bd994e609d5f62811393e6f2 backendpy.exception.BadRequest:1 of 77 | msgid "" 78 | "Bad request error response class inherited from " 79 | ":class:`~backendpy.exception.ExceptionResponse` class." 80 | msgstr "" 81 | 82 | #: ../../exceptions.rst:21 c2af2101098d4f6297c544685f4c32db 83 | msgid "Example usage:" 84 | msgstr "" 85 | 86 | #: ../../exceptions.rst:23 fc6d795b35a14a5e9af87394fc6adc15 87 | msgid "project/apps/hello/handlers.py" 88 | msgstr "" 89 | 90 | #: aa576e135fd9458fb79b12c077108329 backendpy.exception.Unauthorized:1 of 91 | msgid "" 92 | "Unauthorized request error response class inherited from " 93 | ":class:`~backendpy.exception.ExceptionResponse` class." 94 | msgstr "" 95 | 96 | #: backendpy.exception.Forbidden:1 bdb790de398845dc8f91b489caf57f4f of 97 | msgid "" 98 | "Forbidden request error response class inherited from " 99 | ":class:`~backendpy.exception.ExceptionResponse` class." 100 | msgstr "" 101 | 102 | #: 4a993ca1018f452d87b9ea994e03d611 backendpy.exception.NotFound:1 of 103 | msgid "" 104 | "Resource not found error response class inherited from " 105 | ":class:`~backendpy.exception.ExceptionResponse` class." 106 | msgstr "" 107 | 108 | #: 94db790171f4404a945e6659c2ca1dcc backendpy.exception.ServerError:1 of 109 | msgid "" 110 | "Server error response class inherited from " 111 | ":class:`~backendpy.exception.ExceptionResponse` class." 112 | msgstr "" 113 | 114 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/predefined_errors.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-06-04 17:56+0430\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=utf-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: Babel 2.9.1\n" 17 | 18 | #: ../../predefined_errors.rst:2 7f8e4d4d35614b92a478252636afac82 19 | msgid "Predefined errors" 20 | msgstr "‫خطاهای از پیش تعریف شده‬" 21 | 22 | #: ../../predefined_errors.rst:3 68895c41a55842fd9c31a51fe2251926 23 | msgid "" 24 | "To return a response with error content, you can manually return the " 25 | "content of that error with :doc:`exceptions` or create helper classes and" 26 | " assign specific templates to them. But if the number and variety of " 27 | "these errors is large, it is better to assign a code to each error in " 28 | "addition to a fixed format for errors." 29 | msgstr "" 30 | 31 | #: ../../predefined_errors.rst:8 8e3f2430cb234262ad59b21999d027d5 32 | msgid "" 33 | "In the Backendpy framework, a special class for managing error responses " 34 | "is provided to help better and easier manage errors and their code and to" 35 | " prevent the error code of one application from interfering with other " 36 | "applications:" 37 | msgstr "" 38 | 39 | #: 68d8cc95792543eb8a6d97343971cdcb backendpy.error.Error:1 of 40 | msgid "Predefined error response class." 41 | msgstr "" 42 | 43 | #: 504dab9e2674460f822ad8b28eb377f0 backendpy.error.Error of 44 | msgid "Variables" 45 | msgstr "" 46 | 47 | #: backendpy.error.Error:3 e9d6f7890aba48d1b5f070957ce1e152 of 48 | msgid "Predefined error code number" 49 | msgstr "" 50 | 51 | #: 1a736242437c435c92fedbb822f62e38 backendpy.error.Error:4 of 52 | msgid "Values used for error message formatting" 53 | msgstr "" 54 | 55 | #: backendpy.error.Error:5 f0455728a37f478c813db53dbf900ca4 of 56 | msgid "Error related data with a structure supported by JSON format" 57 | msgstr "" 58 | 59 | #: 262d87c944a54a34b77d2f1d902a9364 backendpy.error.Error:6 of 60 | msgid "The HTTP response headers" 61 | msgstr "" 62 | 63 | #: 4ec6b2ecad3540e6ac35ee520a9beb86 backendpy.error.Error:7 of 64 | msgid "Determines whether or not to compress (gzip) the response" 65 | msgstr "" 66 | 67 | #: ../../predefined_errors.rst:14 087149615ea044a5849f610c208bbe6f 68 | msgid "" 69 | "In order to use :class:`~backendpy.error.Error` response class, you must " 70 | "first define a list of errors for the application, including codes and " 71 | "their corresponding messages. The list of errors in an application is " 72 | "defined by an instance of the :class:`~backendpy.error.ErrorList` class, " 73 | "which itself contains instances of the " 74 | ":class:`~backendpy.error.ErrorCode` class." 75 | msgstr "" 76 | 77 | #: a47c76f496f84840a621521877300768 backendpy.error.ErrorList:1 of 78 | msgid "Container class to define list of the :class:`~backendpy.error.ErrorCode`." 79 | msgstr "" 80 | 81 | #: aa99ecf91afc482fbc92dd4faf48e814 backendpy.error.ErrorCode:1 of 82 | msgid "A class to define error response item." 83 | msgstr "" 84 | 85 | #: ../../predefined_errors.rst:25 501a0d17dcfa439d963377824d579bbe 86 | msgid "For example, inside a custom module:" 87 | msgstr "" 88 | 89 | #: ../../predefined_errors.rst:27 fbbeceda028f4df48aaff6a9f3ec8a08 90 | msgid "project/apps/hello/controllers/errors.py" 91 | msgstr "" 92 | 93 | #: ../../predefined_errors.rst:38 2e361c8623dd46eb87648f9f3536442d 94 | msgid "" 95 | "After defining the list of errors, we must add this list to the " 96 | "application. For this purpose, as mentioned before, inside the " 97 | "``main.py`` module of the application, we set the ``errors`` parameter " 98 | "with our own error list (instance of :class:`~backendpy.error.ErrorList` " 99 | "class)." 100 | msgstr "" 101 | 102 | #: ../../predefined_errors.rst:42 089c47e3150d4c5680afd47adc98a4da 103 | msgid "" 104 | "Also note that the errors parameter is a list type, and more than one " 105 | "ErrorList can be assigned to each app, each list being specific to a " 106 | "different part of the app." 107 | msgstr "" 108 | 109 | #: ../../predefined_errors.rst:45 fab6507fb0b94a18bb0511e08d3b7464 110 | msgid "project/apps/hello/main.py" 111 | msgstr "" 112 | 113 | #: ../../predefined_errors.rst:56 a47c76f496f84840a621521877300768 114 | msgid "" 115 | "And finally, examples of returning the :class:`~backendpy.error.Error` " 116 | "response:" 117 | msgstr "" 118 | 119 | #: ../../predefined_errors.rst:58 0a10df53b3cb40d4bc62f8f7ddbf9c99 120 | msgid "project/apps/hello/controllers/handlers.py" 121 | msgstr "" 122 | 123 | -------------------------------------------------------------------------------- /backendpy/db.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import importlib 5 | from collections.abc import Mapping 6 | 7 | from sqlalchemy import text 8 | from sqlalchemy.ext.asyncio import AsyncEngine 9 | from sqlalchemy.ext.asyncio import AsyncSession 10 | from sqlalchemy.ext.asyncio import AsyncAttrs 11 | from sqlalchemy.ext.asyncio import async_scoped_session 12 | from sqlalchemy.ext.asyncio import create_async_engine 13 | from sqlalchemy.ext.asyncio import async_sessionmaker 14 | from sqlalchemy.orm import DeclarativeBase 15 | 16 | from .app import App 17 | from .logging import get_logger 18 | 19 | LOGGER = get_logger(__name__) 20 | 21 | 22 | class Base(AsyncAttrs, DeclarativeBase): 23 | pass 24 | 25 | 26 | def set_database_hooks(app): 27 | """Attach Sqlalchemy engine and session to the project with hooks.""" 28 | 29 | @app.event('startup') 30 | async def on_startup(): 31 | app.context['db_engine'] = get_db_engine( 32 | config=app.config['database']) 33 | app.context['db_session'] = get_db_session( 34 | engine=app.context['db_engine'], 35 | scope_func=app.get_current_request) 36 | 37 | @app.event('shutdown') 38 | async def on_shutdown(): 39 | await app.context['db_engine'].dispose() 40 | 41 | @app.event('request_end') 42 | async def on_request_end(): 43 | await app.context['db_session'].remove() 44 | 45 | @app.event('success_response') 46 | async def on_request_success(): 47 | await app.context['db_session'].commit() 48 | 49 | @app.event('exception_response') 50 | async def on_request_exception(): 51 | await app.context['db_session'].rollback() 52 | 53 | 54 | def get_db_engine(config: Mapping) -> AsyncEngine: 55 | """Create a new Sqlalchemy async engine instance.""" 56 | 57 | return create_async_engine( 58 | 'postgresql+asyncpg://{username}:{password}@{host}:{port}/{name}'.format(**config), 59 | echo=config.get('echo') in (True, 'true', 'yes', 'on'), 60 | future=True, 61 | isolation_level=config.get('isolation_level', 'SERIALIZABLE')) 62 | 63 | 64 | def get_db_session(engine: AsyncEngine, scope_func: callable) -> async_scoped_session[AsyncSession]: 65 | """Construct a new Sqlalchemy async scoped session.""" 66 | 67 | async_session_factory = async_sessionmaker(engine, expire_on_commit=False) 68 | return async_scoped_session(async_session_factory, scopefunc=scope_func) 69 | 70 | 71 | def create_database(app_config: Mapping): 72 | """Create Backendpy project database and tables based on applications models.""" 73 | 74 | loop = asyncio.get_event_loop() 75 | loop.run_until_complete(_create_database(app_config)) 76 | loop.run_until_complete(_create_tables(app_config)) 77 | 78 | 79 | async def _create_database(app_config: Mapping): 80 | try: 81 | LOGGER.info('Start creating database …') 82 | engine = create_async_engine( 83 | 'postgresql+asyncpg://{username}:{password}@{host}:{port}'.format(**app_config['database']), 84 | echo=True, future=True, isolation_level='AUTOCOMMIT') 85 | async with engine.connect() as conn: 86 | await conn.execute(text('CREATE DATABASE {}'.format(app_config['database']['name']))) 87 | LOGGER.info('Database creation completed successfully!') 88 | await engine.dispose() 89 | except Exception as e: 90 | LOGGER.error(e) 91 | LOGGER.warning('Database creation excepted') 92 | 93 | 94 | async def _create_tables(app_config: Mapping): 95 | try: 96 | LOGGER.info('Start creating tables …') 97 | engine = create_async_engine( 98 | 'postgresql+asyncpg://{username}:{password}@{host}:{port}/{name}'.format(**app_config['database']), 99 | echo=True, future=True) 100 | async with engine.begin() as conn: 101 | _import_models(app_config) 102 | await conn.run_sync(Base.metadata.create_all) 103 | await engine.dispose() 104 | LOGGER.info('Tables creation completed successfully!') 105 | except Exception as e: 106 | LOGGER.error(e) 107 | 108 | 109 | def _import_models(app_config: Mapping): 110 | try: 111 | for package_name in app_config['apps']['active']: 112 | try: 113 | app = getattr(importlib.import_module(f'{package_name}.main'), 'app') 114 | if isinstance(app, App): 115 | for model_path in app.models: 116 | try: 117 | importlib.import_module(model_path) 118 | except ImportError: 119 | LOGGER.error(f'models "{model_path}" import error') 120 | else: 121 | LOGGER.error(f'app "{package_name}" instance error') 122 | except (ImportError, AttributeError): 123 | LOGGER.error(f'app "{package_name}" instance import error') 124 | except Exception as e: 125 | LOGGER.error(f'Failed to get app models: {e}') 126 | -------------------------------------------------------------------------------- /backendpy/utils/http.py: -------------------------------------------------------------------------------- 1 | import urllib.parse 2 | import http.client 3 | from typing import Optional 4 | 5 | from .json import to_json, from_json 6 | 7 | 8 | class Client: 9 | def __init__( 10 | self, 11 | base_url: str, 12 | port: Optional[int] = None, 13 | ssl: Optional[bool] = False, 14 | timeout: int = 10): 15 | self._base_url = base_url 16 | self._port = port 17 | self._ssl = ssl 18 | self._timeout = timeout 19 | self._session = None 20 | 21 | def __enter__(self): 22 | self._session = http.client.HTTPSConnection( 23 | host=self._base_url, port=self._port if self._port is not None else 443, timeout=self._timeout) \ 24 | if self._ssl else http.client.HTTPConnection( 25 | host=self._base_url, port=self._port if self._port is not None else 80, timeout=self._timeout) 26 | return self 27 | 28 | def __exit__(self, exc_type, exc_val, exc_tb): 29 | if self._session: 30 | self._session.close() 31 | 32 | def _prepare_data(self, form=None, json=None, body=None, headers=None): 33 | if not headers: 34 | headers = dict() 35 | if json is not None: 36 | data = to_json(json) 37 | headers.setdefault('content-type', 'application/json') 38 | headers.setdefault('accept', 'application/json') 39 | elif form is not None: 40 | data = urllib.parse.urlencode(form) 41 | headers.setdefault('content-type', 'application/x-www-form-urlencoded') 42 | headers.setdefault('accept', 'text/plain') 43 | elif body is not None: 44 | data = body 45 | else: 46 | data = '' 47 | return data, headers 48 | 49 | def get(self, 50 | url: str, 51 | headers: Optional[dict] = None): 52 | if self._session: 53 | self._session.request(method='GET', url=url, headers=headers if headers else dict()) 54 | return Response(self._session.getresponse()) 55 | 56 | def post(self, 57 | url: str, 58 | form: Optional[dict] = None, 59 | json: Optional[dict] = None, 60 | body: Optional[bytes | str] = None, 61 | headers: Optional[dict] = None): 62 | if self._session: 63 | body, headers = self._prepare_data(form, json, body, headers) 64 | self._session.request(method='POST', url=url, body=body, headers=headers) 65 | return Response(self._session.getresponse()) 66 | 67 | def put(self, 68 | url: str, 69 | form: Optional[dict] = None, 70 | json: Optional[dict] = None, 71 | body: Optional[bytes | str] = None, 72 | headers: Optional[dict] = None): 73 | if self._session: 74 | body, headers = self._prepare_data(form, json, body, headers) 75 | self._session.request(method='PUT', url=url, body=body, headers=headers) 76 | return Response(self._session.getresponse()) 77 | 78 | def patch(self, 79 | url: str, 80 | form: Optional[dict] = None, 81 | json: Optional[dict] = None, 82 | body: Optional[bytes | str] = None, 83 | headers: Optional[dict] = None): 84 | if self._session: 85 | body, headers = self._prepare_data(form, json, body, headers) 86 | self._session.request(method='PATCH', url=url, body=body, headers=headers) 87 | return Response(self._session.getresponse()) 88 | 89 | def delete(self, 90 | url: str, 91 | form: Optional[dict] = None, 92 | json: Optional[dict] = None, 93 | headers: Optional[dict] = None): 94 | if self._session: 95 | body, headers = self._prepare_data(form, json, headers=headers) 96 | self._session.request(method='DELETE', url=url, body=body, headers=headers) 97 | return Response(self._session.getresponse()) 98 | 99 | def options(self, 100 | url: str, 101 | headers: Optional[dict] = None): 102 | if self._session: 103 | self._session.request(method='OPTIONS', url=url, headers=headers if headers else dict()) 104 | return Response(self._session.getresponse()) 105 | 106 | def head(self, 107 | url: str, 108 | headers: Optional[dict] = None): 109 | if self._session: 110 | self._session.request(method='HEAD', url=url, headers=headers if headers else dict()) 111 | return Response(self._session.getresponse()) 112 | 113 | 114 | class Response: 115 | def __init__(self, r: http.client.HTTPResponse): 116 | self.headers = {k.lower(): v for k, v in r.getheaders()} 117 | self.status = r.status 118 | self.reason = r.reason 119 | self.message = r.msg 120 | self.version = r.version 121 | self.data = r.read() 122 | if self.headers.get('content-type') == 'application/json': 123 | self.data = from_json(self.data) 124 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/requests.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2023-01-04 04:59+0330\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "Language-Team: LANGUAGE \n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=utf-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Generated-By: Babel 2.9.1\n" 18 | 19 | #: ../../requests.rst:2 10d1019c151d4727a9ff5fc56b797d1c 20 | msgid "Requests" 21 | msgstr "‫درخواست‌ها‬" 22 | 23 | #: ../../requests.rst:3 9f5c966fcc69421b97d7d8060a413d06 24 | msgid "" 25 | "HTTP requests, after being received by the framework, become a " 26 | ":class:`~backendpy.request.Request` object and are sent to the handler " 27 | "functions as a parameter called ``request``." 28 | msgstr "" 29 | 30 | #: ../../requests.rst:6 c3ca449a19e84b1ba64986e61581d526 31 | msgid "project/apps/hello/handlers.py" 32 | msgstr "" 33 | 34 | #: ../../requests.rst:12 d72278dc50734ab49730ebcff01c97df 35 | msgid "Request object contains the following fields:" 36 | msgstr "" 37 | 38 | #: 5e4dd55d294f45618dd1086785972a19 backendpy.request.Request:1 of 39 | msgid "" 40 | "Base HTTP request class whose instances are used to store the information" 41 | " of a request and then these instances are sent to the requests handlers." 42 | msgstr "" 43 | 44 | #: 7e82c3c69fef4ef0b82c895245026d4d backendpy.request.Request of 45 | msgid "Variables" 46 | msgstr "" 47 | 48 | #: 1bda26296ca54f279c58fee787869a22 backendpy.request.Request:4 of 49 | msgid "" 50 | ":class:`~backendpy.Backendpy` class instance of the current project (that" 51 | " is an ASGI application). The information that is defined in the general " 52 | "scope of the project can be accessed through the app field of each " 53 | "request. For example ``request.app.config`` contains project config " 54 | "information. Also, if we want to put information in the App context, this" 55 | " information can be saved or read from ``request.app.context``. The data " 56 | "stored in the App context is valid until the service is stopped. For " 57 | "example, you can put a database connection in it to be used in the scope " 58 | "of all requests and until the service is turned off." 59 | msgstr "" 60 | 61 | #: backendpy.request.Request:11 dd28bf67b0db455ba840141ce4d60c23 of 62 | msgid "" 63 | "A dictionary of request context variables. Applications and middlewares " 64 | "can store their own data in the request context for other components to " 65 | "use until the end of the request. For example, auth middleware can set a " 66 | "user's information into request context after authentication process in " 67 | "the start of the request, so that other sections in the path of handling " 68 | "the request, can use the authenticated user information for their " 69 | "operations. The scope of the data stored in the request context is the " 70 | "request itself and until it responds." 71 | msgstr "" 72 | 73 | #: 8d23fdea5af149daaab147965ef2cf57 backendpy.request.Request:17 of 74 | msgid "Method of HTTP request" 75 | msgstr "" 76 | 77 | #: backendpy.request.Request:18 f40cd8e27e5149c686fecb10498b903a of 78 | msgid "URL path of HTTP request" 79 | msgstr "" 80 | 81 | #: 85af30d6967242809a8774dcad08735e backendpy.request.Request:19 of 82 | msgid "The root path this ASGI application is mounted at" 83 | msgstr "" 84 | 85 | #: backendpy.request.Request:20 cfc547fa679a4902831d1f43187e9ef9 of 86 | msgid "URL scheme of HTTP request" 87 | msgstr "" 88 | 89 | #: 864825731dbb4a029ad1cf97ebf42fa9 backendpy.request.Request:21 of 90 | msgid "A dictionary of server information (including host and port)" 91 | msgstr "" 92 | 93 | #: 3180b9a9ac3444aa8641eb519b9661dd backendpy.request.Request:22 of 94 | msgid "A dictionary of client information (including remote host and port)" 95 | msgstr "" 96 | 97 | #: 848d5f0a48394ffda6ac974b633d4a98 backendpy.request.Request:23 of 98 | msgid "A dictionary of HTTP request headers" 99 | msgstr "" 100 | 101 | #: 74ffc71bf1ce44bb992cc0062896b208 backendpy.request.Request:24 of 102 | msgid "A dictionary of URL path variables" 103 | msgstr "" 104 | 105 | #: 5c9d629f739c454bbcb3ff8ee0c2ff4d backendpy.request.Request:25 of 106 | msgid "A dictionary of HTTP request query string values" 107 | msgstr "" 108 | 109 | #: 964f691f53914828879abba22cb55a2a backendpy.request.Request:26 of 110 | msgid "A :class:`~backendpy.request.RequestBody` class instance" 111 | msgstr "" 112 | 113 | #~ msgid "A dictionary of HTTP request form data" 114 | #~ msgstr "" 115 | 116 | #~ msgid "A dictionary of HTTP request JSON data" 117 | #~ msgstr "" 118 | 119 | #~ msgid "A dictionary of multipart HTTP request files data" 120 | #~ msgstr "" 121 | 122 | #~ msgid "" 123 | #~ "Raw body of HTTP request if it " 124 | #~ "does not belong to any of the " 125 | #~ "\"form\", \"json\" and \"file\" fields" 126 | #~ msgstr "" 127 | 128 | #~ msgid "A dictionary of data processed by request data handler" 129 | #~ msgstr "" 130 | 131 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/responses.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-06-04 17:49+0430\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=utf-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: Babel 2.9.1\n" 17 | 18 | #: ../../responses.rst:2 654b87e637744d61b2c9f4a8da31875f 19 | msgid "Responses" 20 | msgstr "‫پاسخ‌ها‬" 21 | 22 | #: ../../responses.rst:3 499112bffc854da0b53ae8abfb6e3b62 23 | msgid "" 24 | "To respond to a request, we use instances of the " 25 | ":class:`~backendpy.response.Response` class or its subclasses inside the " 26 | "handler function. Default Backendpy responses include " 27 | ":class:`~backendpy.response.Text`, :class:`~backendpy.response.HTML`, " 28 | ":class:`~backendpy.response.JSON`, :class:`~backendpy.response.Binary`, " 29 | ":class:`~backendpy.response.File`, and " 30 | ":class:`~backendpy.response.Redirect`, but you can also create your own " 31 | "custom response types by extending the " 32 | ":class:`~backendpy.response.Response` class." 33 | msgstr "" 34 | 35 | #: ../../responses.rst:10 7969b5ff7c054d2e82347998d144380a 36 | msgid "The details of each of the default response classes are as follows:" 37 | msgstr "" 38 | 39 | #: 05200ca245f843bda0be754ce5654e9a backendpy.response.Response:1 of 40 | msgid "" 41 | "Base HTTP response class whose instances are returned as HTTP responses " 42 | "by requests handlers." 43 | msgstr "" 44 | 45 | #: 9e319f08e9284038af224923fe0c9de6 backendpy.response.Response of 46 | msgid "Variables" 47 | msgstr "" 48 | 49 | #: 23013564f7f0401eaf35b727ad668551 backendpy.response.Response:3 of 50 | msgid "The HTTP response body" 51 | msgstr "" 52 | 53 | #: 48af557fd6584807810efda0fe08c8ed backendpy.response.Response:4 of 54 | msgid "The HTTP response status" 55 | msgstr "" 56 | 57 | #: aab3eab7f034481d864b8c21a5c45cec backendpy.response.Response:5 of 58 | msgid "The HTTP response headers" 59 | msgstr "" 60 | 61 | #: 16d9a7dee23a41a6b5232f9a2217d2a0 backendpy.response.Response:6 of 62 | msgid "The HTTP response content type" 63 | msgstr "" 64 | 65 | #: 23469bdd5dc74d3fbdf00fbaeb53505f backendpy.response.Response:7 of 66 | msgid "Determines whether or not to compress (gzip) the response" 67 | msgstr "" 68 | 69 | #: 0376507f1b1d497fb5266b0e77075a7b backendpy.response.Text:1 of 70 | msgid "" 71 | "Text response class inherited from :class:`~backendpy.response.Response` " 72 | "class." 73 | msgstr "" 74 | 75 | #: ../../responses.rst:18 ../../responses.rst:36 ../../responses.rst:54 76 | #: ../../responses.rst:75 ../../responses.rst:100 77 | #: 74c127aaae464072a1b602aeeb8d4085 78 | msgid "Example usage:" 79 | msgstr "" 80 | 81 | #: ../../responses.rst:20 ../../responses.rst:38 ../../responses.rst:56 82 | #: ../../responses.rst:77 ../../responses.rst:102 83 | #: 2db7cb3992e349a9ad5113ff52d217f8 84 | msgid "project/apps/hello/handlers.py" 85 | msgstr "" 86 | 87 | #: 768e7b64071640d4bcbeb90285f532fb backendpy.response.JSON:1 of 88 | msgid "" 89 | "JSON response class inherited from :class:`~backendpy.response.Response` " 90 | "class." 91 | msgstr "" 92 | 93 | #: 2ec4fd40637f45a4ac82657fd1534d58 backendpy.response.HTML:1 of 94 | msgid "" 95 | "HTML response class inherited from :class:`~backendpy.response.Response` " 96 | "class." 97 | msgstr "" 98 | 99 | #: backendpy.response.Binary:1 bd806421db2340b29acc4ceda7dc3ca8 of 100 | msgid "" 101 | "Binary object response class inherited from " 102 | ":class:`~backendpy.response.Response` class." 103 | msgstr "" 104 | 105 | #: a4a8c3bdf7f8412199f734a61a6050de backendpy.response.File:1 of 106 | msgid "" 107 | "File response class inherited from :class:`~backendpy.response.Response` " 108 | "class which reads and returns file from the given path (which should be a" 109 | " path inside the project configured media path)" 110 | msgstr "" 111 | 112 | #: 8931ff96731840078085d787ca446084 backendpy.response.Redirect:1 of 113 | msgid "" 114 | "Redirect response class inherited from " 115 | ":class:`~backendpy.response.Response` class." 116 | msgstr "" 117 | 118 | #: ../../responses.rst:94 1308a81540b648898c75ea3d24617b32 119 | msgid "" 120 | "There is another type of response to quickly return a success response in" 121 | " predefined json format, which is as follows:" 122 | msgstr "" 123 | 124 | #: 9b3c86c4c2d647be94fb1499373bc4ed backendpy.response.Success:1 of 125 | msgid "" 126 | "JSON formatted success response class inherited from " 127 | ":class:`~backendpy.response.Response` class." 128 | msgstr "" 129 | 130 | #: ../../responses.rst:119 2eb2f895e86f4b099090ff996e9d6f2a 131 | msgid "" 132 | "The json format used in the :class:`~backendpy.response.Success` response" 133 | " is similar to the :class:`~backendpy.error.Error` response, and these " 134 | "two types of responses can be used together in a project. Refer to the " 135 | ":doc:`predefined_errors` section for information on how to use the " 136 | ":class:`~backendpy.error.Error` response." 137 | msgstr "" 138 | 139 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/configurations.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-08-27 04:04+0430\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=utf-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: Babel 2.9.1\n" 17 | 18 | #: ../../configurations.rst:2 4a2666acf6534489a553ef0370139a55 19 | msgid "Configurations" 20 | msgstr "‫تنظیمات‬" 21 | 22 | #: ../../configurations.rst:3 3a70eb8cda0a40289c1a146f6558a0c8 23 | msgid "" 24 | "In Backendpy projects, all the settings of a project are defined in the " 25 | "``config.ini`` file, which is located in the root path of each project " 26 | "and next to the main module of the project. This config file is defined " 27 | "in INI format, which includes sections and options. The basic list of " 28 | "framework configs and example of their definition is as follows:" 29 | msgstr "" 30 | "‫در پروژه‌های Backendpy تمامی تنظیمات پروژه، داخل فایل ``config.ini`` " 31 | "تعریف می‌شوند که در مسیر ریشه‌ی هر پروژه و در کنار ماژول اصلی پروژه قرار " 32 | "می‌گیرد. این فایل تنظیمات با فرمت INI تعریف می‌شود که شامل sectionها و " 33 | "optionها است. فهرست پایه‌ی تنظیمات چارچوب و مثالی از نحوه‌ی تعریف آن‌ها " 34 | "به شرح زیر است:‬" 35 | 36 | #: ../../configurations.rst:8 fea48306db1b4064b036878c37f9d37e 37 | msgid "project/config.ini" 38 | msgstr "" 39 | 40 | #: ../../configurations.rst:38 9601d8b07dd4447a87c4cd08be6d3c31 41 | msgid "" 42 | "In ini format, ``;`` is used for comments, ``[]`` is used to define " 43 | "sections, ``key = value`` is used to define values and the lines are used" 44 | " for list values." 45 | msgstr "" 46 | "‫در فرمت INI برای commentها از ``;``، برای تعریف بخش‌ها از ``[]``، برای " 47 | "تعریف مقادیر از ``key = value`` و برای مقادیر لیست هم از خطوط زیرهم " 48 | "استفاده می‌شود.‬" 49 | 50 | #: ../../configurations.rst:41 dfdb7330d72f47e3b8a352925f6c7778 51 | msgid "" 52 | "**networking** section contains values related to the server and the " 53 | "network." 54 | msgstr "‫بخش **networking** شامل مقادیر مرتبط با سرور و شبکه است.‬" 55 | 56 | #: ../../configurations.rst:43 84c5a929127b47e09ed2696a7622eb45 57 | msgid "" 58 | "**environment** section contains values such as the path to the media " 59 | "files and etc." 60 | msgstr "‫بخش **environment** شامل مقادیری از قبیل مسیر فایل‌های media و … است.‬" 61 | 62 | #: ../../configurations.rst:45 9e5c726b276640aa9f548de9440e576f 63 | msgid "**apps** section contains a list of the project active applications." 64 | msgstr "‫بخش **apps** شامل لیست اپلیکیشن‌های فعال پروژه است.‬" 65 | 66 | #: ../../configurations.rst:47 421e71ad9e5840038e1fb10c1ddf195e 67 | msgid "**middlewares** section contains a list of the project active middlewares." 68 | msgstr "‫بخش **middlewares** شامل لیست middlewareهای فعال پروژه است.‬" 69 | 70 | #: ../../configurations.rst:49 123ba88dbfe3461d87daf461f66deaf9 71 | msgid "" 72 | "**database** section, if using the default ORM, will include the settings" 73 | " related to it." 74 | msgstr "" 75 | "‫بخش **database** هم در صورت استفاده از ORM پیش‌فرض، شامل تنظیمات مرتبط " 76 | "با آن خواهد بود.‬" 77 | 78 | #: ../../configurations.rst:51 eba93d41041741d29a93540861dce056 79 | msgid "" 80 | "Also other custom settings may be required by any of the active apps, " 81 | "which must also be specified in this file. For example, an account " 82 | "application might have settings like this:" 83 | msgstr "" 84 | "‫همچنین ممکن است تنظیمات اختصاصی دیگری برای هر یک از اپلیکیشن‌های فعال " 85 | "موردنیاز باشد که آن‌ها نیز باید در این فایل مشخص شوند. برای مثال، یک " 86 | "اپلیکیشن حساب کاربری ممکن است تنظیماتی مانند زیر داشته باشد: ‬" 87 | 88 | #: ../../configurations.rst:62 0bf1d27aa0ee4bfd8779e9d29adea4e7 89 | msgid "" 90 | "To protect sensitive information in the config file, such as passwords, " 91 | "private keys, etc., be sure to restrict access to this file. For example," 92 | " set the permission to 600." 93 | msgstr "" 94 | "‫برای محافظت از اطلاعات حساس موجود در فایل تنظیمات مانند گذرواژه‌ها، " 95 | "کلیدهای خصوصی و غیره، حتما دسترسی به این فایل را محدود کنید. به عنوان " 96 | "مثال، مجوز آن را روی 600 قرار دهید.‬" 97 | 98 | #: ../../configurations.rst:66 f0c82097447d4a05a3ea39d4823909cd 99 | msgid "" 100 | "In order to access the project configs inside the code, you can use the " 101 | "``config`` attribute of the project :class:`~backendpy.Backendpy` class " 102 | "instance which contains this configs in dictionary format:" 103 | msgstr "" 104 | "‫به منظور دسترسی به تنظیمات پروژه در داخل کد، می‌توانبد از فیلد ``config`` " 105 | "نمونه‌ی کلاس :class:`~backendpy.Backendpy` پروژه استفاده کنید که حاوی این " 106 | "تنظیمات در قالب دیکشنری است:‬" 107 | 108 | 109 | #: ../../configurations.rst:69 757008becc3c47898ea7df92c612938b 110 | msgid "project/main.py" 111 | msgstr "" 112 | 113 | #: ../../configurations.rst:78 8f3e5cf2bb8e4738a765ddd9fca6b151 114 | msgid "And similarly inside the request handlers:" 115 | msgstr "‫و به‌طور مشابه در داخل توابع پردازش‌کننده‌ی درخواست‌ها:‬" 116 | 117 | #: ../../configurations.rst:80 5d6c209d1fa249f7a7e0aca4e8e7d378 118 | msgid "project/apps/hello/handlers.py" 119 | msgstr "" 120 | 121 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/introduction.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-02-17 17:59+0330\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=utf-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: Babel 2.9.1\n" 17 | 18 | #: ../../introduction.rst:2 ace9a8f898e64436b1162ccf71cce742 19 | msgid "Introduction" 20 | msgstr "مقدمه" 21 | 22 | #: ../../introduction.rst:3 f49dde0fb23844eaba9fe4183a489875 23 | msgid "" 24 | "**Backendpy** is an open-source framework for building the back-end of " 25 | "web projects with the Python programming language." 26 | msgstr "" 27 | "‫Backendpy یک چارچوب آزاد و متن‌باز برای ساخت پروژه‌های تحت وب با زبان " 28 | "برنامه‌نویسی پایتون است.‬" 29 | 30 | #: ../../introduction.rst:7 59d101b2e2474c44a16c55364719d6ad 31 | msgid "Why Backendpy?" 32 | msgstr "‫چرا Backendpy؟‬" 33 | 34 | #: ../../introduction.rst:8 453da917856f46fdb7e07870e1ca0ab3 35 | msgid "" 36 | "This framework does not deprive developers of their freedom by " 37 | "restricting them to pre-defined structures, nor does it leave some " 38 | "repetitive and time-consuming tasks to the developer." 39 | msgstr "" 40 | "‫این چارچوب نه آزادی توسعه‌دهندگان را با مقید کردن آن‌ها به ساختارهای " 41 | "ازپیش‌تعیین‌شده سلب می‌کند و نه برخی کارهای تکراری و زمان‌بر را بر عهده‌ی" 42 | " توسعه‌دهنده می‌گذارد.‬" 43 | 44 | #: ../../introduction.rst:11 c023df098136409a9e38ffc09e86d02a 45 | msgid "Some of the features of Backendpy are:" 46 | msgstr "‫برخی از ویژگی‌های Backendpy عبارت‌اند از:‬" 47 | 48 | #: ../../introduction.rst:13 9c5925088db04fe8b74d06538f33893f 49 | msgid "Asynchronous programming (ASGI-based projects)" 50 | msgstr "‫برنامه‌نویسی ناهمگام (پروژه‌های مبتنی بر ASGI)‬" 51 | 52 | #: ../../introduction.rst:14 d62b8033377d42248a040f410b8bb764 53 | msgid "" 54 | "Application-based architecture and the ability to install third-party " 55 | "applications in a project" 56 | msgstr "" 57 | "‫معماری اپلیکیشن‌محور و قابلیت نصب آسان اپلیکیشن‌های ثالث توسط ابزار pip " 58 | "و فعال‌سازی آن‌ها در پروژه‬" 59 | 60 | #: ../../introduction.rst:15 3a67c838df6a4d619429587f767fdb47 61 | msgid "" 62 | "Support of middlewares for different layers such as Application, Handler," 63 | " Request or Response" 64 | msgstr "" 65 | "‫پشتیبانی از انواع مختلف Middlewareها مختص لایه‌های Request، Handler، " 66 | "Application یا Response‬" 67 | 68 | #: ../../introduction.rst:16 390af1bc0a4b4721b5869313d499fd8e 69 | msgid "Supports events and hooks" 70 | msgstr "‫پشتیبانی از Eventها و Hookها‬" 71 | 72 | #: ../../introduction.rst:17 c9c03dd1d6a04c4ea8d8a3aad0bcbf74 73 | msgid "" 74 | "Data handler classes, including validators and filters to automatically " 75 | "apply to request input data" 76 | msgstr "" 77 | "‫کلاس‌های Data Handler‌، شامل اعتبارسنج‌ها و فیلترها برای اعمال خودکار " 78 | "روی داده‌های ورودی Requestها‬" 79 | 80 | #: ../../introduction.rst:18 aba02eebb6e747a69e61766ab9c993c4 81 | msgid "" 82 | "Supports a variety of responses including JSON, HTML, file and… with " 83 | "various settings such as stream, gzip and…" 84 | msgstr "" 85 | "‫پشتیبانی از انواع Responseها شامل JSON، HTML، File و … با تنظیمات مختلف " 86 | "از قبیل stream، gzip و …‬" 87 | 88 | #: ../../introduction.rst:19 0992a1ecaf8846e286f95508ea5e974d 89 | msgid "" 90 | "Router with the ability to define urls as Python decorator or as separate" 91 | " files" 92 | msgstr "" 93 | "‫Router با قابلیت تعریف Urlها به‌صورت Decorator پایتونی و یا به‌صورت " 94 | "فایل‌های مجزا (برحسب سلیقه‌ی توسعه‌دهنده)‬" 95 | 96 | #: ../../introduction.rst:20 a8e7ea39de7d470ca57e236b2e24a6ca 97 | msgid "Application-specific error codes" 98 | msgstr "‫کدهای خطای مختص هر اپلیکیشن‬" 99 | 100 | #: ../../introduction.rst:21 ebd561e05c4f42b1a77044645ff0df87 101 | msgid "" 102 | "Optional default database layer by the Sqlalchemy async ORM with " 103 | "management of sessions for the scope of each request" 104 | msgstr "" 105 | "‫لایه‌ی Database پیش‌فرض (اختیاری) Async توسط Sqlalchemy ORM و سیستم " 106 | "پایگاه‌داده‌ی Postgresql با مدیریت بهینه‌ی Sessionها برای محدوده‌ی هر " 107 | "Request‬" 108 | 109 | #: ../../introduction.rst:22 4727a4eaef1646a3a2e15614f4d69e1b 110 | msgid "Optional default templating layer by the Jinja template engine" 111 | msgstr "‫لایه‌ی Templaing پیش‌فرض (اختیاری) توسط Jinja template engine‬" 112 | 113 | #: ../../introduction.rst:23 b240fe2ff6254c2d859cb370988a70e7 114 | msgid "..." 115 | msgstr "…" 116 | 117 | #: ../../introduction.rst:26 377b56c8996c4e7e98fc8a1ab08c643d 118 | msgid "License" 119 | msgstr "‫پروانه‬" 120 | 121 | #: ../../introduction.rst:27 b72cd9a8988744b1ae6c521f9a1a10dd 122 | msgid "" 123 | "The Backendpy framework licensed under the BSD 3-Clause terms. The source" 124 | " code is available at https://github.com/savangco/backendpy." 125 | msgstr "" 126 | "‫چارچوب Backendpy به‌صورت متن‌باز و با مجوز آزاد BSD 3-Clause منتشر شده " 127 | "است. کد منبع این چارچوب در آدرس https://github.com/savangco/backendpy در " 128 | "دسترس قرار گرفته است.‬" 129 | 130 | #: ../../introduction.rst:32 30df139384df41e9b9db24ee4074aa6c 131 | msgid "This project is under active development." 132 | msgstr "‫این پروژه در حال توسعه است.‬" 133 | 134 | 135 | -------------------------------------------------------------------------------- /backendpy/data_handler/filters.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import base64 5 | import concurrent.futures.thread 6 | import datetime 7 | import decimal 8 | from collections.abc import Iterable, Sequence 9 | from functools import partial 10 | from html import escape, unescape 11 | from io import BytesIO 12 | from typing import Any, Optional 13 | 14 | try: 15 | from PIL import Image 16 | except ImportError: 17 | pass 18 | 19 | 20 | class Filter: 21 | """The base class that will be inherited to create the data filter classes.""" 22 | 23 | async def __call__(self, value: Any) -> Any: 24 | """ 25 | Perform data filtering operation. 26 | 27 | :param value: The data to which the filter should be applied 28 | :return: Filtered value 29 | """ 30 | return value 31 | 32 | 33 | class Escape(Filter): 34 | """Replace special characters "&", "<", ">", (') and (") to HTML-safe sequences.""" 35 | 36 | async def __call__(self, value: str): 37 | if value in (None, '', b''): 38 | return value 39 | if type(value) is not str: 40 | raise TypeError('Escape filter only supports string type.') 41 | return escape(unescape(value), quote=True) 42 | 43 | 44 | class Cut(Filter): 45 | """Cut the sequence to desired length.""" 46 | 47 | def __init__(self, length: int): 48 | self.length = length 49 | 50 | async def __call__(self, value: Sequence): 51 | if value in (None, '', b''): 52 | return value 53 | return value[:self.length] 54 | 55 | 56 | class DecodeBase64(Filter): 57 | """Decode the Base64 encoded bytes-like object or ASCII string.""" 58 | 59 | async def __call__(self, value: bytes | str): 60 | if value in (None, '', b''): 61 | return value 62 | return base64.b64decode(value, validate=True) 63 | 64 | 65 | class ParseDateTime(Filter): 66 | """Convert datetime string to datetime object.""" 67 | 68 | def __init__(self, format: str = '%Y-%m-%d %H:%M:%S'): 69 | self.format = format 70 | 71 | async def __call__(self, value: str): 72 | if value in (None, '', b''): 73 | return value 74 | return datetime.datetime.strptime(value, self.format) 75 | 76 | 77 | class ToStringObject(Filter): 78 | """Convert value to string object.""" 79 | 80 | async def __call__(self, value): 81 | if value in (None, '', b''): 82 | return value 83 | return str(value) 84 | 85 | 86 | class ToIntegerObject(Filter): 87 | """Convert value to integer object.""" 88 | 89 | async def __call__(self, value): 90 | if value in (None, '', b''): 91 | return value 92 | try: 93 | return int(value) 94 | except ValueError: 95 | if type(value) is str and '.' in value: 96 | return int(value.split('.')[0]) 97 | raise ValueError('The input value cannot be converted to int type') 98 | 99 | 100 | class ToFloatObject(Filter): 101 | """Convert value to float object.""" 102 | 103 | async def __call__(self, value): 104 | if value in (None, '', b''): 105 | return value 106 | return float(value) 107 | 108 | 109 | class ToDecimalObject(Filter): 110 | """Convert value to decimal object.""" 111 | 112 | async def __call__(self, value) -> decimal.Decimal: 113 | if value in (None, '', b''): 114 | return value 115 | return decimal.Decimal(str(value)) 116 | 117 | 118 | class ToBooleanObject(Filter): 119 | """Convert input values 0, 1, '0', '1', 'true' and 'false' to boolean value.""" 120 | 121 | async def __call__(self, value) -> bool: 122 | if value in (None, '', b''): 123 | return value 124 | if value in (True, 1, 'true', '1'): 125 | return True 126 | elif value in (False, 0, 'false', '0'): 127 | return False 128 | raise ValueError("Only input values 0, 1, '0', '1', 'true' and 'false' are acceptable.") 129 | 130 | 131 | class BlankToNull(Filter): 132 | """Convert blank value to null value.""" 133 | 134 | async def __call__(self, value): 135 | if value in ('', b''): 136 | return None 137 | return value 138 | 139 | 140 | class ModifyImage(Filter): 141 | """Modify the image.""" 142 | 143 | def __init__(self, format: str = 'JPEG', mode: str = 'RGB', 144 | max_size: Optional[Iterable[float, float]] = None): 145 | self.format = format 146 | self.mode = mode 147 | self.max_size = max_size 148 | 149 | async def __call__(self, value: bytes) -> bytes: 150 | if value in (None, '', b''): 151 | return value 152 | with concurrent.futures.ThreadPoolExecutor() as pool: 153 | return await asyncio.get_running_loop().run_in_executor( 154 | pool, partial(self._modify, value)) 155 | 156 | def _modify(self, value: bytes) -> bytes: 157 | # Todo: (read from / write to) buffer ? 158 | with BytesIO(value) as f_in: 159 | im = Image.open(f_in) 160 | if im.mode != self.mode: 161 | im = im.convert(self.mode) 162 | if self.max_size is not None: 163 | thumb_im = im.thumbnail(self.max_size, Image.Resampling.LANCZOS) 164 | if thumb_im is not None: 165 | im = thumb_im 166 | with BytesIO() as f_out: 167 | im.save(f_out, format=self.format) 168 | return f_out.getvalue() 169 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/hooks.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-06-04 17:23+0430\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=utf-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: Babel 2.9.1\n" 17 | 18 | #: ../../hooks.rst:2 0ebd909583dc47c096e66d9dfd3f62e2 19 | msgid "Hooks" 20 | msgstr "‫Hookها‬" 21 | 22 | #: ../../hooks.rst:3 c2399f2f600e4ab294719a17c04b5b5a 23 | msgid "" 24 | "Sometimes it is necessary to perform a specific operation following an " 25 | "event. For these types of needs, we can use Backendpy hooks feature. For " 26 | "example, when we want to write an email management application that sends" 27 | " an email after certain events in other applications, such as the " 28 | "registration or login of users." 29 | msgstr "" 30 | 31 | #: ../../hooks.rst:8 77f072dca8124610bd5bc3316e6b7ea0 32 | msgid "" 33 | "With this feature, we can both define new events with special labels " 34 | "within our application as points so that others can write their own code " 35 | "for these events to run, or we can assign codes to execute when " 36 | "triggering other events on the system." 37 | msgstr "" 38 | 39 | #: ../../hooks.rst:13 b6c87aa92e444df1a35962df8d006598 40 | msgid "Event Definition" 41 | msgstr "" 42 | 43 | #: ../../hooks.rst:14 e93f1503b4cb4d2d991b1309829f642d 44 | msgid "" 45 | "To define event points, we use the ``execute_event`` method of the " 46 | ":class:`~backendpy.Backendpy` class instance inside any space we have " 47 | "access to this instance. (For example, inside the handler of a request, " 48 | "we access the project :class:`~backendpy.Backendpy` instance via " 49 | "``request.app``)." 50 | msgstr "" 51 | 52 | #: ../../hooks.rst:18 bc6083fef5af45a482363221295612d0 53 | msgid "Example of defining user creation event:" 54 | msgstr "" 55 | 56 | #: ../../hooks.rst:28 376edb820fb14e358aba56462fb1a17e 57 | msgid "" 58 | "If the event also contains arguments, we send them in the second " 59 | "parameter in the form of a dictionary:" 60 | msgstr "" 61 | 62 | #: ../../hooks.rst:40 5aadb70fbc9744dabc09b97702ea6d27 63 | msgid "Default Events" 64 | msgstr "" 65 | 66 | #: ../../hooks.rst:41 018225c609b34934a825762eaab6b4f0 67 | msgid "" 68 | "In addition to the events that developers can add to the project, the " 69 | "default events are also provided in the framework as follows:" 70 | msgstr "" 71 | 72 | #: ../../hooks.rst:44 7e3d5b7d954c41639e5b7a5d104b3ea2 73 | msgid "Framework Default Events" 74 | msgstr "" 75 | 76 | #: ../../hooks.rst:48 45551a9f8c6e420481416120591fe391 77 | msgid "Label" 78 | msgstr "" 79 | 80 | #: ../../hooks.rst:49 279df7e2acad41b485524a1a763b08d3 81 | msgid "Description" 82 | msgstr "" 83 | 84 | #: ../../hooks.rst:50 b94bdffb30024e9c81d8677855432bd1 85 | msgid "``startup``" 86 | msgstr "" 87 | 88 | #: ../../hooks.rst:51 324a2bf03f1e4ada80476ef81d1469cd 89 | msgid "After successfully starting the ASGI server" 90 | msgstr "" 91 | 92 | #: ../../hooks.rst:52 fe2f9cb96e2b403494118955a294351a 93 | msgid "``shutdown``" 94 | msgstr "" 95 | 96 | #: ../../hooks.rst:53 de6e2ef4186447bdae4bf841da8932e2 97 | msgid "After the ASGI server shuts down" 98 | msgstr "" 99 | 100 | #: ../../hooks.rst:54 e3735759e3444d9a9966af92e68fc792 101 | msgid "``request_start``" 102 | msgstr "" 103 | 104 | #: ../../hooks.rst:55 7a4bcb0660e34fa9b78da3c7c7f0fa4d 105 | msgid "At the start of a request" 106 | msgstr "" 107 | 108 | #: ../../hooks.rst:56 4fbc0f33e6fc42efbda80982a27d93e3 109 | msgid "``request_end``" 110 | msgstr "" 111 | 112 | #: ../../hooks.rst:57 85fa96774c9649c683a445d359a2ee99 113 | msgid "After the response to a request is returned" 114 | msgstr "" 115 | 116 | #: ../../hooks.rst:61 298b4e64d4b840b6864fed5747bb3f25 117 | msgid "Hook Definition" 118 | msgstr "" 119 | 120 | #: ../../hooks.rst:62 dbe1fb67d819485aad3183ccae443a52 121 | msgid "" 122 | "To define the code that is executed in events, we use the " 123 | ":class:`~backendpy.hook.Hooks` class and its ``event`` decorator:" 124 | msgstr "" 125 | 126 | #: ../../hooks.rst:65 d533bdc7bc8b4c9199a81466d6537d67 127 | msgid "project/apps/hello/controllers/hooks.py" 128 | msgstr "" 129 | 130 | #: ../../hooks.rst:82 c2f2fab847794585820571696f381cc3 131 | msgid "" 132 | "As can be seen, if an argument is sent to a hook, these arguments are " 133 | "received in the parameters of the hook functions, otherwise they have no " 134 | "parameter." 135 | msgstr "" 136 | 137 | #: ../../hooks.rst:85 3fc6213bf57847ec80f88d35c3e61891 138 | msgid "" 139 | "Here we have written the hooks inside a custom module. To connect these " 140 | "hooks to the application, like the other components, we use the " 141 | "``main.py`` module of the application:" 142 | msgstr "" 143 | 144 | #: ../../hooks.rst:88 15eacd94209b4b0b89dd6961cdeb58be 145 | msgid "project/apps/hello/main.py" 146 | msgstr "" 147 | 148 | #: ../../hooks.rst:99 3ab74551ddac4d95b9fa069f3c1a4277 149 | msgid "" 150 | "Another way to use hooks is to attach them directly to a project (instead" 151 | " of an application), which can be used for special purposes such as " 152 | "managing database connections, which are part of the project-level " 153 | "settings:" 154 | msgstr "" 155 | 156 | #: ../../hooks.rst:102 37aae2fdf5fe4e7591779b859b44fc85 157 | msgid "project/main.py" 158 | msgstr "" 159 | 160 | -------------------------------------------------------------------------------- /backendpy/exception.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Iterable, Mapping, AsyncGenerator 4 | from typing import TYPE_CHECKING, Optional, Any 5 | 6 | from .response import Status, Response 7 | from .utils.json import to_json 8 | 9 | if TYPE_CHECKING: 10 | from .request import Request 11 | 12 | 13 | class ExceptionResponse(BaseException, Response): 14 | """ 15 | Base exception response class that its status code and other parameters must be set manually. 16 | Also, by expanding this class, you can create all kinds of error responses. 17 | 18 | :ivar body: The HTTP response body 19 | :ivar status: The HTTP response status 20 | :ivar headers: The HTTP response headers 21 | :ivar content_type: The HTTP response content type 22 | :ivar compress: Determines whether or not to compress (gzip) the response 23 | """ 24 | def __init__( 25 | self, 26 | body: Any, 27 | status: Status = Status.OK, 28 | headers: Optional[Iterable[[bytes, bytes]]] = None, 29 | content_type: bytes = b'text/plain', 30 | compress: bool = False) -> None: 31 | BaseException.__init__(self) 32 | """ 33 | Initialize instance. 34 | 35 | :param body: The HTTP response body 36 | :param status: The HTTP response status 37 | :param headers: The HTTP response headers 38 | :param content_type: The HTTP response content type 39 | :param compress: Determines whether or not to compress (gzip) the response 40 | """ 41 | Response.__init__( 42 | self, 43 | body=body, 44 | status=status, 45 | headers=headers, 46 | content_type=content_type, 47 | compress=compress) 48 | 49 | async def __call__(self, request: Request) \ 50 | -> tuple[bytes | AsyncGenerator[bytes], 51 | int, 52 | list[[bytes, bytes]], 53 | bool]: 54 | """ 55 | Generate and return response data when the Response object is called. 56 | 57 | :param request: :class:`~backendpy.request.Request` class instance 58 | :return: Tuple of generated response info 59 | """ 60 | return await super().__call__(request) 61 | 62 | 63 | class BadRequest(ExceptionResponse): 64 | """ 65 | Bad request error response class 66 | inherited from :class:`~backendpy.exception.ExceptionResponse` class. 67 | """ 68 | 69 | def __init__( 70 | self, 71 | body: Any = Status.BAD_REQUEST.description, 72 | content_type: Optional[bytes] = None) -> None: 73 | super().__init__( 74 | body=body if not isinstance(body, Mapping) else to_json(body), 75 | status=Status.BAD_REQUEST, 76 | content_type=content_type if content_type else 77 | (b'text/plain' if not isinstance(body, Mapping) else b'application/json')) 78 | 79 | 80 | class Unauthorized(ExceptionResponse): 81 | """ 82 | Unauthorized request error response class 83 | inherited from :class:`~backendpy.exception.ExceptionResponse` class. 84 | """ 85 | 86 | def __init__( 87 | self, 88 | body: Any = Status.UNAUTHORIZED.description, 89 | content_type: Optional[bytes] = None) -> None: 90 | super().__init__( 91 | body=body if not isinstance(body, Mapping) else to_json(body), 92 | status=Status.UNAUTHORIZED, 93 | content_type=content_type if content_type else 94 | (b'text/plain' if not isinstance(body, Mapping) else b'application/json')) 95 | 96 | 97 | class Forbidden(ExceptionResponse): 98 | """ 99 | Forbidden request error response class 100 | inherited from :class:`~backendpy.exception.ExceptionResponse` class. 101 | """ 102 | 103 | def __init__( 104 | self, 105 | body: Any = Status.FORBIDDEN.description, 106 | content_type: Optional[bytes] = None) -> None: 107 | super().__init__( 108 | body=body if not isinstance(body, Mapping) else to_json(body), 109 | status=Status.FORBIDDEN, 110 | content_type=content_type if content_type else 111 | (b'text/plain' if not isinstance(body, Mapping) else b'application/json')) 112 | 113 | 114 | class NotFound(ExceptionResponse): 115 | """ 116 | Resource not found error response class 117 | inherited from :class:`~backendpy.exception.ExceptionResponse` class. 118 | """ 119 | 120 | def __init__( 121 | self, 122 | body: Any = Status.NOT_FOUND.description, 123 | content_type: Optional[bytes] = None) -> None: 124 | super().__init__( 125 | body=body if not isinstance(body, Mapping) else to_json(body), 126 | status=Status.NOT_FOUND, 127 | content_type=content_type if content_type else 128 | (b'text/plain' if not isinstance(body, Mapping) else b'application/json')) 129 | 130 | 131 | class ServerError(ExceptionResponse): 132 | """ 133 | Server error response class 134 | inherited from :class:`~backendpy.exception.ExceptionResponse` class. 135 | """ 136 | 137 | def __init__( 138 | self, 139 | body: Any = Status.INTERNAL_SERVER_ERROR.description, 140 | content_type: Optional[bytes] = None) -> None: 141 | super().__init__( 142 | body=body if not isinstance(body, Mapping) else to_json(body), 143 | status=Status.INTERNAL_SERVER_ERROR, 144 | content_type=content_type if content_type else 145 | (b'text/plain' if not isinstance(body, Mapping) else b'application/json')) 146 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/installation.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2023-09-28 10:45+0330\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "Language-Team: LANGUAGE \n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=utf-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Generated-By: Babel 2.9.1\n" 18 | 19 | #: ../../installation.rst:2 06a90642170b4003bd499ed357ca2b4d 20 | msgid "Installation" 21 | msgstr "‫نصب‬" 22 | 23 | #: ../../installation.rst:5 1b2c32d8c38541e990562cb60d0c69e3 24 | msgid "Requirements" 25 | msgstr "‫نیازمندی‌ها‬" 26 | 27 | #: ../../installation.rst:6 022562a7d9f14b52b03e3e9f34b3829a 28 | msgid "Python 3.8+" 29 | msgstr "" 30 | 31 | #: ../../installation.rst:9 16db6a59d65145b6bdb8a49bf97a2435 32 | msgid "Using pip" 33 | msgstr "‫با pip‬" 34 | 35 | #: ../../installation.rst:10 28a3950212684033b20dd84b03431df0 36 | msgid "To install the Backendpy framework using pip:" 37 | msgstr "‫برای نصب چارچوب Backendpy توسط pip از دستور زیر استفاده می‌کنیم:‬" 38 | 39 | #: ../../installation.rst:16 7ecaf48295d04796b6484840952f484e 40 | msgid "" 41 | "If you also want to use the optional framework features (such as " 42 | "database, templating, etc.), you can use the following command to install" 43 | " the framework with additional dependencies:" 44 | msgstr "" 45 | "‫در صورتی که قصد استفاده از امکانات اختیاری چارچوب (مانند لایه‌های " 46 | "پیش‌فرض ORM، Templating و …) را نیز داشته باشید، می‌توانید از دستور زیر " 47 | "برای نصب چارچوب به‌همراه نصب کتابخانه‌های خارجی مرتبط با این بخش‌ها " 48 | "استفاده کنید:‬" 49 | 50 | #: ../../installation.rst:23 6e1f6bb4d4664e6f9352932dde606578 51 | msgid "" 52 | "If you only need one of these features, you can install the required " 53 | "dependencies separately. The list of these requirements is as follows:" 54 | msgstr "" 55 | "‫در صورت نیاز فقط به یکی از این ویژگی‌ها، می‌توانید بسته‌‌های اضافی " 56 | "موردنظر مرتبط را به‌طور مجزا با ابزار pip نصب کنید. فهرست این نیازمندی‌ها" 57 | " به شرح زیر است:‬" 58 | 59 | #: ../../installation.rst:26 f597debd24e44304ab7f252f190c9ad1 60 | msgid "Optional requirements" 61 | msgstr "‫نیازمندی‌های اختیاری‬" 62 | 63 | #: ../../installation.rst:30 3fe4c85f2910423fa1124733134343c0 64 | msgid "Name" 65 | msgstr "‫نام‬" 66 | 67 | #: ../../installation.rst:31 9122e79327da46a5891cb668e7a18b34 68 | msgid "Version" 69 | msgstr "نسخه" 70 | 71 | #: ../../installation.rst:32 23966b803c8e4441934411766751cb6b 72 | msgid "Usage" 73 | msgstr "‫مورد استفاده‬" 74 | 75 | #: ../../installation.rst:33 756c68e805fb43a480920039525e5803 76 | msgid "asyncpg" 77 | msgstr "" 78 | 79 | #: ../../installation.rst:34 c4359c1dff8a43b5ae7449adf52bdd7a 80 | msgid ">=0.25.0" 81 | msgstr "" 82 | 83 | #: ../../installation.rst:35 ../../installation.rst:38 84 | #: 439fd92cbc6f44fa978f484c713bb493 52556618e7574768b482a473d7737d47 85 | msgid "If using default ORM" 86 | msgstr "‫در صورت استفاده از لایه‌ی ORM پیش‌فرض‬" 87 | 88 | #: ../../installation.rst:36 8ec68aae8d974bb8a36848eeb38b7827 89 | msgid "sqlalchemy" 90 | msgstr "" 91 | 92 | #: ../../installation.rst:37 8ec68aae8d974bb8a36848eeb38b7827 93 | msgid ">=2.0.13" 94 | msgstr "" 95 | 96 | #: ../../installation.rst:39 dba7f6288cd54a9bb32bc83ee205010b 97 | msgid "jinja2" 98 | msgstr "" 99 | 100 | #: ../../installation.rst:40 f618fd695a7b4d269b0212d4bd3dcbd1 101 | msgid ">=3.0.0" 102 | msgstr "" 103 | 104 | #: ../../installation.rst:41 94eb801461864ce7b420eecc67625fdb 105 | msgid "If using default Templating" 106 | msgstr "‫در صورت استفاده از لایه‌ی Templating پیش‌فرض‬" 107 | 108 | #: ../../installation.rst:42 547617db072c4c209ce28d017a499f05 109 | msgid "pillow" 110 | msgstr "" 111 | 112 | #: ../../installation.rst:43 f3ea7672df56445db493f71ff9cf3cdf 113 | msgid ">=9.0.0" 114 | msgstr "" 115 | 116 | #: ../../installation.rst:44 7e91e760dc2945f29120725820c6b666 117 | msgid "If using the ModifyImage filter of the backendpy.data_handler.filters" 118 | msgstr "" 119 | "‫در صورت استفاده از فیلتر ModifyImage از ماژول " 120 | "backendpy.data_handler.filters‬" 121 | 122 | #: ../../installation.rst:45 ce09d2a404684cb8b26d0b9d809aebc8 123 | msgid "ujson" 124 | msgstr "" 125 | 126 | #: ../../installation.rst:46 e3c01e1b75674db7938654d3a594f24f 127 | msgid ">=5.1.0" 128 | msgstr "" 129 | 130 | #: ../../installation.rst:47 cafee142ccb84928b31f544a63932017 131 | msgid "" 132 | "If installed, ujson.loads will be used instead of json.loads, which is " 133 | "faster" 134 | msgstr "" 135 | "‫درصورت نصب‌بودن، به جای json.loads از ujson.loads استفاده خواهد شد که " 136 | "سرعت بیشتری دارد‬" 137 | 138 | #: ../../installation.rst:49 51d9d5d4afa140d9ab2bc2f1553ffd9d 139 | msgid "" 140 | "You also need to install an ASGI server such as Uvicorn, Hypercorn or " 141 | "Daphne:" 142 | msgstr "" 143 | "‫همچنین نیاز است که یک سرور ASGI دلخواه مانند Daphne، Hypercorn، Uvicorn " 144 | "را نصب کنیم:‬" 145 | 146 | #~ msgid "aiohttp" 147 | #~ msgstr "" 148 | 149 | #~ msgid ">=3.8.0" 150 | #~ msgstr "" 151 | 152 | #~ msgid "If using the AsyncHttpClient class of the backendpy.utils.http" 153 | #~ msgstr "‫در صورت استفاده از کلاس AsyncHttpClient از ماژول backendpy.utils.http‬" 154 | 155 | #~ msgid "requests" 156 | #~ msgstr "" 157 | 158 | #~ msgid ">=2.27.0" 159 | #~ msgstr "" 160 | 161 | #~ msgid "If using the HttpClient class of the backendpy.utils.http" 162 | #~ msgstr "‫در صورت استفاده از کلاس HttpClient از ماژول backendpy.utils.http‬" 163 | 164 | #~ msgid ">=1.4.27" 165 | #~ msgstr "" 166 | 167 | -------------------------------------------------------------------------------- /backendpy/error.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Mapping, Iterable, AsyncGenerator 4 | from typing import TYPE_CHECKING, Optional, Any 5 | 6 | from .exception import ExceptionResponse 7 | from .response import Status, JSON 8 | 9 | if TYPE_CHECKING: 10 | from .request import Request 11 | 12 | 13 | class Error(ExceptionResponse, JSON): 14 | """ 15 | Predefined error response class. 16 | 17 | :ivar code: Predefined error code number 18 | :ivar message_data: Values used for error message formatting 19 | :ivar data: Error related data with a structure supported by JSON format 20 | :ivar headers: The HTTP response headers 21 | :ivar compress: Determines whether or not to compress (gzip) the response 22 | """ 23 | 24 | def __init__( 25 | self, 26 | code: int, 27 | message_data: Optional[Mapping[str, Any] | 28 | Iterable[Any] | 29 | Any] = None, 30 | data: Optional[Any] = None, 31 | headers: Optional[Iterable[[bytes, bytes]]] = None, 32 | compress: bool = False) -> None: 33 | """ 34 | Initialize instance. 35 | 36 | :param code: Predefined error code number 37 | :param message_data: Values used for error message formatting 38 | :param data: Error related data with a structure supported by JSON format 39 | :param headers: The HTTP response headers 40 | :param compress: Determines whether or not to compress (gzip) the response 41 | """ 42 | JSON.__init__(self, body=None, headers=headers, compress=compress) 43 | self.code = code 44 | self.message_data = message_data 45 | self.data = data 46 | 47 | async def __call__(self, request: Request) \ 48 | -> tuple[bytes | AsyncGenerator[bytes], 49 | int, 50 | list[[bytes, bytes]], 51 | bool]: 52 | """ 53 | Return error data when the error object is raised. 54 | 55 | :param request: :class:`~backendpy.request.Request` class instance 56 | :return: Tuple of generated error response info 57 | """ 58 | error = request.app.errors[self.code] 59 | self.status = error.status 60 | self.body = error.as_dict() 61 | 62 | if self.message_data: 63 | if isinstance(self.message_data, Mapping): 64 | self.body['message'] = self.body['message'].format(**self.message_data) 65 | elif isinstance(self.message_data, Iterable): 66 | self.body['message'] = self.body['message'].format(*self.message_data) 67 | else: 68 | self.body['message'] = self.body['message'].format(self.message_data) 69 | 70 | if self.data is not None: 71 | self.body['data'] = self.data 72 | 73 | return await JSON.__call__(self, request) 74 | 75 | 76 | class ErrorCode: 77 | """A class to define error response item.""" 78 | 79 | def __init__(self, code: int, message: str, status: Status) -> None: 80 | """ 81 | Initialize instance. 82 | 83 | :param code: The error number 84 | :param message: The error message string 85 | :param status: HTTP response status 86 | """ 87 | self._code: int = code 88 | self._message: str = message 89 | self._status: Status = status 90 | 91 | @property 92 | def code(self) -> int: 93 | return self._code 94 | 95 | @property 96 | def message(self) -> str: 97 | return self._message 98 | 99 | @property 100 | def status(self) -> Status: 101 | return self._status 102 | 103 | def as_dict(self) -> dict: 104 | return {'status': 'error', 105 | 'code': self._code, 106 | 'message': self._message} 107 | 108 | 109 | class ErrorList: 110 | """Container class to define list of the :class:`~backendpy.error.ErrorCode`.""" 111 | 112 | def __init__(self, *codes: ErrorCode) -> None: 113 | """ 114 | Initialize ErrorList instance. 115 | 116 | :param codes: Instances of the :class:`~backendpy.error.ErrorCode` class as arguments 117 | """ 118 | self._items: dict[int, ErrorCode] = dict() 119 | self.extend(*codes) 120 | 121 | @property 122 | def items(self) -> dict[int, ErrorCode]: 123 | """Get items""" 124 | return self._items 125 | 126 | def extend(self, *codes: ErrorCode) -> None: 127 | """Extend items""" 128 | for i in codes: 129 | if not isinstance(i, ErrorCode): 130 | raise Exception('Invalid error code type') 131 | if i.code in self._items: 132 | raise Exception(f'Duplicate error code "{i.code}"') 133 | self._items[i.code] = i 134 | 135 | def merge(self, other: ErrorList) -> None: 136 | """Merge items from another ErrorList class instance with this instance.""" 137 | if not isinstance(other, self.__class__): 138 | raise TypeError(f"{type(self.__class__)} and {type(other)} cannot be merged.") 139 | for code, i in other.items.items(): 140 | if code in self._items: 141 | raise Exception(f'Duplicate error code "{code}"') 142 | self._items[code] = i 143 | 144 | def __getitem__(self, item) -> ErrorCode: 145 | return self._items[item] 146 | 147 | 148 | base_errors = ErrorList( 149 | ErrorCode(1000, "Server error", Status.INTERNAL_SERVER_ERROR), 150 | ErrorCode(1001, "Not found", Status.NOT_FOUND), 151 | ErrorCode(1002, "Unexpected data", Status.BAD_REQUEST), 152 | ErrorCode(1003, "Disallowed host", Status.BAD_REQUEST),) 153 | -------------------------------------------------------------------------------- /docs/middlewares.rst: -------------------------------------------------------------------------------- 1 | Middlewares 2 | =========== 3 | An ASGI application based on the Backendpy framework (instance of :class:`~backendpy.Backendpy` class) can be used 4 | with a variety of external ASGI middlewares. 5 | In addition to external middlewares, in the Backendpy framework itself, the ability to create middleware for the 6 | internal components and layers of the system is also available. 7 | These types of internal middlewares are discussed below: 8 | 9 | Types of middleware 10 | ------------------- 11 | The types of Backendpy internal middlewares depending on the layer they are processing are as follows: 12 | 13 | Backendpy instance middleware 14 | ............................. 15 | This type of middleware, like the external middleware mentioned earlier, processes an ASGI application (instance 16 | of :class:`~backendpy.Backendpy` class) and adds to or modifies its functionality. 17 | 18 | The difference between this type of middleware and external middleware is the easier way to create and attach it to 19 | the project, which instead of changing the code, we set it in the project config file. 20 | 21 | Request middleware 22 | .................. 23 | This middleware takes a :class:`~backendpy.request.Request` object before it reaches the handler layer and delivers a 24 | processed or modified Request object to the handler layer. 25 | 26 | Also, depending on the type of processing in this middleware, the middleware can prevent the request process from 27 | continuing and interrupt it with either raise an error response or returning a direct response in the second index 28 | of return tuple and prevent request from reaching the handler layer. 29 | 30 | Handler middleware 31 | .................. 32 | This middleware takes a request handler (which is an async function) before executing it and returns a processed or 33 | modified handler. As a result, this new handler will be used instead of the original handler to return the response 34 | to this request. 35 | 36 | In this middleware, it is also possible to interrupt the process and return the error response. 37 | 38 | Response middleware 39 | ................... 40 | This middleware captures the final :class:`~backendpy.response.Response` object before sending it to the client and 41 | returns a processed, modified, or replaced Response object. 42 | 43 | Creating middleware 44 | ------------------- 45 | To create a middleware, use :class:`~backendpy.middleware.middleware.Middleware` class and implement its methods. Each of these 46 | methods is specific to implementing different types of middleware mentioned in the previous section. 47 | 48 | How to define these methods is as follows: 49 | 50 | .. code-block:: python 51 | :caption: project/apps/hello/middlewares/example.py 52 | 53 | from backendpy.middleware import Middleware 54 | 55 | class MyMiddleware(Middleware): 56 | 57 | @staticmethod 58 | def process_application(application): 59 | ... 60 | return application 61 | 62 | @staticmethod 63 | async def process_request(request): 64 | ... 65 | return request, None 66 | 67 | @staticmethod 68 | async def process_handler(request, handler): 69 | ... 70 | return handler 71 | 72 | @staticmethod 73 | async def process_response(request, response): 74 | ... 75 | return response 76 | 77 | .. autoclass:: backendpy.middleware.middleware.Middleware 78 | :noindex: 79 | 80 | As can be seen, all methods are static and also except for ``process_application`` which is a simple function, all 81 | other methods (which are in the path of handling a request) must be defined as an ``async`` function. 82 | 83 | As an example of a request middleware, it can be used to authenticate the user before executing the request handler: 84 | 85 | .. code-block:: python 86 | :caption: project/apps/hello/middlewares/auth.py 87 | 88 | from backendpy.middleware import Middleware 89 | from backendpy.exception import BadRequest 90 | ... 91 | 92 | class AuthMiddleware(Middleware): 93 | 94 | @staticmethod 95 | async def process_request(request): 96 | auth_token = request.headers.get('authorization') 97 | 98 | if is_invalid_token(auth_token): 99 | raise BadRequest({'error': 'Invalid auth token'}) 100 | 101 | user = get_user(auth_token) 102 | 103 | request.context["auth"] = { 104 | 'user_id': user.id, 105 | 'user_roles': user.roles} 106 | 107 | return request, None 108 | 109 | In this example, after receiving a request, first the user identity is checked inside the middleware and if there 110 | is an error, the error response is returned and if successful, the user information is added to the request context 111 | and we can access this information inside the request handler. 112 | 113 | Activation of middleware 114 | ------------------------ 115 | In order to activate a middleware in a project, we need to define them in the project ``config.ini`` file as follows: 116 | 117 | .. code-block:: python 118 | :caption: project/config.ini 119 | 120 | ... 121 | [middlewares] 122 | active = 123 | project.apps.hello.middlewares.auth.AuthMiddleware 124 | 125 | The middlewares are independent classes and can be written as part of an application or as a standalone module. 126 | In both cases, to enable them, their class must be added to the project config. 127 | This means that by activating an application, its internal middlewares will not be enabled by default. 128 | 129 | .. note:: 130 | Note that in a project you can define an unlimited number of middlewares of one type or in different types. 131 | Middlewares of the same type will be queued and executed in the order in which they are defined, and the output of 132 | each middleware will be passed to the next middleware. 133 | 134 | -------------------------------------------------------------------------------- /backendpy/middleware/defaults/cors.py: -------------------------------------------------------------------------------- 1 | from ..middleware import Middleware 2 | from ...response import Status, Response 3 | 4 | 5 | class CORSMiddleware(Middleware): 6 | 7 | @staticmethod 8 | async def process_request(request): 9 | if request.method == 'OPTIONS': 10 | config = request.app.config.get('cors', {}) 11 | response_headers = dict() 12 | 13 | if 'origin' in request.headers: 14 | requested_origin = request.headers.get('origin') 15 | if requested_origin: 16 | allowed_origins = config.get('allowed_origins') 17 | if allowed_origins: 18 | if allowed_origins != '*' and type(allowed_origins) is str: 19 | allowed_origins = [allowed_origins] 20 | if allowed_origins == '*' or requested_origin.lower() in allowed_origins: 21 | response_headers['access-control-allow-origin'] = requested_origin 22 | 23 | requested_method = request.headers.get('access-control-request-method') 24 | if requested_method: 25 | allowed_methods = config.get('allowed_methods') 26 | if allowed_methods: 27 | if allowed_methods == '*': 28 | allowed_methods = ['GET', 'POST', 'PATCH', 'PUT', 'DELETE', 'OPTIONS', 'HEAD'] 29 | elif type(allowed_methods) is str: 30 | allowed_methods = [allowed_methods.upper()] 31 | else: 32 | allowed_methods = list(map(str.upper, allowed_methods)) 33 | if requested_method.upper() in allowed_methods: 34 | response_headers['access-control-allow-methods'] = ', '.join(allowed_methods) 35 | 36 | requested_headers = \ 37 | list(map(str.lower, request.headers.get('access-control-request-headers') 38 | .replace(' ', '').split(','))) 39 | if requested_headers: 40 | allowed_headers = config.get('allowed_headers') 41 | if allowed_headers: 42 | if allowed_headers != '*': 43 | if type(allowed_headers) is str: 44 | allowed_headers = [allowed_headers.lower()] 45 | else: 46 | allowed_headers = list(map(str.lower, allowed_headers)) 47 | allowed_requested_headers = requested_headers if allowed_headers == '*' else \ 48 | (h for h in requested_headers if h in allowed_headers) 49 | response_headers['access-control-allow-headers'] = \ 50 | ', '.join(allowed_requested_headers) 51 | 52 | if config.get('allow_credentials') == 'true' \ 53 | and ('authorization' in allowed_requested_headers 54 | or 'proxy-authorization' in allowed_requested_headers 55 | or 'cookie' in allowed_requested_headers 56 | or 'client-cert' in allowed_requested_headers 57 | or 'client-cert-chain' in allowed_requested_headers): 58 | response_headers['access-control-allow-credentials'] = 'true' 59 | 60 | if response_headers: 61 | response_headers['access-control-max-age'] = config.get('max-age', '86400') 62 | 63 | response_headers['vary'] = 'origin' 64 | 65 | response = Response( 66 | status=Status.NO_CONTENT, 67 | body=b'', 68 | headers=list(response_headers.items())) 69 | 70 | return request, response 71 | else: 72 | return request, None 73 | 74 | @staticmethod 75 | async def process_response(request, response): 76 | if request.method != 'OPTIONS': 77 | response_headers = dict(response.headers) if response.headers is not None else dict() 78 | if 'origin' in request.headers: 79 | requested_origin = request.headers.get('origin') 80 | if requested_origin: 81 | config = request.app.config.get('cors', {}) 82 | allowed_origins = config.get('allowed_origins') 83 | if allowed_origins: 84 | if allowed_origins != '*' and type(allowed_origins) is str: 85 | allowed_origins = [allowed_origins] 86 | if allowed_origins == '*' or requested_origin.lower() in allowed_origins: 87 | response_headers['access-control-allow-origin'] = requested_origin 88 | if config.get('allow_credentials') == 'true' \ 89 | and ('authorization' in request.headers 90 | or 'proxy-authorization' in request.headers 91 | or 'cookie' in request.headers 92 | or 'client-cert' in request.headers 93 | or 'client-cert-chain' in request.headers): 94 | response_headers['access-control-allow-credentials'] = 'true' 95 | vary = response_headers.get('vary') 96 | response_headers['vary'] = ', '.join(set(vary.replace(' ', '').split(',') + ['origin'])) \ 97 | if vary else 'origin' 98 | response.headers = list(response_headers.items()) 99 | return response 100 | -------------------------------------------------------------------------------- /docs/routes.rst: -------------------------------------------------------------------------------- 1 | Routes 2 | ====== 3 | Routes in an application are the accessible URLs defined for that application. 4 | The routes of an application are defined according to a specific format, and for each 5 | route, a handler function is assigned. When each request reaches the server, if the request 6 | url matches with a route, the request will be delivered to the handler of that route. 7 | 8 | Generally, there are two ways to define routes, one is to use Python decorators on top of 9 | handler functions, and the other is to use a separate section like urls.py file containing 10 | a list of all the routes and linking them to the handlers. For different developers and 11 | depending on the architecture of the application, each of these methods can take precedence 12 | over the other. 13 | One of the features of the Backendpy framework is the possibility of defining routes in both methods. 14 | 15 | Consider the following examples: 16 | 17 | Decorator based routes 18 | ---------------------- 19 | To define :class:`~backendpy.router.Route` we can use :func:`~backendpy.router.Routes.get`, 20 | :func:`~backendpy.router.Routes.post`, :func:`~backendpy.router.Routes.path`, 21 | :func:`~backendpy.router.Routes.put` and :func:`~backendpy.router.Routes.delete` decorators 22 | as follows: 23 | 24 | .. code-block:: python 25 | :caption: project/apps/hello/handlers.py 26 | 27 | from backendpy.router import Routes 28 | from backendpy.response import Text 29 | 30 | routes = Routes() 31 | 32 | @routes.get('/hello-world') 33 | async def hello_world(request): 34 | return Text('Hello World!') 35 | 36 | @routes.post('/login') 37 | async def login(request): 38 | ... 39 | 40 | Also, if we need to access a handler with different http methods, we can use 41 | :func:`~backendpy.router.Routes.route` decorator as follows: 42 | 43 | .. code-block:: python 44 | :caption: project/apps/hello/handlers.py 45 | 46 | from backendpy.router import Routes 47 | from backendpy.response import Text 48 | 49 | routes = Routes() 50 | 51 | @routes.route('/hello-world', ('GET', 'POST')) 52 | async def hello_world(request): 53 | return Text('Hello World!') 54 | 55 | Separate routes 56 | --------------- 57 | We can define the list of :class:`~backendpy.router.Route` separately from the handlers as follows: 58 | 59 | .. code-block:: python 60 | :caption: project/apps/hello/handlers.py 61 | 62 | from backendpy.response import Text 63 | 64 | async def hello_world(request): 65 | return Text('Hello World!') 66 | 67 | async def login(request): 68 | ... 69 | 70 | .. code-block:: python 71 | :caption: project/apps/hello/urls.py 72 | 73 | from backendpy.router import Routes, Route 74 | from .handlers import hello_world, login 75 | 76 | routes = Routes( 77 | Route('/hello-world', ('GET',), hello_world), 78 | Route('/login', ('POST',), login) 79 | ) 80 | 81 | As can be seen in the examples, in both cases, the :class:`~backendpy.router.Routes` object 82 | is defined, which is used to hold the list of :class:`~backendpy.router.Route`. 83 | 84 | The complete list of parameters of a :class:`~backendpy.router.Route` is as follows: 85 | 86 | .. autoclass:: backendpy.router.Route 87 | :noindex: 88 | 89 | Note that in ``@route`` decorator, which is defined on the handler function itself, the ``handler`` 90 | parameter does not exist. and in ``@get``,``@post`` and ... decorators, the ``method`` 91 | parameter also does not exist. 92 | 93 | After defining the routes, the Routes object can then be assigned to the application via 94 | the :class:`~backendpy.app.App` class ``routes``parameter in the ``main.py`` module of 95 | the application: 96 | 97 | .. code-block:: python 98 | :caption: project/apps/hello/main.py 99 | 100 | from backendpy.app import App 101 | from .handlers import routes 102 | 103 | app = App( 104 | routes=[routes]) 105 | 106 | In an application, more than one object of the Routes class can be defined. Each of which 107 | can be used to define the routes of separate parts of the application or even different 108 | versions of the API and the like. For example: 109 | 110 | .. code-block:: python 111 | :caption: project/apps/hello/main.py 112 | 113 | from backendpy.app import App 114 | from .handlers.v1 import routes as routes_v1 115 | from .handlers.v2 import routes as routes_v2 116 | 117 | app = App( 118 | routes=[routes_v1, routes_v2]) 119 | 120 | Url variables 121 | ------------- 122 | In order to get variable values from URL, they can be specified by ``<`` and ``>`` characters inside the route. 123 | 124 | .. code-block:: python 125 | :caption: project/apps/hello/handlers.py 126 | 127 | from backendpy.router import Routes 128 | 129 | routes = Routes() 130 | 131 | @routes.patch('/users/') 132 | async def user_modification(request): 133 | id = request.url_vars['id'] 134 | ... 135 | 136 | The default matchable data type of variables is string. 137 | You can also specify the type of value that can be matched in the URL with the ``:`` separator. 138 | 139 | .. code-block:: python 140 | :caption: project/apps/hello/handlers.py 141 | 142 | from backendpy.router import Routes 143 | 144 | routes = Routes() 145 | 146 | @routes.patch('/users/') 147 | async def user_modification(request): 148 | id = int(request.url_vars['id']) 149 | ... 150 | 151 | Allowed data types are ``str``, ``int``, ``float`` and ``uuid``. 152 | 153 | .. note:: 154 | Note that these data types determine the matchable data type in the URL, 155 | and not the data converter to related types in Python, and these data 156 | will be available with the Python string data type. 157 | In order to automatically convert types of received data, as well as 158 | to access various features of working with input data, refer to the 159 | :doc:`data_handlers` section. 160 | 161 | Priority 162 | -------- 163 | Pay attention that when defining the routes, if several routes overlap, the route 164 | that is defined in a non-variable and explicit way will be matched first. If the 165 | routes are the same in this respect, they will be prioritized according to the order 166 | of their definition in the code. 167 | For example, ``/users/posts`` and ``/users/1`` will take precedence over ``/users/``, 168 | even if they are defined in the code after that. 169 | -------------------------------------------------------------------------------- /docs/locale/fa/LC_MESSAGES/project_creation.po: -------------------------------------------------------------------------------- 1 | # Backendpy docs persian translations 2 | # Copyright (C) 2022, Savang Co. 3 | # This file is distributed under the same license as the Backendpy package. 4 | # Jalil Hamdollahi Oskouei , 2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: Backendpy \n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2022-06-04 17:23+0430\n" 11 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: Jalil Hamdollahi Oskouei \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=utf-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Generated-By: Babel 2.9.1\n" 17 | 18 | #: ../../project_creation.rst:2 eee3f15f60b942e9aad46df5dc95d588 19 | msgid "Create a project" 20 | msgstr "‫ایجاد پروژه‬" 21 | 22 | #: ../../project_creation.rst:5 3180592743cd4bb594e45677eb0e59f9 23 | msgid "Basic structure" 24 | msgstr "‫ساختار پایه‬" 25 | 26 | #: ../../project_creation.rst:6 0089a86833e9469f9b8d2c574989021d 27 | msgid "" 28 | "A Backendpy-based project does not have a mandatory, predetermined " 29 | "structure, and it is the programmer who decides how to structure his " 30 | "project according to his needs." 31 | msgstr "" 32 | "‫یک پروژه‌ی مبتنی بر Backendpy ساختار اجباری و از پیش‌تعیین‌شده‌ای ندارد " 33 | "و این برنامه‌نویس است که تصمیم می‌گیرد برحسب سلیقه و نیازهایش، پروژه‌ی " 34 | "خود را به چه نحوی ساختاربندی کند.‬" 35 | 36 | #: ../../project_creation.rst:9 2504dd1343d14ac9873e7645ef9fad4e 37 | msgid "" 38 | "The programmer only needs to create a Python module with a custom name " 39 | "(for example \"main.py\") and set the instance of " 40 | ":class:`~backendpy.Backendpy` class (which is an ASGI application) inside" 41 | " it." 42 | msgstr "" 43 | "‫برنامه‌نویس نیاز است یک ماژول پایتون با نام دلخواه (برای مثال main.py) " 44 | "ایجاد و نمونه‌ی کلاس :class:`~backendpy.Backendpy` (که یک اپلیکیشن ASGI است) را داخل آن " 45 | "مقداردهی کند.‬" 46 | 47 | #: ../../project_creation.rst:12 435079627d8540a095ddcd84d4cab49d 48 | msgid "project/main.py" 49 | msgstr "" 50 | 51 | #: ../../project_creation.rst:19 19a9185481bc4d90aae371aaf9b1aa9b 52 | msgid "" 53 | "Also for project settings, the ``config.ini`` file must be created in the" 54 | " same path next to the module. Check out the :doc:`configurations` " 55 | "section for more information." 56 | msgstr "" 57 | "‫همچنین برای تنظیمات پروژه، فایل ``config.ini`` می‌تواند در همان مسیر در کنار" 58 | " ماژول ایجاد شود. برای اطلاعات بیشتر بخش :doc:`configurations` را " 59 | "ببینید.‬" 60 | 61 | #: ../../project_creation.rst:23 7736feb7209f4b60ba1eaac33b456148 62 | msgid "Applications" 63 | msgstr "‫اپلیکیشن‌ها‬" 64 | 65 | #: ../../project_creation.rst:24 e0be77f8ec474bec84843c3ff8967e3b 66 | msgid "" 67 | "Backendpy projects are developed by components called Applications. It is" 68 | " also possible to connect third-party apps to the project." 69 | msgstr "" 70 | "‫پروژه‌‌های Backendpy توسط اجزایی به‌نام Applicationها توسعه" 71 | " داده می‌شوند. همچنین امکان اتصال اپ‌های ثالث به پروژه وجود دارد.‬" 72 | 73 | #: ../../project_creation.rst:27 6b3c0a5bc4d242018f095fd3bfa7829c 74 | msgid "" 75 | "To create an application, first create a package containing the " 76 | "``main.py`` module in the desired path within the project (or any other " 77 | "path that can be imported)." 78 | msgstr "" 79 | "‫برای ایجاد یک اپلیکیشن، ابتدا یک پکیج حاوی ماژول ``main.py`` در مسیری " 80 | "دلخواه در داخل پروژه (یا هر مسیر دیگری که قابلیت import داشته باشد) ایجاد" 81 | " کنید.‬" 82 | 83 | #: ../../project_creation.rst:30 8389fe3a2078430bb4b58fd8e8829337 84 | msgid "" 85 | "Then inside the main.py module of an application we need to set an " 86 | "instance of the :class:`~backendpy.app.App` class. All parts and settings" 87 | " of an application are assigned by the parameters of the App class." 88 | msgstr "" 89 | "‫سپس در ماژول main.py یک اپلیکیشن باید نمونه‌ای از کلاس :class:`~backendpy.app.App` را " 90 | "مقداردهی کنیم. تمام قسمت‌ها و تنظیمات یک اپلیکیشن توسط پارامترهای کلاس " 91 | "App تخصیص داده می‌شوند.‬" 92 | 93 | #: ../../project_creation.rst:33 915e0ae2953c454193cb529f99caf3bb 94 | msgid "" 95 | "For example, in the \"/apps\" path inside the project, we create a " 96 | "package called \"hello\" and main.py file as follows:" 97 | msgstr "" 98 | "‫به عنوان مثال در مسیر /apps در داخل پروژه، بسته‌ای به نام hello و فایل " 99 | "main.py را به صورت زیر ایجاد می‌کنیم:‬" 100 | 101 | #: ../../project_creation.rst:35 2c6997562ee744e98c448877445d6d4c 102 | msgid "project/apps/hello/main.py" 103 | msgstr "" 104 | 105 | #: ../../project_creation.rst:44 14e4627c7ccd407ca70c49b5432b693d 106 | msgid "project/apps/hello/handlers.py" 107 | msgstr "" 108 | 109 | #: ../../project_creation.rst:56 fdd2223143124a90865591c7e5fbf3dc 110 | msgid "" 111 | "As you can see, we have created another optional module called " 112 | "handlers.py and then introduced the routes defined in it to the App class" 113 | " instance. The complete list of App class parameters is described in " 114 | "section :doc:`application_structure`." 115 | msgstr "" 116 | "‫همانطور که مشاهده می کنید ماژول اختیاری دیگری به نام handlers.py ایجاد کرده‌ایم" 117 | " و سپس Routeهای تعریف شده در آن را به پارامتر ``routes`` کلاس App ارسال کرده‌ایم. لیست" 118 | " کامل پارامترهای کلاس App در بخش :doc:`application_structure` توضیح داده شده است.‬" 119 | 120 | #: ../../project_creation.rst:60 fdd2223143124a90865591c7e5fbf3dc 121 | msgid "" 122 | "Only the items that are introduced to the App class are important to the " 123 | "framework, and the internal structuring of the applications is completely" 124 | " optional." 125 | msgstr "" 126 | "‫فقط مواردی که به کلاس App معرفی می‌شوند برای چارچوب مهم‌اند و ساختار داخلی" 127 | " اپلیکیشن‌ها کاملا اختیاری است.‬" 128 | 129 | #: ../../project_creation.rst:63 18a7217263614aa88b88ce0a72dd58c3 130 | msgid "" 131 | "Our application is now ready and you just need to enable it in the " 132 | "project config.ini file as follows:" 133 | msgstr "" 134 | "‫اکنون اپلیکیشن ما آماده است و تنها نیاز است که آن را در فایل config.ini " 135 | "پروژه به‌صورت زیر فعال‌سازی کنیم:‬" 136 | 137 | #: ../../project_creation.rst:65 42b1ad072322451db023ae210db04006 138 | msgid "project/config.ini" 139 | msgstr "" 140 | 141 | #: ../../project_creation.rst:72 9d7f23677b374dcfbafc65aba47d341a 142 | msgid "To run the project, see the :doc:`run` section." 143 | msgstr "‫برای اجرای پروژه، بخش :doc:`run` را ببینید.‬" 144 | 145 | #: ../../project_creation.rst:74 b8abb6532a174cb0a1f7208493574f05 146 | msgid "Refer to the :doc:`apps` section to learn how to develop applications." 147 | msgstr "‫برای یادگیری نحوه‌ی توسعه‌ی اپلیکیشن‌ها به بخش :doc:`apps` مراجعه کنید.‬" 148 | 149 | #: ../../project_creation.rst:77 a40de8be422f4427943599da01065dda 150 | msgid "Command line" 151 | msgstr "‫خط‌فرمان‬" 152 | 153 | #: ../../project_creation.rst:78 b724f91bd87c4609ab9a5707d0ee6c22 154 | msgid "" 155 | "The ``backendpy`` command can also be used to create projects and apps. " 156 | "To do this, first enter the desired path and then use the following " 157 | "commands:" 158 | msgstr "" 159 | "‫دستور خط‌فرمان ``backendpy`` نیز می‌تواند برای ایجاد پروژه‌ها و " 160 | "اپلیکیشن‌ها استفاده شود. برای این کار ابتدا وارد مسیر موردنظر شده و سپس " 161 | "از دستورات زیر استفاده کنید:‬" 162 | 163 | #: ../../project_creation.rst:82 435079627d8540a095ddcd84d4cab49d 164 | msgid "Project creation" 165 | msgstr "‫ایجاد پروژه‬" 166 | 167 | #: ../../project_creation.rst:88 d32d10afec564f80ba23b8c1d8d0ed10 168 | msgid "To create a project with more complete sample components:" 169 | msgstr "‫برای ایجاد یک پروژه با اجزای نمونه‌ی کامل‌تر:‬" 170 | 171 | #: ../../project_creation.rst:95 435079627d8540a095ddcd84d4cab49d 172 | msgid "App creation" 173 | msgstr "‫ایجاد اپلیکیشن‬" 174 | -------------------------------------------------------------------------------- /docs/database.rst: -------------------------------------------------------------------------------- 1 | Database 2 | ======== 3 | In Backendpy, developers can use any database system of their choice, depending on the needs of the project, using any 4 | type of engine layer that supports async requests, so there is no mandatory structure for this. 5 | However, this framework will provide helpers to speed up work with various database systems. Helpers are currently 6 | available for Sqlalchemy. 7 | 8 | Use custom database 9 | ------------------- 10 | With the :doc:`hooks` feature described in the previous sections, you can easily configure your connections, sessions, 11 | and database system according to different system events. 12 | 13 | For example: 14 | 15 | .. code-block:: python 16 | :caption: project/main.py 17 | 18 | from backendpy import Backendpy 19 | ... 20 | 21 | bp = Backendpy() 22 | 23 | @bp.event('startup') 24 | async def on_startup(): 25 | bp.context['db_engine'] = get_db_engine(config=bp.config['database']) 26 | bp.context['db_session'] = get_db_session(engine=bp.context['db_engine'], scope_func=bp.get_current_request) 27 | 28 | @bp.event('shutdown') 29 | async def on_shutdown(): 30 | await bp.context['db_engine'].dispose() 31 | 32 | @bp.event('request_end') 33 | async def on_request_end(): 34 | await bp.context['db_session'].remove() 35 | 36 | And then we use these resources inside the project: 37 | 38 | .. code-block:: python 39 | 40 | @routes.get('/hello-world') 41 | async def hello(request): 42 | db_session = request.app.context['db_session']() 43 | ... 44 | 45 | In this example, we used the ``startup`` event to initialize the engine and connect to the database at the start of 46 | the service, the ``request_end`` event to remove the dedicated database session of each request at the end of it, and 47 | the ``shutdown`` event to close the database connection when the service shuts down. 48 | 49 | Depending on your architecture for managing database connections and sessions, you may want to make the scope of each 50 | database session dependent on anything like threads and so on. 51 | In this example, the database sessions are set based on the scope of each request, which means that when a request 52 | starts, a database session starts (if requested inside the handler by calling db_session) and closes at the end of the 53 | request. 54 | The Backendpy framework provides the ``get_current_request`` as a callable for specifying session scope, which can be 55 | set in your engine or ORM settings. 56 | 57 | Note that in the example above, the names of some functions such as get_db_engine, etc. are used, which have only the 58 | aspect of an example and must be implemented by the developer according to the database system used. 59 | For more information, you can refer to the specific engine, database or ORM guides you use. 60 | 61 | Use Sqlalchemy helper layer 62 | --------------------------- 63 | When using Sqlalchemy ORM, Backendpy provides default helpers for this package, which makes it easier to work with. 64 | 65 | .. note:: 66 | Async capability has been added from Sqlalchemy version 1.4.27, so lower versions are not compatible with 67 | Backendpy framework. 68 | Also, among the available engines, only those that support async are usable, such as ``asyncpg`` package, which 69 | can be used based on the ``Postgresql`` database system. 70 | 71 | To use Sqlalchemy in Backendpy projects, do the following: 72 | 73 | First, in order to set the database engine and session settings into the project, we use the helper function 74 | ``set_database_hooks()`` as follows: 75 | 76 | .. code-block:: python 77 | :caption: project/main.py 78 | 79 | from backendpy import Backendpy 80 | from backendpy.db import set_database_hooks 81 | 82 | bp = Backendpy() 83 | set_database_hooks(bp) 84 | 85 | In addition to setting up the engine and creating and deleting the database connection at the start and shutdown of 86 | the service, this function also sets database sessions for the scope of each request, which can be used by calling 87 | ``request.app.context['db_session']`` inside the request handler: 88 | 89 | .. code-block:: python 90 | 91 | @routes.get('/hello-world') 92 | async def hello(request): 93 | db_session = request.app.context['db_session']() 94 | ... 95 | 96 | The database settings should also be stored in the config.ini file as follows, and the framework will use these 97 | settings to connect to the database: 98 | 99 | .. code-block:: 100 | :caption: project/config.ini 101 | 102 | [database] 103 | host = localhost 104 | port = 5432 105 | name = your_db_name 106 | username = your_db_user 107 | password = your_db_password 108 | 109 | After setting up the project, here's how to use Sqlalchemy ORM in applications: 110 | 111 | To create models of an application, inside the desired module of the application, we use the :class:`~backendpy.db.Base` 112 | class as follows: 113 | 114 | .. code-block:: python 115 | :caption: project/apps/hello/db/models.py 116 | 117 | from sqlalchemy import Column, Integer, String 118 | from backendpy.db import Base 119 | 120 | class User(Base): 121 | __tablename__ = 'users' 122 | id = Column(Integer(), primary_key=True) 123 | first_name = Column(String(50)) 124 | last_name = Column(String(50)) 125 | 126 | If you use this Base class, it is possible to connect between models of different applications, and also the CLI 127 | commands of the framework related to the database can be used. 128 | 129 | After defining the data models, these models should also be introduced to the application (so that they can be imported 130 | when needed for the framework). For this purpose, according to the procedure of other sections, we will use ``main.py`` 131 | module of the application: 132 | 133 | .. code-block:: python 134 | :caption: project/apps/hello/main.py 135 | 136 | from backendpy.app import App 137 | 138 | app = App( 139 | ... 140 | models=['project.apps.hello.db.models'], 141 | ...) 142 | 143 | As shown in the example, to introduce the models, we set their module path as a string to the application ``models`` 144 | parameter. This parameter is of iterable type and several model modules can be assigned to it. 145 | These module paths must also be within valid Python path. In this example, it is inside the project path that has 146 | already been added to the Python path by default. 147 | 148 | We can now use database queries in any part of the application: 149 | 150 | .. code-block:: python 151 | :caption: project/apps/hello/db/queries.py 152 | 153 | from .models import User 154 | 155 | async def get_user(session, identifier): 156 | return await session.get(User, identifier) 157 | 158 | .. code-block:: python 159 | :caption: project/apps/hello/controllers/handlers.py 160 | 161 | ... 162 | from ..db import queries 163 | 164 | @routes.get('/users/', data_handler=UserFilterData) 165 | async def user_detail(request): 166 | data = await request.get_cleaned_data() 167 | db_session = request.app.context['db_session']() 168 | result = await queries.get_user(db_session, data['id']) 169 | return Success(to_dict(result)) 170 | 171 | Note that in the sample code above, some functions such as to_dict or UserFilterData, etc. are used, which have an 172 | example aspect and must be created by the developer. 173 | 174 | For more information about Sqlalchemy and how to use it, you can refer to its specific documentation. 175 | 176 | Create database and models with command line 177 | ............................................ 178 | If you use the default Sqlalchemy layer as described above, you can automatically create the database and all data 179 | models within the project after entering the project path in the command line and using the following command: 180 | 181 | .. code-block:: console 182 | 183 | $ backendpy create_db 184 | 185 | --------------------------------------------------------------------------------