├── .github └── workflows │ ├── deploy.yml │ └── tests.yml ├── .gitignore ├── .python-version ├── .readthedocs.yaml ├── CONTRIBUTORS.md ├── LICENSE ├── README.md ├── docker-compose.yml ├── docs ├── Makefile ├── conf.py ├── index.rst ├── make.bat ├── peewee_async │ ├── api.rst │ ├── connection.rst │ ├── examples.rst │ ├── installing.rst │ ├── quickstart.rst │ └── transaction.rst └── samples │ └── tornado_sample.py ├── examples ├── README.md ├── aiohttp_example.py └── requirements.txt ├── load-testing ├── README.md ├── app.py ├── load.yaml └── requirements.txt ├── peewee_async ├── __init__.py ├── aio_model.py ├── connection.py ├── databases.py ├── pool.py ├── result_wrappers.py ├── transactions.py └── utils.py ├── pyproject.toml └── tests ├── __init__.py ├── aio_model ├── __init__.py ├── test_deleting.py ├── test_inserting.py ├── test_selecting.py ├── test_shortcuts.py └── test_updating.py ├── conftest.py ├── db_config.py ├── models.py ├── test_common.py ├── test_database.py ├── test_transaction.py └── utils.py /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to PyPI 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-22.04 10 | env: 11 | POETRY_VIRTUALENVS_CREATE: "false" 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Install poetry 16 | run: | 17 | python -m pip install --upgrade pip 18 | pip install poetry 19 | 20 | - name: Build 21 | run: | 22 | poetry version ${{ github.ref_name }} 23 | poetry build 24 | 25 | - name: Publish 26 | run: | 27 | poetry config pypi-token.pypi ${{ secrets.PYPI_API_TOKEN }} 28 | poetry publish 29 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-22.04 13 | strategy: 14 | max-parallel: 4 15 | matrix: 16 | python-version: ['3.9', '3.10', '3.11', '3.12'] 17 | 18 | services: 19 | postgres: 20 | image: postgres 21 | env: 22 | POSTGRES_USER: postgres 23 | POSTGRES_PASSWORD: postgres 24 | POSTGRES_DB: postgres 25 | ports: 26 | - 5432:5432 27 | # needed because the postgres container does not provide a healthcheck 28 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 29 | mysql: 30 | image: mysql 31 | env: 32 | MYSQL_ROOT_PASSWORD: mysql 33 | MYSQL_DATABASE: mysql 34 | ports: 35 | - 3306:3306 36 | options: --health-cmd "mysqladmin ping -h 127.0.0.1 -u root --password=$MYSQL_ROOT_PASSWORD" --health-interval 10s --health-timeout 5s --health-retries 10 37 | 38 | steps: 39 | - uses: actions/checkout@v1 40 | - name: Set up Python ${{ matrix.python-version }} 41 | uses: actions/setup-python@v1 42 | with: 43 | python-version: ${{ matrix.python-version }} 44 | - name: Install dependencies 45 | run: | 46 | python -m pip install --upgrade pip 47 | pip install -e .[develop] 48 | - name: Typing check 49 | run: mypy . 50 | - name: Run tests 51 | run: pytest -s -v 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | .vscode 4 | .venv/ 5 | .env 6 | load-testing/logs 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | .installed.cfg 18 | env/ 19 | venv/ 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | *.egg-info/ 31 | *.egg* 32 | *.ini 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .coverage 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | 59 | # Sphinx documentation 60 | docs/_build/ 61 | 62 | # PyBuilder 63 | target/ 64 | 65 | # poetry 66 | poetry.lock 67 | 68 | # pytest 69 | .pytest_cache/ 70 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.9.19 2 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | build: 4 | os: "ubuntu-22.04" 5 | tools: 6 | python: "3.10" 7 | 8 | python: 9 | install: 10 | - method: pip 11 | path: . 12 | extra_requirements: 13 | - docs 14 | 15 | sphinx: 16 | configuration: docs/conf.py 17 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | * @rudyryk | Alexey Kinev 4 | * @kalombos | Nikolay Gorshkov 5 | * @akerlay | Kirill Mineev 6 | * @mrbox | Jakub Paczkowski 7 | * @CyberROFL | Ilnaz Nizametdinov 8 | * @insolite | Oleg 9 | * @smagafurov 10 | * @Koos85 11 | * @spumer 12 | 13 | Thanks to every contributor! Please ping [@rudyryk](https://github.com/rudyryk) if you believe you should be mentioned. 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014, Alexey Kinev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | peewee-async 2 | ============ 3 | 4 | Asynchronous interface for **[peewee](https://github.com/coleifer/peewee)** 5 | ORM powered by **[asyncio](https://docs.python.org/3/library/asyncio.html)**. 6 | 7 | [![CI workflow](https://github.com/05bit/peewee-async/actions/workflows/tests.yml/badge.svg)](https://github.com/05bit/peewee-async/actions/workflows/tests.yml) [![PyPi Version](https://img.shields.io/pypi/v/peewee-async.svg)](https://pypi.python.org/pypi/peewee-async) 8 | [![Documentation Status](https://readthedocs.org/projects/peewee-async-lib/badge/?version=latest)](https://peewee-async-lib.readthedocs.io/en/latest/?badge=latest) 9 | 10 | 11 | Overview 12 | -------- 13 | 14 | * Requires Python 3.9+ 15 | * Has support for PostgreSQL via [aiopg](https://github.com/aio-libs/aiopg) 16 | * Has support for MySQL via [aiomysql](https://github.com/aio-libs/aiomysql) 17 | * Asynchronous analogues of peewee sync methods with prefix aio_ 18 | * Drop-in replacement for sync code, sync will remain sync 19 | * Basic operations are supported 20 | * Transactions support is present 21 | 22 | The complete documentation: 23 | http://peewee-async-lib.readthedocs.io 24 | 25 | 26 | Install 27 | ------- 28 | 29 | Install with `pip` for PostgreSQL aiopg backend: 30 | 31 | ```bash 32 | pip install peewee-async[postgresql] 33 | ``` 34 | 35 | or for PostgreSQL psycopg3 backend: 36 | 37 | ```bash 38 | pip install peewee-async[psycopg] 39 | ``` 40 | 41 | or for MySQL: 42 | 43 | ```bash 44 | pip install peewee-async[mysql] 45 | ``` 46 | 47 | 48 | Quickstart 49 | ---------- 50 | 51 | Create 'test' PostgreSQL database for running this snippet: 52 | 53 | createdb -E utf-8 test 54 | 55 | ```python 56 | import asyncio 57 | import peewee 58 | import peewee_async 59 | 60 | # Nothing special, just define model and database: 61 | 62 | database = peewee_async.PooledPostgresqlDatabase( 63 | database='db_name', 64 | user='user', 65 | host='127.0.0.1', 66 | port='5432', 67 | password='password', 68 | ) 69 | 70 | class TestModel(peewee_async.AioModel): 71 | text = peewee.CharField() 72 | 73 | class Meta: 74 | database = database 75 | 76 | # Look, sync code is working! 77 | 78 | TestModel.create_table(True) 79 | TestModel.create(text="Yo, I can do it sync!") 80 | database.close() 81 | 82 | # No need for sync anymore! 83 | 84 | database.set_allow_sync(False) 85 | 86 | async def handler(): 87 | await TestModel.aio_create(text="Not bad. Watch this, I'm async!") 88 | all_objects = await TestModel.select().aio_execute() 89 | for obj in all_objects: 90 | print(obj.text) 91 | 92 | loop = asyncio.get_event_loop() 93 | loop.run_until_complete(handler()) 94 | loop.close() 95 | 96 | # Clean up, can do it sync again: 97 | with database.allow_sync(): 98 | TestModel.drop_table(True) 99 | 100 | # Expected output: 101 | # Yo, I can do it sync! 102 | # Not bad. Watch this, I'm async! 103 | ``` 104 | 105 | 106 | More examples 107 | ------------- 108 | 109 | Check the .`/examples` directory for more. 110 | 111 | 112 | Documentation 113 | ------------- 114 | 115 | http://peewee-async-lib.readthedocs.io 116 | 117 | http://peewee-async.readthedocs.io - **DEPRECATED** 118 | 119 | 120 | Developing 121 | ---------- 122 | 123 | Install dependencies using pip: 124 | 125 | ```bash 126 | pip install -e .[develop] 127 | ``` 128 | 129 | Or using [poetry](https://python-poetry.org/docs/): 130 | 131 | ```bash 132 | poetry install -E develop 133 | ``` 134 | 135 | Run databases: 136 | 137 | ```bash 138 | docker-compose up -d 139 | ``` 140 | 141 | Run tests: 142 | 143 | ```bash 144 | pytest tests -v -s 145 | ``` 146 | 147 | 148 | Discuss 149 | ------- 150 | 151 | You are welcome to add discussion topics or bug reports to tracker on GitHub: https://github.com/05bit/peewee-async/issues 152 | 153 | License 154 | ------- 155 | 156 | Copyright (c) 2014, Alexey Kinev 157 | 158 | Licensed under The MIT License (MIT), 159 | see LICENSE file for more details. 160 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: postgres 4 | ports: 5 | - ${POSTGRES_PORT:-5432}:5432 6 | command: postgres -c log_statement=all 7 | environment: 8 | - POSTGRES_PASSWORD=postgres 9 | - POSTGRES_USER=postgres 10 | - POSTGRES_DB=postgres 11 | 12 | mysql: 13 | image: mysql 14 | ports: 15 | - ${MYSQL_PORT:-3306}:3306 16 | environment: 17 | - MYSQL_ROOT_PASSWORD=mysql 18 | - MYSQL_DATABASE=mysql 19 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/peewee-async.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/peewee-async.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/peewee-async" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/peewee-async" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # peewee-async documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Oct 11 20:12:24 2014. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import os 17 | import sys 18 | 19 | import peewee_async 20 | 21 | docs_dir = os.path.dirname(__file__) 22 | root_dir = os.path.realpath(os.path.join(docs_dir, '..')) 23 | sys.path.insert(0, root_dir) 24 | 25 | 26 | # If extensions (or modules to document with autodoc) are in another directory, 27 | # add these directories to sys.path here. If the directory is relative to the 28 | # documentation root, use os.path.abspath to make it absolute, like shown here. 29 | #sys.path.insert(0, os.path.abspath('.')) 30 | 31 | # -- General configuration ------------------------------------------------ 32 | 33 | # If your documentation needs a minimal Sphinx version, state it here. 34 | #needs_sphinx = '1.0' 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be 37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 38 | # ones. 39 | extensions = [ 40 | 'sphinx.ext.autodoc', 41 | 'sphinx.ext.intersphinx', 42 | 'sphinx.ext.todo', 43 | ] 44 | 45 | # Enable ToDo lists 46 | todo_include_todos = True 47 | 48 | # Add any paths that contain templates here, relative to this directory. 49 | templates_path = ['_templates'] 50 | 51 | # The suffix of source filenames. 52 | source_suffix = '.rst' 53 | 54 | # The encoding of source files. 55 | #source_encoding = 'utf-8-sig' 56 | 57 | # The master toctree document. 58 | master_doc = 'index' 59 | 60 | # General information about the project. 61 | project = 'peewee-async' 62 | copyright = '2014, Alexey Kinev' 63 | 64 | # The version info for the project you're documenting, acts as replacement for 65 | # |version| and |release|, also used in various other places throughout the 66 | # built documents. 67 | # 68 | # The short X.Y version. 69 | version = peewee_async.__version__ 70 | # The full version, including alpha/beta/rc tags. 71 | release = peewee_async.__version__ 72 | 73 | # The language for content autogenerated by Sphinx. Refer to documentation 74 | # for a list of supported languages. 75 | #language = None 76 | 77 | # There are two options for replacing |today|: either, you set today to some 78 | # non-false value, then it is used: 79 | #today = '' 80 | # Else, today_fmt is used as the format for a strftime call. 81 | #today_fmt = '%B %d, %Y' 82 | 83 | # List of patterns, relative to source directory, that match files and 84 | # directories to ignore when looking for source files. 85 | exclude_patterns = ['_build'] 86 | 87 | # The reST default role (used for this markup: `text`) to use for all 88 | # documents. 89 | #default_role = None 90 | 91 | # If true, '()' will be appended to :func: etc. cross-reference text. 92 | #add_function_parentheses = True 93 | 94 | # If true, the current module name will be prepended to all description 95 | # unit titles (such as .. function::). 96 | #add_module_names = True 97 | 98 | # If true, sectionauthor and moduleauthor directives will be shown in the 99 | # output. They are ignored by default. 100 | #show_authors = False 101 | 102 | # The name of the Pygments (syntax highlighting) style to use. 103 | pygments_style = 'sphinx' 104 | 105 | # A list of ignored prefixes for module index sorting. 106 | #modindex_common_prefix = [] 107 | 108 | # If true, keep warnings as "system message" paragraphs in the built documents. 109 | #keep_warnings = False 110 | 111 | 112 | # -- Options for HTML output ---------------------------------------------- 113 | 114 | # The theme to use for HTML and HTML Help pages. See the documentation for 115 | # a list of builtin themes. 116 | html_theme = 'sphinx_rtd_theme' 117 | 118 | # Theme options are theme-specific and customize the look and feel of a theme 119 | # further. For a list of options available for each theme, see the 120 | # documentation. 121 | #html_theme_options = {} 122 | 123 | # Add any paths that contain custom themes here, relative to this directory. 124 | #html_theme_path = [] 125 | 126 | # The name for this set of Sphinx documents. If None, it defaults to 127 | # " v documentation". 128 | #html_title = None 129 | 130 | # A shorter title for the navigation bar. Default is the same as html_title. 131 | #html_short_title = None 132 | 133 | # The name of an image file (relative to this directory) to place at the top 134 | # of the sidebar. 135 | #html_logo = None 136 | 137 | # The name of an image file (within the static path) to use as favicon of the 138 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 139 | # pixels large. 140 | #html_favicon = None 141 | 142 | 143 | # Add any extra paths that contain custom files (such as robots.txt or 144 | # .htaccess) here, relative to this directory. These files are copied 145 | # directly to the root of the documentation. 146 | #html_extra_path = [] 147 | 148 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 149 | # using the given strftime format. 150 | #html_last_updated_fmt = '%b %d, %Y' 151 | 152 | # If true, SmartyPants will be used to convert quotes and dashes to 153 | # typographically correct entities. 154 | #html_use_smartypants = True 155 | 156 | # Custom sidebar templates, maps document names to template names. 157 | #html_sidebars = {} 158 | 159 | # Additional templates that should be rendered to pages, maps page names to 160 | # template names. 161 | #html_additional_pages = {} 162 | 163 | # If false, no module index is generated. 164 | #html_domain_indices = True 165 | 166 | # If false, no index is generated. 167 | #html_use_index = True 168 | 169 | # If true, the index is split into individual pages for each letter. 170 | #html_split_index = False 171 | 172 | # If true, links to the reST sources are added to the pages. 173 | #html_show_sourcelink = True 174 | 175 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 176 | #html_show_sphinx = True 177 | 178 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 179 | #html_show_copyright = True 180 | 181 | # If true, an OpenSearch description file will be output, and all pages will 182 | # contain a tag referring to it. The value of this option must be the 183 | # base URL from which the finished HTML is served. 184 | #html_use_opensearch = '' 185 | 186 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 187 | #html_file_suffix = None 188 | 189 | # Output file base name for HTML help builder. 190 | htmlhelp_basename = 'peewee_async_doc' 191 | 192 | 193 | # -- Options for LaTeX output --------------------------------------------- 194 | 195 | latex_elements = { 196 | # The paper size ('letterpaper' or 'a4paper'). 197 | #'papersize': 'letterpaper', 198 | 199 | # The font size ('10pt', '11pt' or '12pt'). 200 | #'pointsize': '10pt', 201 | 202 | # Additional stuff for the LaTeX preamble. 203 | #'preamble': '', 204 | } 205 | 206 | # Grouping the document tree into LaTeX files. List of tuples 207 | # (source start file, target name, title, 208 | # author, documentclass [howto, manual, or own class]). 209 | latex_documents = [ 210 | ('index', 'peewee-async.tex', 'peewee-async Documentation', 211 | 'Alexey Kinev', 'manual'), 212 | ] 213 | 214 | # The name of an image file (relative to this directory) to place at the top of 215 | # the title page. 216 | #latex_logo = None 217 | 218 | # For "manual" documents, if this is true, then toplevel headings are parts, 219 | # not chapters. 220 | #latex_use_parts = False 221 | 222 | # If true, show page references after internal links. 223 | #latex_show_pagerefs = False 224 | 225 | # If true, show URL addresses after external links. 226 | #latex_show_urls = False 227 | 228 | # Documents to append as an appendix to all manuals. 229 | #latex_appendices = [] 230 | 231 | # If false, no module index is generated. 232 | #latex_domain_indices = True 233 | 234 | 235 | # -- Options for manual page output --------------------------------------- 236 | 237 | # One entry per manual page. List of tuples 238 | # (source start file, name, description, authors, manual section). 239 | man_pages = [ 240 | ('index', 'peewee-async', 'peewee-async Documentation', 241 | ['Alexey Kinev'], 1) 242 | ] 243 | 244 | # If true, show URL addresses after external links. 245 | #man_show_urls = False 246 | 247 | 248 | # -- Options for Texinfo output ------------------------------------------- 249 | 250 | # Grouping the document tree into Texinfo files. List of tuples 251 | # (source start file, target name, title, author, 252 | # dir menu entry, description, category) 253 | texinfo_documents = [ 254 | ('index', 'peewee-async', 'peewee-async Documentation', 255 | 'Alexey Kinev', 'peewee-async', 256 | 'Asynchronous interface for peewee ORM powered by asyncio.', 257 | 'Miscellaneous'), 258 | ] 259 | 260 | # Documents to append as an appendix to all manuals. 261 | #texinfo_appendices = [] 262 | 263 | # If false, no module index is generated. 264 | #texinfo_domain_indices = True 265 | 266 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 267 | #texinfo_show_urls = 'footnote' 268 | 269 | # If true, do not generate a @detailmenu in the "Top" node's menu. 270 | #texinfo_no_detailmenu = False 271 | 272 | 273 | intersphinx_mapping = { 274 | 'python': ('https://docs.python.org/3/', None), 275 | 'sphinx': ('https://www.sphinx-doc.org/en/master/', None), 276 | } 277 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. peewee-async documentation master file, created by 2 | sphinx-quickstart on Sat Oct 11 20:12:24 2014. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | peewee-async 7 | ============ 8 | 9 | **peewee-async** is a library providing asynchronous interface powered by `asyncio`_ for `peewee`_ ORM. 10 | 11 | .. _peewee: https://github.com/coleifer/peewee 12 | .. _asyncio: https://docs.python.org/3/library/asyncio.html 13 | 14 | 15 | * Works on Python 3.9+ 16 | * Has support for PostgreSQL via `aiopg` 17 | * Has support for MySQL via `aiomysql` 18 | * Asynchronous analogues of peewee sync methods with prefix **aio_** 19 | * Drop-in replacement for sync code, sync will remain sync 20 | * Basic operations are supported 21 | * Transactions support is present 22 | 23 | The source code is hosted on `GitHub`_. 24 | 25 | .. _GitHub: https://github.com/05bit/peewee-async 26 | 27 | 28 | Contents 29 | -------- 30 | 31 | .. toctree:: 32 | :maxdepth: 2 33 | 34 | peewee_async/installing 35 | peewee_async/quickstart 36 | peewee_async/api 37 | peewee_async/connection 38 | peewee_async/transaction 39 | peewee_async/examples 40 | 41 | Indices and tables 42 | ================== 43 | 44 | * :ref:`genindex` 45 | * :ref:`modindex` 46 | * :ref:`search` 47 | 48 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\peewee-async.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\peewee-async.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/peewee_async/api.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ==================== 3 | 4 | 5 | Databases 6 | ++++++++++ 7 | 8 | .. autoclass:: peewee_async.databases.AioDatabase 9 | 10 | .. automethod:: peewee_async.databases.AioDatabase.aio_connect 11 | 12 | .. autoproperty:: peewee_async.databases.AioDatabase.is_connected 13 | 14 | .. automethod:: peewee_async.databases.AioDatabase.aio_close 15 | 16 | .. automethod:: peewee_async.databases.AioDatabase.aio_execute 17 | 18 | .. automethod:: peewee_async.databases.AioDatabase.set_allow_sync 19 | 20 | .. automethod:: peewee_async.databases.AioDatabase.allow_sync 21 | 22 | .. automethod:: peewee_async.databases.AioDatabase.aio_atomic 23 | 24 | .. autoclass:: peewee_async.PsycopgDatabase 25 | :members: init 26 | 27 | .. autoclass:: peewee_async.PooledPostgresqlDatabase 28 | :members: init 29 | 30 | .. autoclass:: peewee_async.PooledPostgresqlExtDatabase 31 | :members: init 32 | 33 | .. autoclass:: peewee_async.PooledMySQLDatabase 34 | :members: init 35 | 36 | AioModel 37 | ++++++++++ 38 | 39 | .. autoclass:: peewee_async.AioModel 40 | 41 | .. automethod:: peewee_async.AioModel.aio_get 42 | 43 | .. automethod:: peewee_async.AioModel.aio_get_or_none 44 | 45 | .. automethod:: peewee_async.AioModel.aio_create 46 | 47 | .. automethod:: peewee_async.AioModel.aio_get_or_create 48 | 49 | .. automethod:: peewee_async.AioModel.aio_delete_instance 50 | 51 | .. automethod:: peewee_async.AioModel.aio_save 52 | 53 | .. autofunction:: peewee_async.aio_prefetch 54 | 55 | AioModelSelect 56 | ++++++++++++++ 57 | 58 | .. autoclass:: peewee_async.aio_model.AioModelSelect 59 | 60 | .. automethod:: peewee_async.aio_model.AioModelSelect.aio_peek 61 | 62 | .. automethod:: peewee_async.aio_model.AioModelSelect.aio_scalar 63 | 64 | .. automethod:: peewee_async.aio_model.AioModelSelect.aio_first 65 | 66 | .. automethod:: peewee_async.aio_model.AioModelSelect.aio_get 67 | 68 | .. automethod:: peewee_async.aio_model.AioModelSelect.aio_count 69 | 70 | .. automethod:: peewee_async.aio_model.AioModelSelect.aio_exists 71 | 72 | .. automethod:: peewee_async.aio_model.AioModelSelect.aio_prefetch -------------------------------------------------------------------------------- /docs/peewee_async/connection.rst: -------------------------------------------------------------------------------- 1 | Connections 2 | =========== 3 | 4 | Every time some query is executed, for example, like this: 5 | 6 | .. code-block:: python 7 | 8 | await MyModel.aio_get(id=1) 9 | 10 | 11 | under the hood the execution runs under special async context manager: 12 | 13 | .. code-block:: python 14 | 15 | async with database.aio_connection() as connection: 16 | await execute_your_query() 17 | 18 | which takes a connection from the ``connection_context`` contextvar if it is not ``None`` or acquire from the pool otherwise. 19 | It releases the connection at the exit and set the ``connection_context`` to ``None``. 20 | 21 | 22 | .. code-block:: python 23 | 24 | # acquire a connection and put it to connection_context context variable 25 | await MyModel.aio_get(id=1) 26 | # release the connection and set the connection_context to None 27 | 28 | # acquire a connection and put it to connection_context context variable 29 | await MyModel.aio_get(id=2) 30 | # release the connection and set the connection_context to None 31 | 32 | # acquire a connection and put it to connection_context context variable 33 | await MyModel.aio_get(id=3) 34 | # release the connection and set the connection_context to None 35 | 36 | Manual management 37 | +++++++++++++++++++++++++++++ 38 | 39 | If you want to manage connections manually or you want to use one connection for a batch of queries you 40 | can run ``database.aio_connection`` by yourself and run the queries under the context manager. 41 | 42 | .. code-block:: python 43 | 44 | # acquire a connection and put it to connection_context context variable 45 | async with database.aio_connection() as connection: 46 | 47 | # using the connection from the contextvar 48 | await MyModel.aio_get(id=1) 49 | 50 | # using the connection from the contextvar 51 | await MyModel.aio_get(id=2) 52 | 53 | # using the connection from the contextvar 54 | await MyModel.aio_get(id=3) 55 | # release the connection set connection_context to None at the exit of the async contextmanager 56 | 57 | You can even run nested ``aio_connection`` context managers. 58 | In this case you will use one connection for the all managers from the highest context manager of the stack of calls 59 | and it will be closed when the highest manager is exited. -------------------------------------------------------------------------------- /docs/peewee_async/examples.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ============= 3 | 4 | 5 | Using async peewee with Tornado 6 | +++++++++++++++++++++++++++++++ 7 | 8 | `Tornado`_ is a mature and powerful asynchronous web framework. It provides its own event loop, but there's an option to run Tornado on asyncio event loop. And that's exactly what we need! 9 | 10 | .. _Tornado: http://www.tornadoweb.org 11 | 12 | The complete working example is provided below. And here are some general notes: 13 | 14 | 1. **Be aware of current asyncio event loop!** 15 | 16 | In the provided example we use the default event loop everywhere, and that's OK. But if you see your application got silently stuck, that's most probably that some task is started on the different loop and will never complete as long as that loop is not running. 17 | 18 | 2. Tornado request handlers **does not** start asyncio tasks by default. 19 | 20 | The ``CreateHandler`` demostrates that, ``current_task()`` returns ``None`` until task is run explicitly. 21 | 22 | 3. Transactions **must** run within task context. 23 | 24 | All transaction operations have to be done within task. So if you need to run a transaction from Tornado handler, you have to wrap your call into task with ``create_task()`` or ``ensure_future()``. 25 | 26 | **Also note:** if you spawn an extra task during a transaction, it will run outside of that transaction. 27 | 28 | .. literalinclude:: ../samples/tornado_sample.py 29 | :start-after: # Start example -------------------------------------------------------------------------------- /docs/peewee_async/installing.rst: -------------------------------------------------------------------------------- 1 | Installing 2 | ==================== 3 | 4 | Install latest version from PyPI. 5 | 6 | For PostgreSQL aiopg backend: 7 | 8 | .. code-block:: console 9 | 10 | pip install peewee-async[postgresql] 11 | 12 | For PostgreSQL psycopg3 backend: 13 | 14 | .. code-block:: console 15 | 16 | pip install peewee-async[psycopg] 17 | 18 | For MySQL: 19 | 20 | .. code-block:: console 21 | 22 | pip install peewee-async[mysql] 23 | 24 | Installing and developing 25 | +++++++++++++++++++++++++ 26 | 27 | Clone source code from GitHub: 28 | 29 | .. code-block:: console 30 | 31 | git clone https://github.com/05bit/peewee-async.git 32 | cd peewee-async 33 | 34 | Install dependencies using pip: 35 | 36 | .. code-block:: console 37 | 38 | pip install -e .[develop] 39 | 40 | 41 | Or using `poetry`_: 42 | 43 | .. _poetry: https://python-poetry.org/docs/ 44 | 45 | .. code-block:: console 46 | 47 | poetry install -E develop 48 | 49 | 50 | Running tests 51 | +++++++++++++ 52 | 53 | * Clone source code from GitHub as shown above 54 | * Run docker environment with PostgreSQL database for testing 55 | 56 | .. code-block:: console 57 | 58 | docker-compose up -d 59 | 60 | Then run tests: 61 | 62 | .. code-block:: console 63 | 64 | pytest -s -v 65 | 66 | Report bugs and discuss 67 | +++++++++++++++++++++++ 68 | 69 | You are welcome to add discussion topics or bug reports to `tracker on GitHub`_! 70 | 71 | .. _tracker on GitHub: https://github.com/05bit/peewee-async/issues 72 | -------------------------------------------------------------------------------- /docs/peewee_async/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart 2 | ==================== 3 | 4 | Let's provide an example:: 5 | 6 | import asyncio 7 | import peewee 8 | import logging 9 | from peewee_async import PooledPostgresqlDatabase 10 | 11 | database = PooledPostgresqlDatabase('test') 12 | 13 | 14 | # Let's define a simple model: 15 | class PageBlock(peewee_async.AioModel): 16 | key = peewee.CharField(max_length=40, unique=True) 17 | text = peewee.TextField(default='') 18 | 19 | class Meta: 20 | database = database 21 | 22 | -- as you can see, nothing special in this code, just plain ``peewee_async.AioModel`` definition and disabling sync queries. 23 | 24 | Now we need to create a table for model:: 25 | 26 | with database.allow_sync(): 27 | PageBlock.create_table(True) 28 | 29 | -- this code is **sync**, and will do **absolutely the same thing** as would do code with regular ``peewee.PostgresqlDatabase``. This is intentional, I believe there's just no need to run database initialization code asynchronously! *Less code, less errors*. 30 | 31 | Finally, let's do something async:: 32 | 33 | async def my_async_func(): 34 | # Add new page block 35 | await PageBlock.aio_create( 36 | key='title', 37 | text="Peewee is AWESOME with async!" 38 | ) 39 | 40 | # Get one by key 41 | title = await PageBlock.aio_get(key='title') 42 | print("Was:", title.text) 43 | 44 | # Save with new text using manager 45 | title.text = "Peewee is SUPER awesome with async!" 46 | await title.aio_save() 47 | print("New:", title.text) 48 | 49 | loop.run_until_complete(my_async_func()) 50 | loop.close() 51 | 52 | **That's it!** As you may notice there's no need to connect and re-connect before executing async queries! It's all automatic. But you can run ``AioDatabase.aio_connect()`` or ``AioDatabase.aio_close()`` when you need it. 53 | 54 | And you can use methods from from **AioModel** for operations like selecting, deleting etc. 55 | All of them you can find in the next section. 56 | 57 | 58 | Using sync calls 59 | +++++++++++++++++++++++++++++++ 60 | 61 | If you may notice in the example above if you need to run sync query you can use :py:meth:`~peewee_async.databases.AioDatabase.allow_sync` context manager: 62 | 63 | .. code-block:: python 64 | 65 | with database.allow_sync(): 66 | PageBlock.create_table(True) 67 | 68 | Be careful when using such queries. It is not recommended to use them in an asynchronous application for the following reasons: 69 | 70 | 1. For each such query, a new connection to the database is open and closed upon its completion. Which is very expensive in terms of resources. 71 | 2. If such a query is executed for a long time, then the application will not be able to execute other coroutines until the query is completed 72 | 73 | Synchronous queries should be used in tests or single-threaded tasks. -------------------------------------------------------------------------------- /docs/peewee_async/transaction.rst: -------------------------------------------------------------------------------- 1 | Transactions 2 | ========================= 3 | 4 | Peewee-async provides similiar to peewee interface for working with transactions. The interface is the :py:meth:`~peewee_async.databases.AioDatabase.aio_atomic` method, 5 | which also supports nested transactions and works as context manager. **aio_atomic()** blocks will be run in a transaction or savepoint, depending on the level of nesting. 6 | 7 | 8 | If an exception occurs in a wrapped block, the current transaction/savepoint will be rolled back. Otherwise the statements will be committed at the end of the wrapped block. 9 | 10 | .. code-block:: python 11 | 12 | async with db.aio_atomic(): # BEGIN 13 | await TestModel.aio_create(text='FOO') # INSERT INTO "testmodel" ("text", "data") VALUES ('FOO', '') RETURNING "testmodel"."id" 14 | 15 | async with db.aio_atomic(): # SAVEPOINT PWASYNC__e83bf5fc118f4e28b0fbdac90ab857ca 16 | await TestModel.update(text="BAR").aio_execute() # UPDATE "testmodel" SET "text" = 'BAR' 17 | # RELEASE SAVEPOINT PWASYNC__e83bf5fc118f4e28b0fbdac90ab857ca 18 | # COMMIT 19 | 20 | Manual management 21 | +++++++++++++++++ 22 | 23 | If you want to manage transactions manually you have to acquire a connection by yourself and manage the transaction inside the context manager of the connection: 24 | 25 | .. code-block:: python 26 | 27 | from peewee_async import Transaction 28 | async with db.aio_connection() as connection: 29 | tr = Transaction(connection) 30 | await tr.begin() # BEGIN 31 | await TestModel.aio_create(text='FOO') 32 | try: 33 | await TestModel.aio_create(text='FOO') 34 | except: 35 | await tr.rollback() # ROLLBACK 36 | else: 37 | await tr.commit() # COMMIT 38 | 39 | Raw sql for transactions 40 | ++++++++++++++++++++++++ 41 | 42 | if the above options are not enough for you you can always use raw sql for more opportunities: 43 | 44 | .. code-block:: python 45 | 46 | async with db.aio_connection() as connection: 47 | await db.aio_execute_sql(sql="begin isolation level repeatable read;") 48 | await TestModel.aio_create(text='FOO') 49 | try: 50 | await TestModel.aio_create(text='FOO') 51 | except: 52 | await await db.aio_execute_sql(sql="ROLLBACK") 53 | else: 54 | await await db.aio_execute_sql(sql="COMMIT") 55 | 56 | Just remember a transaction should work during one connection. 57 | 58 | Aware different tasks when working with transactions. 59 | +++++++++++++++++++++++++++++++++++++++++++++++++++++ 60 | 61 | As has been said a transaction should work during one connection. 62 | And as you know from :doc:`the connection section <./connection>` every connection is got from the contextvar variable which means every **asyncio.Task** has an own connection. 63 | So this example will not work and may lead to bugs: 64 | 65 | .. code-block:: python 66 | 67 | async with db.aio_atomic(): 68 | await asyncio.gather(TestModel.aio_create(text='FOO1'), TestModel.aio_create(text='FOO2'), TestModel.aio_create(text='FOO3')) 69 | 70 | Every sql query of the exmaple will run in the separate task which know nothing about started transaction in main task. 71 | -------------------------------------------------------------------------------- /docs/samples/tornado_sample.py: -------------------------------------------------------------------------------- 1 | """ 2 | Usage example for `peewee-async`_ with `Tornado`_ web framewotk. 3 | 4 | Asynchronous interface for `peewee`_ ORM powered by `asyncio`_: 5 | https://github.com/05bit/peewee-async 6 | 7 | .. _peewee-async: https://github.com/05bit/peewee-async 8 | .. _Tornado: http://www.tornadoweb.org 9 | 10 | Licensed under The MIT License (MIT) 11 | 12 | Copyright (c) 2016, Alexey Kinëv 13 | 14 | """ 15 | import asyncio 16 | import logging 17 | 18 | import peewee 19 | 20 | import peewee_async 21 | # Start example [marker for docs] 22 | import tornado.web 23 | # Set up Tornado application on asyncio 24 | from tornado.platform.asyncio import AsyncIOMainLoop 25 | 26 | # Set up database and manager 27 | database = peewee_async.PooledPostgresqlDatabase('test') 28 | 29 | 30 | # Define model 31 | class TestNameModel(peewee_async.AioModel): 32 | name = peewee.CharField() 33 | class Meta: 34 | database = database 35 | 36 | def __str__(self): 37 | return self.name 38 | 39 | 40 | # Create table, add some instances 41 | TestNameModel.create_table(True) 42 | TestNameModel.get_or_create(id=1, defaults={'name': "TestNameModel id=1"}) 43 | TestNameModel.get_or_create(id=2, defaults={'name': "TestNameModel id=2"}) 44 | TestNameModel.get_or_create(id=3, defaults={'name': "TestNameModel id=3"}) 45 | database.close() 46 | 47 | AsyncIOMainLoop().install() 48 | app = tornado.web.Application(debug=True) 49 | app.listen(port=8888) 50 | app.database = database 51 | 52 | 53 | # Add handlers 54 | class RootHandler(tornado.web.RequestHandler): 55 | """Accepts GET and POST methods. 56 | 57 | POST: create new instance, `name` argument is required 58 | GET: get instance by id, `id` argument is required 59 | """ 60 | async def post(self): 61 | name = self.get_argument('name') 62 | obj = await TestNameModel.aio_create(name=name) 63 | self.write({ 64 | 'id': obj.id, 65 | 'name': obj.name 66 | }) 67 | 68 | async def get(self): 69 | obj_id = self.get_argument('id', None) 70 | 71 | if not obj_id: 72 | self.write("Please provide the 'id' query argument, i.e. ?id=1") 73 | return 74 | 75 | try: 76 | obj = await TestNameModel.aio_get(id=obj_id) 77 | self.write({ 78 | 'id': obj.id, 79 | 'name': obj.name, 80 | }) 81 | except TestNameModel.DoesNotExist: 82 | raise tornado.web.HTTPError(404, "Object not found!") 83 | 84 | 85 | class CreateHandler(tornado.web.RequestHandler): 86 | async def get(self): 87 | loop = asyncio.get_event_loop() 88 | task1 = asyncio.Task.current_task() # Just to demonstrate it's None 89 | task2 = loop.create_task(self.get_or_create()) 90 | obj = await task2 91 | self.write({ 92 | 'task1': task1 and id(task1), 93 | 'task2': task2 and id(task2), 94 | 'obj': str(obj), 95 | 'text': "'task1' should be null, " 96 | "'task2' should be not null, " 97 | "'obj' should be newly created object", 98 | }) 99 | 100 | async def get_or_create(self): 101 | obj_id = self.get_argument('id', None) 102 | async with self.application.database.aio_atomic(): 103 | obj, created = await TestNameModel.aio_get_or_create( 104 | id=obj_id, 105 | defaults={'name': "TestNameModel id=%s" % obj_id}) 106 | return obj 107 | 108 | 109 | app.add_handlers('', [ 110 | (r"/", RootHandler), 111 | (r"/create", CreateHandler), 112 | ]) 113 | 114 | # Setup verbose logging 115 | log = logging.getLogger('') 116 | log.addHandler(logging.StreamHandler()) 117 | log.setLevel(logging.DEBUG) 118 | 119 | # Run loop 120 | print("""Run application server http://127.0.0.1:8888 121 | 122 | Try GET urls: 123 | http://127.0.0.1:8888?id=1 124 | http://127.0.0.1:8888/create?id=100 125 | 126 | Try POST with name= data: 127 | http://127.0.0.1:8888 128 | 129 | ^C to stop server""") 130 | loop = asyncio.get_event_loop() 131 | try: 132 | loop.run_forever() 133 | except KeyboardInterrupt: 134 | print(" server stopped") 135 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Examples for peewee-async 2 | ========================= 3 | 4 | To run the examples install dependencies first: 5 | 6 | ```bash 7 | pip install -r examples/requirements.txt 8 | ``` 9 | 10 | Also please run database service and provide credentials in environment variables. 11 | Feel free to use development database services with the default credentials, e.g: 12 | 13 | ```bash 14 | docker compose up postgres 15 | ``` 16 | 17 | ## Example for `aiohttp` server 18 | 19 | 20 | Define database connection settings if needed, environment variables used: 21 | 22 | - `POSTGRES_DB` 23 | - `POSTGRES_USER` 24 | - `POSTGRES_PASSWORD` 25 | - `POSTGRES_HOST` 26 | - `POSTGRES_PORT` 27 | 28 | Run this command to create example tables in the database: 29 | 30 | ```bash 31 | python -m examples.aiohttp_example 32 | ``` 33 | 34 | Run this command to start an example application: 35 | 36 | ```bash 37 | gunicorn --bind 127.0.0.1:8080 --log-level INFO --access-logfile - \ 38 | --worker-class aiohttp.GunicornWebWorker --reload \ 39 | examples.aiohttp_example:app 40 | ``` 41 | 42 | Application should be up and running: 43 | 44 | ```bash 45 | curl 'http://127.0.0.1:8080/?p=1' 46 | ``` 47 | 48 | the output should be: 49 | 50 | ``` 51 | This is a first post 52 | ``` 53 | -------------------------------------------------------------------------------- /examples/aiohttp_example.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | from secrets import token_hex 5 | from datetime import datetime 6 | from aiohttp import web 7 | from peewee import CharField, TextField, DateTimeField 8 | from peewee_async import PooledPostgresqlDatabase, AioModel 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | database = PooledPostgresqlDatabase( 13 | os.environ.get('POSTGRES_DB', 'postgres'), 14 | user=os.environ.get('POSTGRES_USER', 'postgres'), 15 | password=os.environ.get('POSTGRES_PASSWORD', 'postgres'), 16 | host=os.environ.get('POSTGRES_HOST', '127.0.0.1'), 17 | port=int(os.environ.get('POSTGRES_PORT', 5432)), 18 | min_connections=2, 19 | max_connections=10, 20 | ) 21 | 22 | app = web.Application() 23 | 24 | routes = web.RouteTableDef() 25 | 26 | 27 | class Post(AioModel): 28 | title = CharField(unique=True) 29 | key = CharField(unique=True, default=lambda: token_hex(8)) 30 | text = TextField() 31 | created_at = DateTimeField(index=True, default=datetime.utcnow) 32 | 33 | class Meta: 34 | database = database 35 | 36 | def __str__(self): 37 | return self.title 38 | 39 | 40 | def add_post(title, text): 41 | with database.atomic(): 42 | Post.create(title=title, text=text) 43 | 44 | 45 | @routes.get('/') 46 | async def get_post_endpoint(request): 47 | query = dict(request.query) 48 | post_id = query.pop('p', 1) 49 | post = await Post.aio_get_or_none(id=post_id) 50 | if post: 51 | return web.Response(text=post.text) 52 | else: 53 | return web.Response(text="Not found", status=404) 54 | 55 | 56 | @routes.post('/') 57 | async def update_post_endpoint(request): 58 | query = dict(request.query) 59 | post_id = query.pop('p', 1) 60 | try: 61 | data = await request.content.read() 62 | data = json.loads(data) 63 | text = data.get('text') 64 | if not text: 65 | raise ValueError("Missing 'text' in data") 66 | except Exception as exc: 67 | return web.Response(text=str(exc), status=400) 68 | 69 | post = await Post.aio_get_or_none(id=post_id) 70 | if post: 71 | post.text = text 72 | await post.aio_save() 73 | return web.Response(text=post.text) 74 | else: 75 | return web.Response(text="Not found", status=404) 76 | 77 | 78 | # Setup application routes 79 | 80 | app.add_routes(routes) 81 | 82 | 83 | if __name__ == '__main__': 84 | logging.basicConfig(level=logging.INFO) 85 | 86 | print("Initialize tables and add some random posts...") 87 | 88 | try: 89 | with database: 90 | database.create_tables([Post], safe=True) 91 | print("Tables are created.") 92 | except Exception as exc: 93 | print("Error creating tables: {}".format(exc)) 94 | 95 | try: 96 | add_post("Hello, world", "This is a first post") 97 | add_post("Hello, world 2", "This is a second post") 98 | add_post("42", "What is this all about?") 99 | add_post("Let it be!", "Let it be, let it be, let it be, let it be") 100 | print("Done.") 101 | except Exception as exc: 102 | print("Error adding posts: {}".format(exc)) 103 | -------------------------------------------------------------------------------- /examples/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | gunicorn 3 | peewee-async[postgresql] 4 | -------------------------------------------------------------------------------- /load-testing/README.md: -------------------------------------------------------------------------------- 1 | peewee-async load testing 2 | ============ 3 | 4 | Created for help to find race conditions in the library 5 | 6 | How to use 7 | ------- 8 | 9 | Install requirements: 10 | 11 | ```bash 12 | pip install requirments 13 | ``` 14 | 15 | Run the app: 16 | 17 | ```bash 18 | uvicorn app:app 19 | ``` 20 | 21 | Run yandex-tank: 22 | 23 | ```bash 24 | docker run -v $(pwd):/var/loadtest --net host -it yandex/yandex-tank 25 | ``` 26 | 27 | Firewall rulle to make postgreql connection unreacheable 28 | 29 | ```bash 30 | sudo iptables -I INPUT -p tcp --dport 5432 -j DROP 31 | ``` 32 | 33 | Revert the rule back: 34 | 35 | 36 | ```bash 37 | sudo iptables -D INPUT 1 38 | ``` 39 | -------------------------------------------------------------------------------- /load-testing/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from contextlib import asynccontextmanager 3 | 4 | import peewee 5 | import uvicorn 6 | from fastapi import FastAPI 7 | import asyncio 8 | from aiopg.connection import Connection 9 | from aiopg.pool import Pool 10 | import random 11 | 12 | 13 | import peewee_async 14 | 15 | 16 | aiopg_database = peewee_async.PooledPostgresqlDatabase( 17 | database='postgres', 18 | user='postgres', 19 | password='postgres', 20 | host='localhost', 21 | port=5432, 22 | pool_params = { 23 | "minsize": 0, 24 | "maxsize": 3, 25 | } 26 | ) 27 | 28 | psycopg_database = peewee_async.PsycopgDatabase( 29 | database='postgres', 30 | user='postgres', 31 | password='postgres', 32 | host='localhost', 33 | port=5432, 34 | pool_params = { 35 | "min_size": 0, 36 | "max_size": 3, 37 | } 38 | ) 39 | 40 | database = psycopg_database 41 | 42 | 43 | def patch_aiopg(): 44 | acquire = Pool.acquire 45 | cursor = Connection.cursor 46 | 47 | 48 | def new_acquire(self): 49 | choice = random.randint(1, 5) 50 | if choice == 5: 51 | raise Exception("some network error") # network error imitation 52 | return acquire(self) 53 | 54 | 55 | def new_cursor(self): 56 | choice = random.randint(1, 5) 57 | if choice == 5: 58 | raise Exception("some network error") # network error imitation 59 | return cursor(self) 60 | 61 | Connection.cursor = new_cursor 62 | Pool.acquire = new_acquire 63 | 64 | 65 | def setup_logging(): 66 | logging.basicConfig() 67 | # logging.getLogger("psycopg.pool").setLevel(logging.DEBUG) 68 | logger = logging.getLogger("uvicorn.error") 69 | handler = logging.FileHandler(filename="app.log", mode="w") 70 | logger.addHandler(handler) 71 | 72 | 73 | class AppTestModel(peewee_async.AioModel): 74 | text = peewee.CharField(max_length=100) 75 | 76 | class Meta: 77 | database = database 78 | 79 | 80 | @asynccontextmanager 81 | async def lifespan(app: FastAPI): 82 | AppTestModel.drop_table() 83 | AppTestModel.create_table() 84 | await AppTestModel.aio_create(id=1, text="1") 85 | await AppTestModel.aio_create(id=2, text="2") 86 | setup_logging() 87 | yield 88 | await database.aio_close() 89 | 90 | app = FastAPI(lifespan=lifespan) 91 | errors = set() 92 | 93 | 94 | @app.get("/errors") 95 | async def select(): 96 | return errors 97 | 98 | 99 | @app.get("/select") 100 | async def select(): 101 | await AppTestModel.select().aio_execute() 102 | 103 | 104 | 105 | @app.get("/transaction") 106 | async def transaction() -> None: 107 | async with database.aio_atomic(): 108 | await AppTestModel.update(text="5").where(AppTestModel.id==1).aio_execute() 109 | await AppTestModel.update(text="10").where(AppTestModel.id==1).aio_execute() 110 | 111 | 112 | async def nested_atomic() -> None: 113 | async with database.aio_atomic(): 114 | await AppTestModel.update(text="1").where(AppTestModel.id==2).aio_execute() 115 | 116 | 117 | @app.get("/savepoint") 118 | async def savepoint(): 119 | async with database.aio_atomic(): 120 | await AppTestModel.update(text="2").where(AppTestModel.id==2).aio_execute() 121 | await nested_atomic() 122 | 123 | 124 | 125 | @app.get("/recreate_pool") 126 | async def atomic(): 127 | await database.aio_close() 128 | await database.aio_connect() 129 | 130 | 131 | if __name__ == "__main__": 132 | uvicorn.run( 133 | app, 134 | host="0.0.0.0", 135 | access_log=True 136 | ) 137 | -------------------------------------------------------------------------------- /load-testing/load.yaml: -------------------------------------------------------------------------------- 1 | phantom: 2 | address: 127.0.0.1:8000 # [Target's address]:[target's port] 3 | uris: 4 | - /select 5 | - /transaction 6 | - /savepoint 7 | load_profile: 8 | load_type: rps # schedule load by defining requests per second 9 | schedule: const(100, 10m) # starting from 1rps growing linearly to 10rps during 10 minutes 10 | console: 11 | enabled: true # enable console output 12 | telegraf: 13 | enabled: false # let's disable telegraf monitoring for the first time 14 | -------------------------------------------------------------------------------- /load-testing/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | -------------------------------------------------------------------------------- /peewee_async/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | peewee-async 3 | ============ 4 | 5 | Asynchronous interface for `peewee`_ ORM powered by `asyncio`_: 6 | https://github.com/05bit/peewee-async 7 | 8 | .. _peewee: https://github.com/coleifer/peewee 9 | .. _asyncio: https://docs.python.org/3/library/asyncio.html 10 | 11 | Licensed under The MIT License (MIT) 12 | 13 | Copyright (c) 2014, Alexey Kinëv 14 | 15 | """ 16 | from importlib.metadata import version 17 | 18 | from playhouse.db_url import register_database 19 | from .aio_model import aio_prefetch, AioModel 20 | from .connection import connection_context 21 | from .databases import ( 22 | PooledPostgresqlDatabase, 23 | PooledPostgresqlExtDatabase, 24 | PooledMySQLDatabase, 25 | PsycopgDatabase, 26 | ) 27 | from .pool import PostgresqlPoolBackend, MysqlPoolBackend 28 | from .transactions import Transaction 29 | 30 | __version__ = version('peewee-async') 31 | 32 | 33 | __all__ = [ 34 | 'PooledPostgresqlDatabase', 35 | 'PooledPostgresqlExtDatabase', 36 | 'PooledMySQLDatabase', 37 | 'Transaction', 38 | 'AioModel', 39 | 'aio_prefetch', 40 | 'connection_context', 41 | 'PostgresqlPoolBackend', 42 | 'MysqlPoolBackend', 43 | ] 44 | 45 | register_database(PooledPostgresqlDatabase, 'postgres+pool+async', 'postgresql+pool+async') 46 | register_database(PooledPostgresqlExtDatabase, 'postgresext+pool+async', 'postgresqlext+pool+async') 47 | register_database(PsycopgDatabase, 'psycopg+pool+async', 'psycopg+pool+async') 48 | register_database(PooledMySQLDatabase, 'mysql+pool+async') 49 | -------------------------------------------------------------------------------- /peewee_async/aio_model.py: -------------------------------------------------------------------------------- 1 | import peewee 2 | from peewee import PREFETCH_TYPE 3 | 4 | from .databases import AioDatabase 5 | from .result_wrappers import fetch_models 6 | from .utils import CursorProtocol 7 | from typing_extensions import Self 8 | from typing import Tuple, List, Any, cast, Optional, Dict, Union 9 | 10 | 11 | async def aio_prefetch(sq: Any, *subqueries: Any, prefetch_type: PREFETCH_TYPE = PREFETCH_TYPE.WHERE) -> Any: 12 | """Asynchronous version of `prefetch()`. 13 | 14 | See also: 15 | http://docs.peewee-orm.com/en/3.15.3/peewee/api.html#prefetch 16 | """ 17 | if not subqueries: 18 | return sq 19 | 20 | fixed_queries = peewee.prefetch_add_subquery(sq, subqueries, prefetch_type) 21 | deps: Dict[Any, Any] = {} 22 | rel_map: Dict[Any, Any] = {} 23 | 24 | for pq in reversed(fixed_queries): 25 | query_model = pq.model 26 | if pq.fields: 27 | for rel_model in pq.rel_models: 28 | rel_map.setdefault(rel_model, []) 29 | rel_map[rel_model].append(pq) 30 | 31 | deps[query_model] = {} 32 | id_map = deps[query_model] 33 | has_relations = bool(rel_map.get(query_model)) 34 | 35 | result = await pq.query.aio_execute() 36 | 37 | for instance in result: 38 | if pq.fields: 39 | pq.store_instance(instance, id_map) 40 | if has_relations: 41 | for rel in rel_map[query_model]: 42 | rel.populate_instance(instance, deps[rel.model]) 43 | 44 | return result 45 | 46 | 47 | class AioQueryMixin: 48 | @peewee.database_required 49 | async def aio_execute(self, database: AioDatabase) -> Any: 50 | return await database.aio_execute(self) 51 | 52 | async def fetch_results(self, cursor: CursorProtocol) -> Any: 53 | return await fetch_models(cursor, self) 54 | 55 | 56 | class AioModelDelete(peewee.ModelDelete, AioQueryMixin): 57 | async def fetch_results(self, cursor: CursorProtocol) -> Union[List[Any], int]: 58 | if self._returning: 59 | return await fetch_models(cursor, self) 60 | return cursor.rowcount 61 | 62 | 63 | class AioModelUpdate(peewee.ModelUpdate, AioQueryMixin): 64 | 65 | async def fetch_results(self, cursor: CursorProtocol) -> Union[List[Any], int]: 66 | if self._returning: 67 | return await fetch_models(cursor, self) 68 | return cursor.rowcount 69 | 70 | 71 | class AioModelInsert(peewee.ModelInsert, AioQueryMixin): 72 | async def fetch_results(self, cursor: CursorProtocol) -> Union[List[Any], Any, int]: 73 | if self._returning is not None and len(self._returning) > 1: 74 | return await fetch_models(cursor, self) 75 | 76 | if self._returning: 77 | row = await cursor.fetchone() 78 | return row[0] if row else None 79 | else: 80 | return cursor.lastrowid 81 | 82 | 83 | class AioModelRaw(peewee.ModelRaw, AioQueryMixin): 84 | pass 85 | 86 | 87 | class AioSelectMixin(AioQueryMixin, peewee.SelectBase): 88 | 89 | 90 | @peewee.database_required 91 | async def aio_peek(self, database: AioDatabase, n: int = 1) -> Any: 92 | """ 93 | Asynchronous version of 94 | `peewee.SelectBase.peek `_ 95 | """ 96 | 97 | async def fetch_results(cursor: CursorProtocol) -> Any: 98 | return await fetch_models(cursor, self, n) 99 | 100 | rows = await database.aio_execute(self, fetch_results=fetch_results) 101 | if rows: 102 | return rows[0] if n == 1 else rows 103 | 104 | @peewee.database_required 105 | async def aio_scalar( 106 | self, 107 | database: AioDatabase, 108 | as_tuple: bool = False, 109 | as_dict: bool = False 110 | ) -> Any: 111 | """ 112 | Asynchronous version of `peewee.SelectBase.scalar 113 | `_ 114 | """ 115 | if as_dict: 116 | return await self.dicts().aio_peek(database) 117 | row = await self.tuples().aio_peek(database) 118 | 119 | return row[0] if row and not as_tuple else row 120 | 121 | @peewee.database_required 122 | async def aio_first(self, database: AioDatabase, n: int = 1) -> Any: 123 | """ 124 | Asynchronous version of `peewee.SelectBase.first 125 | `_ 126 | """ 127 | 128 | if self._limit != n: # type: ignore 129 | self._limit = n 130 | return await self.aio_peek(database, n=n) 131 | 132 | async def aio_get(self, database: Optional[AioDatabase] = None) -> Any: 133 | """ 134 | Asynchronous version of `peewee.SelectBase.get 135 | `_ 136 | """ 137 | clone = self.paginate(1, 1) 138 | try: 139 | return (await clone.aio_execute(database))[0] 140 | except IndexError: 141 | sql, params = clone.sql() 142 | raise self.model.DoesNotExist('%s instance matching query does ' 143 | 'not exist:\nSQL: %s\nParams: %s' % 144 | (clone.model, sql, params)) 145 | 146 | @peewee.database_required 147 | async def aio_count(self, database: AioDatabase, clear_limit: bool = False) -> int: 148 | """ 149 | Asynchronous version of `peewee.SelectBase.count 150 | `_ 151 | """ 152 | clone = self.order_by().alias('_wrapped') 153 | if clear_limit: 154 | clone._limit = clone._offset = None 155 | try: 156 | if clone._having is None and clone._group_by is None and \ 157 | clone._windows is None and clone._distinct is None and \ 158 | clone._simple_distinct is not True: 159 | clone = clone.select(peewee.SQL('1')) 160 | except AttributeError: 161 | pass 162 | return cast( 163 | int, 164 | await AioSelect([clone], [peewee.fn.COUNT(peewee.SQL('1'))]).aio_scalar(database) 165 | ) 166 | 167 | @peewee.database_required 168 | async def aio_exists(self, database: AioDatabase) -> bool: 169 | """ 170 | Asynchronous version of `peewee.SelectBase.exists 171 | `_ 172 | """ 173 | clone = self.columns(peewee.SQL('1')) 174 | clone._limit = 1 175 | clone._offset = None 176 | return bool(await clone.aio_scalar()) 177 | 178 | def union_all(self, rhs: Any) -> "AioModelCompoundSelectQuery": 179 | return AioModelCompoundSelectQuery(self.model, self, 'UNION ALL', rhs) 180 | __add__ = union_all 181 | 182 | def union(self, rhs: Any) -> "AioModelCompoundSelectQuery": 183 | return AioModelCompoundSelectQuery(self.model, self, 'UNION', rhs) 184 | __or__ = union 185 | 186 | def intersect(self, rhs: Any) -> "AioModelCompoundSelectQuery": 187 | return AioModelCompoundSelectQuery(self.model, self, 'INTERSECT', rhs) 188 | __and__ = intersect 189 | 190 | def except_(self, rhs: Any) -> "AioModelCompoundSelectQuery": 191 | return AioModelCompoundSelectQuery(self.model, self, 'EXCEPT', rhs) 192 | __sub__ = except_ 193 | 194 | def aio_prefetch(self, *subqueries: Any, prefetch_type: PREFETCH_TYPE = PREFETCH_TYPE.WHERE) -> Any: 195 | """ 196 | Asynchronous version of `peewee.ModelSelect.prefetch 197 | `_ 198 | """ 199 | return aio_prefetch(self, *subqueries, prefetch_type=prefetch_type) 200 | 201 | 202 | class AioSelect(AioSelectMixin, peewee.Select): 203 | pass 204 | 205 | 206 | class AioModelSelect(AioSelectMixin, peewee.ModelSelect): 207 | """Asynchronous version of **peewee.ModelSelect** that provides async versions of ModelSelect methods 208 | """ 209 | pass 210 | 211 | 212 | class AioModelCompoundSelectQuery(AioSelectMixin, peewee.ModelCompoundSelectQuery): 213 | pass 214 | 215 | 216 | class AioModel(peewee.Model): 217 | """Async version of **peewee.Model** that allows to execute queries asynchronously 218 | with **aio_execute** method 219 | 220 | Example:: 221 | 222 | class User(peewee_async.AioModel): 223 | username = peewee.CharField(max_length=40, unique=True) 224 | 225 | await User.select().where(User.username == 'admin').aio_execute() 226 | 227 | Also it provides async versions of **peewee.Model** shortcuts 228 | 229 | Example:: 230 | 231 | user = await User.aio_get(User.username == 'user') 232 | """ 233 | 234 | @classmethod 235 | def select(cls, *fields: Any) -> AioModelSelect: 236 | is_default = not fields 237 | if not fields: 238 | fields = cls._meta.sorted_fields 239 | return AioModelSelect(cls, fields, is_default=is_default) 240 | 241 | @classmethod 242 | def update(cls, __data: Any = None, **update: Any) -> AioModelUpdate: 243 | return AioModelUpdate(cls, cls._normalize_data(__data, update)) 244 | 245 | @classmethod 246 | def insert(cls, __data: Any = None, **insert: Any) -> AioModelInsert: 247 | return AioModelInsert(cls, cls._normalize_data(__data, insert)) 248 | 249 | @classmethod 250 | def insert_many(cls, rows: Any, fields: Any = None) -> AioModelInsert: 251 | return AioModelInsert(cls, insert=rows, columns=fields) 252 | 253 | @classmethod 254 | def insert_from(cls, query: Any, fields: Any) -> AioModelInsert: 255 | columns = [getattr(cls, field) if isinstance(field, str) 256 | else field for field in fields] 257 | return AioModelInsert(cls, insert=query, columns=columns) 258 | 259 | @classmethod 260 | def raw(cls, sql: Optional[str], *params: Optional[List[Any]]) -> AioModelRaw: 261 | return AioModelRaw(cls, sql, params) 262 | 263 | @classmethod 264 | def delete(cls) -> AioModelDelete: 265 | return AioModelDelete(cls) 266 | 267 | async def aio_delete_instance(self, recursive: bool = False, delete_nullable: bool = False) -> int: 268 | """ 269 | Async version of **peewee.Model.delete_instance** 270 | 271 | See also: 272 | http://docs.peewee-orm.com/en/3.15.3/peewee/api.html#Model.delete_instance 273 | """ 274 | if recursive: 275 | dependencies = self.dependencies(delete_nullable) 276 | for query, fk in reversed(list(dependencies)): 277 | model = fk.model 278 | if fk.null and not delete_nullable: 279 | await model.update(**{fk.name: None}).where(query).aio_execute() 280 | else: 281 | await model.delete().where(query).aio_execute() 282 | return cast(int, await type(self).delete().where(self._pk_expr()).aio_execute()) 283 | 284 | async def aio_save(self, force_insert: bool = False, only: Any =None) -> int: 285 | """ 286 | Async version of **peewee.Model.save** 287 | 288 | See also: 289 | http://docs.peewee-orm.com/en/3.15.3/peewee/api.html#Model.save 290 | """ 291 | field_dict = self.__data__.copy() 292 | if self._meta.primary_key is not False: 293 | pk_field = self._meta.primary_key 294 | pk_value = self._pk # type: ignore 295 | else: 296 | pk_field = pk_value = None 297 | if only is not None: 298 | field_dict = self._prune_fields(field_dict, only) 299 | elif self._meta.only_save_dirty and not force_insert: 300 | field_dict = self._prune_fields(field_dict, self.dirty_fields) 301 | if not field_dict: 302 | self._dirty.clear() 303 | return False 304 | 305 | self._populate_unsaved_relations(field_dict) 306 | rows = 1 307 | 308 | if self._meta.auto_increment and pk_value is None: 309 | field_dict.pop(pk_field.name, None) 310 | 311 | if pk_value is not None and not force_insert: 312 | if self._meta.composite_key: 313 | for pk_part_name in pk_field.field_names: 314 | field_dict.pop(pk_part_name, None) 315 | else: 316 | field_dict.pop(pk_field.name, None) 317 | if not field_dict: 318 | raise ValueError('no data to save!') 319 | rows = await self.update(**field_dict).where(self._pk_expr()).aio_execute() 320 | elif pk_field is not None: 321 | pk = await self.insert(**field_dict).aio_execute() 322 | if pk is not None and (self._meta.auto_increment or 323 | pk_value is None): 324 | self._pk = pk 325 | # Although we set the primary-key, do not mark it as dirty. 326 | self._dirty.discard(pk_field.name) 327 | else: 328 | await self.insert(**field_dict).aio_execute() 329 | 330 | self._dirty -= set(field_dict) # Remove any fields we saved. 331 | return rows 332 | 333 | @classmethod 334 | async def aio_get(cls, *query: Any, **filters: Any) -> Self: 335 | """Async version of **peewee.Model.get** 336 | 337 | See also: 338 | http://docs.peewee-orm.com/en/3.15.3/peewee/api.html#Model.get 339 | """ 340 | sq = cls.select() 341 | if query: 342 | if len(query) == 1 and isinstance(query[0], int): 343 | sq = sq.where(cls._meta.primary_key == query[0]) 344 | else: 345 | sq = sq.where(*query) 346 | if filters: 347 | sq = sq.filter(**filters) 348 | return cast(Self, await sq.aio_get()) 349 | 350 | @classmethod 351 | async def aio_get_or_none(cls, *query: Any, **filters: Any) -> Optional[Self]: 352 | """ 353 | Async version of **peewee.Model.get_or_none** 354 | 355 | See also: 356 | http://docs.peewee-orm.com/en/3.15.3/peewee/api.html#Model.get_or_none 357 | """ 358 | try: 359 | return await cls.aio_get(*query, **filters) 360 | except cls.DoesNotExist: 361 | return None 362 | 363 | @classmethod 364 | async def aio_create(cls, **query: Any) -> Self: 365 | """ 366 | Async version of **peewee.Model.create** 367 | 368 | See also: 369 | http://docs.peewee-orm.com/en/3.15.3/peewee/api.html#Model.create 370 | """ 371 | inst = cls(**query) 372 | await inst.aio_save(force_insert=True) 373 | return inst 374 | 375 | @classmethod 376 | async def aio_get_or_create(cls, **kwargs: Any) -> Tuple[Self, bool]: 377 | """ 378 | Async version of **peewee.Model.get_or_create** 379 | 380 | See also: 381 | http://docs.peewee-orm.com/en/3.15.3/peewee/api.html#Model.get_or_create 382 | """ 383 | defaults = kwargs.pop('defaults', {}) 384 | query = cls.select() 385 | for field, value in kwargs.items(): 386 | query = query.where(getattr(cls, field) == value) 387 | 388 | try: 389 | return await query.aio_get(), False 390 | except cls.DoesNotExist: 391 | try: 392 | if defaults: 393 | kwargs.update(defaults) 394 | async with cls._meta.database.aio_atomic(): 395 | return await cls.aio_create(**kwargs), True 396 | except peewee.IntegrityError as exc: 397 | try: 398 | return await query.aio_get(), False 399 | except cls.DoesNotExist: 400 | raise exc 401 | -------------------------------------------------------------------------------- /peewee_async/connection.py: -------------------------------------------------------------------------------- 1 | from contextvars import ContextVar 2 | from types import TracebackType 3 | from typing import Optional, Type 4 | 5 | from .pool import PoolBackend 6 | from .utils import ConnectionProtocol 7 | 8 | 9 | class ConnectionContext: 10 | def __init__(self, connection: ConnectionProtocol) -> None: 11 | self.connection = connection 12 | # needs for to know whether begin a transaction or create a savepoint 13 | self.transaction_is_opened = False 14 | 15 | 16 | connection_context: ContextVar[Optional[ConnectionContext]] = ContextVar("connection_context", default=None) 17 | 18 | 19 | class ConnectionContextManager: 20 | def __init__(self, pool_backend: PoolBackend) -> None: 21 | self.pool_backend = pool_backend 22 | self.connection_context = connection_context.get() 23 | self.resuing_connection = self.connection_context is not None 24 | 25 | async def __aenter__(self) -> ConnectionProtocol: 26 | if self.connection_context is not None: 27 | connection = self.connection_context.connection 28 | else: 29 | connection = await self.pool_backend.acquire() 30 | self.connection_context = ConnectionContext(connection) 31 | connection_context.set(self.connection_context) 32 | return connection 33 | 34 | async def __aexit__( 35 | self, 36 | exc_type: Optional[Type[BaseException]], 37 | exc_value: Optional[BaseException], 38 | traceback: Optional[TracebackType] 39 | ) -> None: 40 | if self.resuing_connection is False: 41 | if self.connection_context is not None: 42 | await self.pool_backend.release(self.connection_context.connection) 43 | connection_context.set(None) 44 | -------------------------------------------------------------------------------- /peewee_async/databases.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import logging 3 | import warnings 4 | from typing import Type, Optional, Any, AsyncIterator, Iterator, Dict, List 5 | 6 | import peewee 7 | from playhouse import postgres_ext as ext 8 | from playhouse.psycopg3_ext import Psycopg3Database 9 | 10 | from .connection import connection_context, ConnectionContextManager 11 | from .pool import PoolBackend, PostgresqlPoolBackend, MysqlPoolBackend, PsycopgPoolBackend 12 | from .transactions import Transaction 13 | from .utils import aiopg, aiomysql, psycopg, __log__, FetchResults 14 | 15 | 16 | class AioDatabase(peewee.Database): 17 | """Base async database driver providing **single drop-in sync** 18 | connection and **async connections pool** interface. 19 | 20 | :param pool_params: parameters that are passed to the pool 21 | 22 | Example:: 23 | 24 | database = PooledPostgresqlExtDatabase( 25 | 'database': 'postgres', 26 | 'host': '127.0.0.1', 27 | 'port':5432, 28 | 'password': 'postgres', 29 | 'user': 'postgres', 30 | 'pool_params': { 31 | "minsize": 0, 32 | "maxsize": 5, 33 | "timeout": 30, 34 | 'pool_recycle': 1.5 35 | } 36 | ) 37 | 38 | See also: 39 | https://peewee.readthedocs.io/en/latest/peewee/api.html#Database 40 | """ 41 | 42 | _allow_sync = False # whether sync queries are allowed 43 | 44 | pool_backend_cls: Type[PoolBackend] 45 | pool_backend: PoolBackend 46 | 47 | def __init__(self, *args: Any, **kwargs: Any) -> None: 48 | self.pool_params: Dict[str, Any] = {} 49 | super().__init__(*args, **kwargs) 50 | 51 | def init_pool_params_defaults(self) -> None: 52 | pass 53 | 54 | def init_pool_params(self) -> None: 55 | self.init_pool_params_defaults() 56 | if "min_connections" in self.connect_params or "max_connections" in self.connect_params: 57 | warnings.warn( 58 | "`min_connections` and `max_connections` are deprecated, use `pool_params` instead.", 59 | DeprecationWarning 60 | ) 61 | self.pool_params.update( 62 | { 63 | "minsize": self.connect_params.pop("min_connections", 1), 64 | "maxsize": self.connect_params.pop("max_connections", 20), 65 | } 66 | ) 67 | pool_params = self.connect_params.pop('pool_params', {}) 68 | self.pool_params.update(pool_params) 69 | self.pool_params.update(self.connect_params) 70 | 71 | def init(self, database: Optional[str], **kwargs: Any) -> None: 72 | super().init(database, **kwargs) 73 | self.init_pool_params() 74 | self.pool_backend = self.pool_backend_cls( 75 | database=self.database, 76 | **self.pool_params 77 | ) 78 | 79 | async def aio_connect(self) -> None: 80 | """Creates a connection pool 81 | """ 82 | if self.deferred: 83 | raise Exception('Error, database must be initialized before creating a connection pool') 84 | await self.pool_backend.connect() 85 | 86 | @property 87 | def is_connected(self) -> bool: 88 | """Checks if pool is connected 89 | """ 90 | return self.pool_backend.is_connected 91 | 92 | async def aio_close(self) -> None: 93 | """Close pool backend. The pool is closed until you run aio_connect manually.""" 94 | 95 | if self.deferred: 96 | raise Exception('Error, database must be initialized before creating a connection pool') 97 | 98 | await self.pool_backend.close() 99 | 100 | @contextlib.asynccontextmanager 101 | async def aio_atomic(self) -> AsyncIterator[None]: 102 | """Similar to peewee `Database.atomic()` method, but returns 103 | asynchronous context manager. 104 | """ 105 | async with self.aio_connection() as connection: 106 | _connection_context = connection_context.get() 107 | assert _connection_context is not None 108 | begin_transaction = _connection_context.transaction_is_opened is False 109 | try: 110 | async with Transaction(connection, is_savepoint=begin_transaction is False): 111 | _connection_context.transaction_is_opened = True 112 | yield 113 | finally: 114 | if begin_transaction is True: 115 | _connection_context.transaction_is_opened = False 116 | 117 | def set_allow_sync(self, value: bool) -> None: 118 | """Allow or forbid sync queries for the database. See also 119 | the :meth:`.allow_sync()` context manager. 120 | """ 121 | self._allow_sync = value 122 | 123 | @contextlib.contextmanager 124 | def allow_sync(self) -> Iterator[None]: 125 | """Allow sync queries within context. Close sync 126 | connection on exit if connected. 127 | 128 | Example:: 129 | 130 | with database.allow_sync(): 131 | PageBlock.create_table(True) 132 | """ 133 | old_allow_sync = self._allow_sync 134 | self._allow_sync = True 135 | 136 | try: 137 | yield 138 | except: 139 | raise 140 | finally: 141 | self._allow_sync = old_allow_sync 142 | self.close() 143 | 144 | def execute_sql(self, *args: Any, **kwargs: Any) -> Any: 145 | """Sync execute SQL query, `allow_sync` must be set to True. 146 | """ 147 | assert self._allow_sync, ( 148 | "Error, sync query is not allowed! Call the `.set_allow_sync()` " 149 | "or use the `.allow_sync()` context manager.") 150 | return super().execute_sql(*args, **kwargs) 151 | 152 | def aio_connection(self) -> ConnectionContextManager: 153 | if self.deferred: 154 | raise Exception('Error, database must be initialized before creating a connection pool') 155 | 156 | return ConnectionContextManager(self.pool_backend) 157 | 158 | async def aio_execute_sql( 159 | self, 160 | sql: str, 161 | params: Optional[List[Any]] = None, 162 | fetch_results: Optional[FetchResults] = None 163 | ) -> Any: 164 | __log__.debug((sql, params)) 165 | with peewee.__exception_wrapper__: 166 | async with self.aio_connection() as connection: 167 | async with connection.cursor() as cursor: 168 | await cursor.execute(sql, params or ()) 169 | if fetch_results is not None: 170 | return await fetch_results(cursor) 171 | 172 | async def aio_execute(self, query: Any, fetch_results: Optional[FetchResults] = None) -> Any: 173 | """Execute *SELECT*, *INSERT*, *UPDATE* or *DELETE* query asyncronously. 174 | 175 | :param query: peewee query instance created with ``Model.select()``, 176 | ``Model.update()`` etc. 177 | :param fetch_results: function with cursor param. It let you get data manually and 178 | don't need to close cursor It will be closed automatically. 179 | :return: result depends on query type, it's the same as for sync `query.execute()` 180 | """ 181 | ctx = self.get_sql_context() 182 | sql, params = ctx.sql(query).query() 183 | fetch_results = fetch_results or getattr(query, 'fetch_results', None) 184 | return await self.aio_execute_sql(sql, params, fetch_results=fetch_results) 185 | 186 | 187 | class PsycopgDatabase(AioDatabase, Psycopg3Database): 188 | """Extension for `peewee.PostgresqlDatabase` providing extra methods 189 | for managing async connection based on psycopg3 pool backend. 190 | 191 | Example:: 192 | 193 | database = PsycopgDatabase( 194 | 'database': 'postgres', 195 | 'host': '127.0.0.1', 196 | 'port': 5432, 197 | 'password': 'postgres', 198 | 'user': 'postgres', 199 | 'pool_params': { 200 | "min_size": 0, 201 | "max_size": 5, 202 | 'max_lifetime': 15 203 | } 204 | ) 205 | 206 | See also: 207 | https://www.psycopg.org/psycopg3/docs/advanced/pool.html 208 | """ 209 | 210 | pool_backend_cls = PsycopgPoolBackend 211 | 212 | def init(self, database: Optional[str], **kwargs: Any) -> None: 213 | if not psycopg: 214 | raise Exception("Error, psycopg is not installed!") 215 | super().init(database, **kwargs) 216 | 217 | 218 | class PooledPostgresqlDatabase(AioDatabase, peewee.PostgresqlDatabase): 219 | """Extension for `peewee.PostgresqlDatabase` providing extra methods 220 | for managing async connection based on aiopg pool backend. 221 | 222 | 223 | Example:: 224 | 225 | database = PooledPostgresqlExtDatabase( 226 | 'database': 'postgres', 227 | 'host': '127.0.0.1', 228 | 'port':5432, 229 | 'password': 'postgres', 230 | 'user': 'postgres', 231 | 'pool_params': { 232 | "minsize": 0, 233 | "maxsize": 5, 234 | "timeout": 30, 235 | 'pool_recycle': 1.5 236 | } 237 | ) 238 | 239 | See also: 240 | https://peewee.readthedocs.io/en/latest/peewee/api.html#PostgresqlDatabase 241 | """ 242 | 243 | pool_backend_cls = PostgresqlPoolBackend 244 | 245 | def init_pool_params_defaults(self) -> None: 246 | self.pool_params.update({"enable_json": False, "enable_hstore": False}) 247 | 248 | def init(self, database: Optional[str], **kwargs: Any) -> None: 249 | if not aiopg: 250 | raise Exception("Error, aiopg is not installed!") 251 | super().init(database, **kwargs) 252 | 253 | 254 | class PooledPostgresqlExtDatabase( 255 | PooledPostgresqlDatabase, 256 | ext.PostgresqlExtDatabase 257 | ): 258 | """PosgtreSQL database extended driver providing **single drop-in sync** 259 | connection and **async connections pool** interface based on aiopg pool backend. 260 | 261 | JSON fields support is enabled by default, HStore supports is disabled by 262 | default, but can be enabled through pool_params or with ``register_hstore=False`` argument. 263 | 264 | See also: 265 | https://peewee.readthedocs.io/en/latest/peewee/playhouse.html#PostgresqlExtDatabase 266 | """ 267 | def init_pool_params_defaults(self) -> None: 268 | self.pool_params.update({ 269 | "enable_json": True, 270 | "enable_hstore": self._register_hstore 271 | }) 272 | 273 | 274 | class PooledMySQLDatabase(AioDatabase, peewee.MySQLDatabase): 275 | """MySQL database driver providing **single drop-in sync** 276 | connection and **async connections pool** interface. 277 | 278 | Example:: 279 | 280 | database = PooledMySQLDatabase( 281 | 'database': 'mysql', 282 | 'host': '127.0.0.1', 283 | 'port': 3306, 284 | 'user': 'root', 285 | 'password': 'mysql', 286 | 'connect_timeout': 30, 287 | "pool_params": { 288 | "minsize": 0, 289 | "maxsize": 5, 290 | "pool_recycle": 2 291 | } 292 | ) 293 | 294 | See also: 295 | http://peewee.readthedocs.io/en/latest/peewee/api.html#MySQLDatabase 296 | """ 297 | pool_backend_cls = MysqlPoolBackend 298 | 299 | def init_pool_params_defaults(self) -> None: 300 | self.pool_params.update({"autocommit": True}) 301 | 302 | def init(self, database: Optional[str], **kwargs: Any) -> None: 303 | if not aiomysql: 304 | raise Exception("Error, aiomysql is not installed!") 305 | super().init(database, **kwargs) 306 | -------------------------------------------------------------------------------- /peewee_async/pool.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import asyncio 3 | from typing import Any, Optional, cast 4 | 5 | from .utils import aiopg, aiomysql, ConnectionProtocol, format_dsn, psycopg, psycopg_pool 6 | 7 | 8 | class PoolBackend(metaclass=abc.ABCMeta): 9 | """Asynchronous database connection pool.""" 10 | 11 | def __init__(self, *, database: str, **kwargs: Any) -> None: 12 | self.pool: Optional[Any] = None 13 | self.database = database 14 | self.connect_params = kwargs 15 | self._connection_lock = asyncio.Lock() 16 | 17 | @property 18 | def is_connected(self) -> bool: 19 | if self.pool is not None: 20 | return self.pool.closed is False 21 | return False 22 | 23 | @abc.abstractmethod 24 | def has_acquired_connections(self) -> bool: 25 | """Checks if the pool has acquired connections""" 26 | ... 27 | 28 | async def connect(self) -> None: 29 | async with self._connection_lock: 30 | if self.is_connected is False: 31 | await self.create() 32 | 33 | @abc.abstractmethod 34 | async def acquire(self) -> ConnectionProtocol: 35 | """Acquire connection from the pool.""" 36 | ... 37 | 38 | @abc.abstractmethod 39 | async def release(self, conn: ConnectionProtocol) -> None: 40 | """Release connection to the pool.""" 41 | ... 42 | 43 | @abc.abstractmethod 44 | async def create(self) -> None: 45 | """Create connection pool asynchronously.""" 46 | ... 47 | 48 | @abc.abstractmethod 49 | async def close(self) -> None: 50 | """Close the pool.""" 51 | ... 52 | 53 | 54 | class PostgresqlPoolBackend(PoolBackend): 55 | """Asynchronous database connection pool based on aiopg.""" 56 | 57 | async def create(self) -> None: 58 | if "connect_timeout" in self.connect_params: 59 | self.connect_params['timeout'] = self.connect_params.pop("connect_timeout") 60 | self.pool = await aiopg.create_pool( 61 | database=self.database, 62 | **self.connect_params 63 | ) 64 | 65 | async def acquire(self) -> ConnectionProtocol: 66 | if self.pool is None: 67 | await self.connect() 68 | assert self.pool is not None, "Pool is not connected" 69 | return cast(ConnectionProtocol, await self.pool.acquire()) 70 | 71 | async def release(self, conn: ConnectionProtocol) -> None: 72 | assert self.pool is not None, "Pool is not connected" 73 | self.pool.release(conn) 74 | 75 | async def close(self) -> None: 76 | if self.pool is not None: 77 | self.pool.terminate() 78 | await self.pool.wait_closed() 79 | 80 | def has_acquired_connections(self) -> bool: 81 | if self.pool is not None: 82 | return len(self.pool._used) > 0 83 | return False 84 | 85 | class PsycopgPoolBackend(PoolBackend): 86 | """Asynchronous database connection pool based on psycopg + psycopg_pool.""" 87 | 88 | async def create(self) -> None: 89 | params = self.connect_params.copy() 90 | pool = psycopg_pool.AsyncConnectionPool( 91 | format_dsn( 92 | 'postgresql', 93 | host=params.pop('host'), 94 | port=params.pop('port'), 95 | user=params.pop('user'), 96 | password=params.pop('password'), 97 | path=self.database, 98 | ), 99 | kwargs={ 100 | 'cursor_factory': psycopg.AsyncClientCursor, 101 | 'autocommit': True, 102 | }, 103 | open=False, 104 | **params, 105 | ) 106 | 107 | await pool.open() 108 | self.pool = pool 109 | 110 | def has_acquired_connections(self) -> bool: 111 | if self.pool is not None: 112 | stats = self.pool.get_stats() 113 | return stats['pool_size'] > stats['pool_available'] # type: ignore 114 | return False 115 | 116 | async def acquire(self) -> ConnectionProtocol: 117 | if self.pool is None: 118 | await self.connect() 119 | assert self.pool is not None, "Pool is not connected" 120 | return cast(ConnectionProtocol, await self.pool.getconn()) 121 | 122 | async def release(self, conn: ConnectionProtocol) -> None: 123 | assert self.pool is not None, "Pool is not connected" 124 | await self.pool.putconn(conn) 125 | 126 | async def close(self) -> None: 127 | """Close the pool. Notes the pool does not close active connections""" 128 | if self.pool is not None: 129 | await self.pool.close() 130 | 131 | 132 | class MysqlPoolBackend(PoolBackend): 133 | """Asynchronous database connection pool based on aiomysql.""" 134 | 135 | async def create(self) -> None: 136 | self.pool = await aiomysql.create_pool( 137 | db=self.database, **self.connect_params 138 | ) 139 | 140 | async def acquire(self) -> ConnectionProtocol: 141 | if self.pool is None: 142 | await self.connect() 143 | assert self.pool is not None, "Pool is not connected" 144 | return cast(ConnectionProtocol, await self.pool.acquire()) 145 | 146 | async def release(self, conn: ConnectionProtocol) -> None: 147 | assert self.pool is not None, "Pool is not connected" 148 | self.pool.release(conn) 149 | 150 | def has_acquired_connections(self) -> bool: 151 | if self.pool is not None: 152 | return len(self.pool._used) > 0 153 | return False 154 | 155 | async def close(self) -> None: 156 | if self.pool is not None: 157 | self.pool.terminate() 158 | await self.pool.wait_closed() 159 | -------------------------------------------------------------------------------- /peewee_async/result_wrappers.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List 2 | from typing import Optional, Sequence 3 | 4 | from peewee import BaseQuery 5 | 6 | from .utils import CursorProtocol 7 | 8 | 9 | class SyncCursorAdapter(object): 10 | def __init__(self, rows: List[Any], description: Optional[Sequence[Any]]) -> None: 11 | self._rows = rows 12 | self.description = description 13 | self._idx = 0 14 | 15 | def fetchone(self) -> Any: 16 | if self._idx >= len(self._rows): 17 | return None 18 | row = self._rows[self._idx] 19 | self._idx += 1 20 | return row 21 | 22 | def close(self) -> None: 23 | pass 24 | 25 | 26 | async def fetch_models(cursor: CursorProtocol, query: BaseQuery, count: Optional[int] = None) -> List[Any]: 27 | if count is None: 28 | rows = await cursor.fetchall() 29 | else: 30 | rows = await cursor.fetchmany(count) 31 | sync_cursor = SyncCursorAdapter(rows, cursor.description) 32 | _result_wrapper = query._get_cursor_wrapper(sync_cursor) 33 | return list(_result_wrapper) 34 | -------------------------------------------------------------------------------- /peewee_async/transactions.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from types import TracebackType 3 | from typing import Optional, Type 4 | 5 | from .utils import ConnectionProtocol 6 | 7 | 8 | class Transaction: 9 | 10 | def __init__(self, connection: ConnectionProtocol, is_savepoint: bool = False): 11 | self.connection = connection 12 | self.savepoint: Optional[str] = None 13 | if is_savepoint: 14 | self.savepoint = f"PWASYNC__{uuid.uuid4().hex}" 15 | 16 | @property 17 | def is_savepoint(self) -> bool: 18 | return self.savepoint is not None 19 | 20 | async def execute(self, sql: str) -> None: 21 | async with self.connection.cursor() as cursor: 22 | await cursor.execute(sql) 23 | 24 | async def begin(self) -> None: 25 | sql = "BEGIN" 26 | if self.savepoint: 27 | sql = f"SAVEPOINT {self.savepoint}" 28 | await self.execute(sql) 29 | 30 | async def __aenter__(self) -> 'Transaction': 31 | await self.begin() 32 | return self 33 | 34 | async def __aexit__( 35 | self, 36 | exc_type: Optional[Type[BaseException]], 37 | exc_value: Optional[BaseException], 38 | traceback: Optional[TracebackType] 39 | ) -> None: 40 | if exc_type is not None: 41 | await self.rollback() 42 | else: 43 | await self.commit() 44 | 45 | async def commit(self) -> None: 46 | 47 | sql = "COMMIT" 48 | if self.savepoint: 49 | sql = f"RELEASE SAVEPOINT {self.savepoint}" 50 | await self.execute(sql) 51 | 52 | async def rollback(self) -> None: 53 | sql = "ROLLBACK" 54 | if self.savepoint: 55 | sql = f"ROLLBACK TO SAVEPOINT {self.savepoint}" 56 | await self.execute(sql) 57 | -------------------------------------------------------------------------------- /peewee_async/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any, Protocol, Optional, Sequence, Set, AsyncContextManager, List, Callable, Awaitable, Union 3 | 4 | try: 5 | import aiopg 6 | import psycopg2 7 | except ImportError: 8 | aiopg = None # type: ignore 9 | psycopg2 = None 10 | 11 | try: 12 | import psycopg 13 | import psycopg_pool 14 | except ImportError: 15 | psycopg = None # type: ignore 16 | psycopg_pool = None # type: ignore 17 | 18 | try: 19 | import aiomysql 20 | import pymysql 21 | except ImportError: 22 | aiomysql = None 23 | pymysql = None # type: ignore 24 | 25 | __log__ = logging.getLogger('peewee.async') 26 | __log__.addHandler(logging.NullHandler()) 27 | 28 | 29 | class CursorProtocol(Protocol): 30 | async def fetchone(self) -> Any: 31 | ... 32 | 33 | async def fetchall(self) -> List[Any]: 34 | ... 35 | 36 | async def fetchmany(self, size: int) -> List[Any]: 37 | ... 38 | 39 | @property 40 | def lastrowid(self) -> int: 41 | ... 42 | 43 | @property 44 | def description(self) -> Optional[Sequence[Any]]: 45 | ... 46 | 47 | @property 48 | def rowcount(self) -> int: 49 | ... 50 | 51 | async def execute(self, query: str, *args: Any, **kwargs: Any) -> None: 52 | ... 53 | 54 | 55 | class ConnectionProtocol(Protocol): 56 | def cursor( 57 | self, 58 | **kwargs: Any 59 | ) -> AsyncContextManager[CursorProtocol]: 60 | ... 61 | 62 | 63 | FetchResults = Callable[[CursorProtocol], Awaitable[Any]] 64 | 65 | 66 | def format_dsn(protocol: str, host: str, port: Union[str, int], user: str, password: str, path: str = '') -> str: 67 | return f'{protocol}://{user}:{password}@{host}:{port}/{path}' 68 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "peewee-async" 3 | version = "1.0.0" 4 | description = "Asynchronous interface for peewee ORM powered by asyncio." 5 | authors = ["Alexey Kinev ", "Gorshkov Nikolay(contributor) "] 6 | readme = "README.md" 7 | 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.9" 11 | peewee = "^3.15.4" 12 | typing-extensions = "^4.12.2" 13 | 14 | aiopg = { version = "^1.4.0", optional = true } 15 | aiomysql = { version = "^0.2.0", optional = true } 16 | cryptography = { version = "^43.0.1", optional = true } 17 | pytest = { version = "^7.4.1", optional = true } 18 | pytest-asyncio = { version = "^0.21.1", optional = true } 19 | pytest-mock = { version = "^3.14.0", optional = true } 20 | sphinx = { version = "^7.1.2", optional = true } 21 | sphinx-rtd-theme = { version = "^1.3.0rc1", optional = true } 22 | mypy = { version = "^1.10.1", optional = true } 23 | types-PyMySQL = { version = "^1.1.0.20240524", optional = true } 24 | psycopg = { version = "^3.2.0", optional = true } 25 | psycopg-pool = { version = "^3.2.0", optional = true } 26 | 27 | [tool.poetry.extras] 28 | postgresql = ["aiopg"] 29 | mysql = ["aiomysql", "cryptography"] 30 | develop = ["aiopg", "aiomysql", "cryptography", "pytest", "pytest-asyncio", "pytest-mock", "mypy", "types-PyMySQL", "psycopg", "psycopg-pool"] 31 | docs = ["aiopg", "aiomysql", "cryptography", "sphinx", "sphinx-rtd-theme"] 32 | psycopg = ["psycopg", "psycopg-pool"] 33 | 34 | [build-system] 35 | requires = ["poetry-core"] 36 | build-backend = "poetry.core.masonry.api" 37 | 38 | [tool.mypy] 39 | python_version = "3.9" 40 | ignore_missing_imports = true 41 | no_implicit_optional = true 42 | strict_equality = true 43 | check_untyped_defs = true 44 | warn_redundant_casts = true 45 | warn_unused_configs = true 46 | warn_unused_ignores = true 47 | warn_return_any = true 48 | disallow_any_generics = true 49 | disallow_untyped_calls = true 50 | disallow_untyped_defs = true 51 | disallow_incomplete_defs = true 52 | exclude = "(venv|load-testing|examples|docs)" 53 | 54 | [tool.pytest.ini_options] 55 | asyncio_mode = "auto" -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/05bit/peewee-async/92009afdd944494b321965110f78e8989a82b23a/tests/__init__.py -------------------------------------------------------------------------------- /tests/aio_model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/05bit/peewee-async/92009afdd944494b321965110f78e8989a82b23a/tests/aio_model/__init__.py -------------------------------------------------------------------------------- /tests/aio_model/test_deleting.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from peewee_async.databases import AioDatabase 4 | from tests.conftest import dbs_all, dbs_postgres 5 | from tests.models import TestModel 6 | from tests.utils import model_has_fields 7 | 8 | 9 | @dbs_all 10 | async def test_delete__count(db: AioDatabase) -> None: 11 | query = TestModel.insert_many([ 12 | {'text': "Test %s" % uuid.uuid4()}, 13 | {'text': "Test %s" % uuid.uuid4()}, 14 | ]) 15 | await query.aio_execute() 16 | 17 | count = await TestModel.delete().aio_execute() 18 | 19 | assert count == 2 20 | 21 | 22 | @dbs_all 23 | async def test_delete__by_condition(db: AioDatabase) -> None: 24 | expected_text = "text1" 25 | deleted_text = "text2" 26 | query = TestModel.insert_many([ 27 | {'text': expected_text}, 28 | {'text': deleted_text}, 29 | ]) 30 | await query.aio_execute() 31 | 32 | await TestModel.delete().where(TestModel.text == deleted_text).aio_execute() 33 | 34 | res = await TestModel.select().aio_execute() 35 | assert len(res) == 1 36 | assert res[0].text == expected_text 37 | 38 | 39 | @dbs_postgres 40 | async def test_delete__return_model(db: AioDatabase) -> None: 41 | m = await TestModel.aio_create(text="text", data="data") 42 | 43 | res = await TestModel.delete().returning(TestModel).aio_execute() 44 | assert model_has_fields(res[0], { 45 | "id": m.id, 46 | "text": m.text, 47 | "data": m.data 48 | }) is True 49 | -------------------------------------------------------------------------------- /tests/aio_model/test_inserting.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from peewee_async.databases import AioDatabase 4 | from tests.conftest import dbs_all, dbs_postgres 5 | from tests.models import TestModel, UUIDTestModel 6 | from tests.utils import model_has_fields 7 | 8 | 9 | @dbs_all 10 | async def test_insert_many(db: AioDatabase) -> None: 11 | last_id = await TestModel.insert_many([ 12 | {'text': "Test %s" % uuid.uuid4()}, 13 | {'text': "Test %s" % uuid.uuid4()}, 14 | ]).aio_execute() 15 | 16 | res = await TestModel.select().aio_execute() 17 | 18 | assert len(res) == 2 19 | assert last_id in [m.id for m in res] 20 | 21 | 22 | @dbs_all 23 | async def test_insert__return_id(db: AioDatabase) -> None: 24 | last_id = await TestModel.insert(text="Test %s" % uuid.uuid4()).aio_execute() 25 | 26 | res = await TestModel.select().aio_execute() 27 | obj = res[0] 28 | assert last_id == obj.id 29 | 30 | 31 | @dbs_postgres 32 | async def test_insert_on_conflict_ignore__last_id_is_none(db: AioDatabase) -> None: 33 | query = TestModel.insert(text="text").on_conflict_ignore() 34 | await query.aio_execute() 35 | 36 | last_id = await query.aio_execute() 37 | 38 | assert last_id is None 39 | 40 | 41 | @dbs_postgres 42 | async def test_insert_on_conflict_ignore__return_model(db: AioDatabase) -> None: 43 | query = TestModel.insert(text="text", data="data").on_conflict_ignore().returning(TestModel) 44 | 45 | res = await query.aio_execute() 46 | 47 | inserted = res[0] 48 | res = await TestModel.select().aio_execute() 49 | expected = res[0] 50 | 51 | assert model_has_fields(inserted, { 52 | "id": expected.id, 53 | "text": expected.text, 54 | "data": expected.data 55 | }) is True 56 | 57 | 58 | @dbs_postgres 59 | async def test_insert_on_conflict_ignore__inserted_once(db: AioDatabase) -> None: 60 | query = TestModel.insert(text="text").on_conflict_ignore() 61 | last_id = await query.aio_execute() 62 | 63 | await query.aio_execute() 64 | 65 | res = await TestModel.select().aio_execute() 66 | assert len(res) == 1 67 | assert res[0].id == last_id 68 | 69 | 70 | @dbs_postgres 71 | async def test_insert__uuid_pk(db: AioDatabase) -> None: 72 | query = UUIDTestModel.insert(text="Test %s" % uuid.uuid4()) 73 | last_id = await query.aio_execute() 74 | assert len(str(last_id)) == 36 75 | 76 | 77 | @dbs_postgres 78 | async def test_insert__return_model(db: AioDatabase) -> None: 79 | text = "Test %s" % uuid.uuid4() 80 | data = "data" 81 | query = TestModel.insert(text=text, data=data).returning(TestModel) 82 | 83 | res = await query.aio_execute() 84 | 85 | inserted = res[0] 86 | assert model_has_fields( 87 | inserted, {"id": inserted.id, "text": text, "data": data} 88 | ) is True 89 | 90 | 91 | @dbs_postgres 92 | async def test_insert_many__return_model(db: AioDatabase) -> None: 93 | texts = [f"text{n}" for n in range(2)] 94 | query = TestModel.insert_many([ 95 | {"text": text} for text in texts 96 | ]).returning(TestModel) 97 | 98 | res = await query.aio_execute() 99 | 100 | texts = [m.text for m in res] 101 | assert sorted(texts) == ["text0", "text1"] 102 | -------------------------------------------------------------------------------- /tests/aio_model/test_selecting.py: -------------------------------------------------------------------------------- 1 | from peewee_async.aio_model import AioModelCompoundSelectQuery, AioModelRaw 2 | from peewee_async.databases import AioDatabase 3 | from tests.conftest import dbs_all 4 | from tests.models import TestModel, TestModelAlpha, TestModelBeta 5 | 6 | 7 | @dbs_all 8 | async def test_select_w_join(db: AioDatabase) -> None: 9 | alpha = await TestModelAlpha.aio_create(text="Test 1") 10 | beta = await TestModelBeta.aio_create(alpha_id=alpha.id, text="text") 11 | 12 | result = (await TestModelBeta.select(TestModelBeta, TestModelAlpha).join( 13 | TestModelAlpha, 14 | attr="joined_alpha", 15 | ).aio_execute())[0] 16 | 17 | assert result.id == beta.id 18 | assert result.joined_alpha.id == alpha.id 19 | 20 | 21 | @dbs_all 22 | async def test_raw_select(db: AioDatabase) -> None: 23 | obj1 = await TestModel.aio_create(text="Test 1") 24 | obj2 = await TestModel.aio_create(text="Test 2") 25 | query = TestModel.raw( 26 | 'SELECT id, text, data FROM testmodel m ORDER BY m.text' 27 | ) 28 | assert isinstance(query, AioModelRaw) 29 | result = await query.aio_execute() 30 | assert list(result) == [obj1, obj2] 31 | 32 | 33 | @dbs_all 34 | async def test_tuples(db: AioDatabase) -> None: 35 | obj = await TestModel.aio_create(text="Test 1") 36 | 37 | result = await TestModel.select(TestModel.id, TestModel.text).tuples().aio_execute() 38 | assert result[0] == (obj.id, obj.text) 39 | 40 | 41 | @dbs_all 42 | async def test_dicts(db: AioDatabase) -> None: 43 | obj = await TestModel.aio_create(text="Test 1") 44 | 45 | result = await TestModel.select(TestModel.id, TestModel.text).dicts().aio_execute() 46 | assert result[0] == {"id": obj.id, "text": obj.text} 47 | 48 | 49 | @dbs_all 50 | async def test_union_all(db: AioDatabase) -> None: 51 | obj1 = await TestModel.aio_create(text="1") 52 | obj2 = await TestModel.aio_create(text="2") 53 | query = ( 54 | TestModel.select().where(TestModel.id == obj1.id) + 55 | TestModel.select().where(TestModel.id == obj2.id) + 56 | TestModel.select().where(TestModel.id == obj2.id) 57 | ) 58 | result = await query.aio_execute() 59 | assert sorted(r.text for r in result) == ["1", "2", "2"] 60 | 61 | 62 | @dbs_all 63 | async def test_union(db: AioDatabase) -> None: 64 | obj1 = await TestModel.aio_create(text="1") 65 | obj2 = await TestModel.aio_create(text="2") 66 | query = ( 67 | TestModel.select().where(TestModel.id == obj1.id) | 68 | TestModel.select().where(TestModel.id == obj2.id) | 69 | TestModel.select().where(TestModel.id == obj2.id) 70 | ) 71 | assert isinstance(query, AioModelCompoundSelectQuery) 72 | result = await query.aio_execute() 73 | assert sorted(r.text for r in result) == ["1", "2"] 74 | 75 | 76 | @dbs_all 77 | async def test_intersect(db: AioDatabase) -> None: 78 | await TestModel.aio_create(text="1") 79 | await TestModel.aio_create(text="2") 80 | await TestModel.aio_create(text="3") 81 | query = ( 82 | TestModel.select().where( 83 | (TestModel.text == "1") | (TestModel.text == "2") 84 | ) & 85 | TestModel.select().where( 86 | (TestModel.text == "2") | (TestModel.text == "3") 87 | ) 88 | ) 89 | result = await query.aio_execute() 90 | assert sorted(r.text for r in result) == ["2"] 91 | 92 | 93 | @dbs_all 94 | async def test_except(db: AioDatabase) -> None: 95 | await TestModel.aio_create(text="1") 96 | await TestModel.aio_create(text="2") 97 | await TestModel.aio_create(text="3") 98 | query = ( 99 | TestModel.select().where( 100 | (TestModel.text == "1") | (TestModel.text == "2") | (TestModel.text == "3") 101 | ) - 102 | TestModel.select().where( 103 | (TestModel.text == "2") 104 | ) 105 | ) 106 | result = await query.aio_execute() 107 | assert sorted(r.text for r in result) == ["1", "3"] -------------------------------------------------------------------------------- /tests/aio_model/test_shortcuts.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union 2 | import uuid 3 | 4 | import peewee 5 | import pytest 6 | from peewee import fn 7 | 8 | from peewee_async.databases import AioDatabase 9 | from tests.conftest import dbs_all 10 | from tests.models import TestModel, IntegerTestModel, TestModelAlpha, TestModelBeta, TestModelGamma 11 | 12 | 13 | @dbs_all 14 | async def test_aio_get(db: AioDatabase) -> None: 15 | obj1 = await TestModel.aio_create(text="Test 1") 16 | obj2 = await TestModel.aio_create(text="Test 2") 17 | 18 | result = await TestModel.aio_get(TestModel.id == obj1.id) 19 | assert result.id == obj1.id 20 | 21 | result = await TestModel.aio_get(TestModel.text == "Test 2") 22 | assert result.id == obj2.id 23 | 24 | with pytest.raises(TestModel.DoesNotExist): 25 | await TestModel.aio_get(TestModel.text == "unknown") 26 | 27 | 28 | @dbs_all 29 | async def test_aio_get_or_none(db: AioDatabase) -> None: 30 | obj1 = await TestModel.aio_create(text="Test 1") 31 | 32 | result = await TestModel.aio_get_or_none(TestModel.id == obj1.id) 33 | assert result is not None and result.id == obj1.id 34 | 35 | result = await TestModel.aio_get_or_none(TestModel.text == "unknown") 36 | assert result is None 37 | 38 | 39 | @dbs_all 40 | @pytest.mark.parametrize( 41 | ["peek_num", "expected"], 42 | ( 43 | (1, 1), 44 | (2, [1,2]), 45 | (5, [1,2,3]), 46 | ) 47 | ) 48 | async def test_aio_peek( 49 | db: AioDatabase, 50 | peek_num: int, 51 | expected: Union[int, List[int]] 52 | ) -> None: 53 | await IntegerTestModel.aio_create(num=1) 54 | await IntegerTestModel.aio_create(num=2) 55 | await IntegerTestModel.aio_create(num=3) 56 | 57 | rows = await IntegerTestModel.select().order_by( 58 | IntegerTestModel.num 59 | ).aio_peek(n=peek_num) 60 | 61 | if isinstance(rows, list): 62 | result = [r.num for r in rows] 63 | else: 64 | result = rows.num 65 | assert result == expected 66 | 67 | 68 | @dbs_all 69 | @pytest.mark.parametrize( 70 | ["first_num", "expected"], 71 | ( 72 | (1, 1), 73 | (2, [1,2]), 74 | (5, [1,2,3]), 75 | ) 76 | ) 77 | async def test_aio_first( 78 | db: AioDatabase, 79 | first_num: int, 80 | expected: Union[int, List[int]] 81 | ) -> None: 82 | await IntegerTestModel.aio_create(num=1) 83 | await IntegerTestModel.aio_create(num=2) 84 | await IntegerTestModel.aio_create(num=3) 85 | 86 | rows = await IntegerTestModel.select().order_by( 87 | IntegerTestModel.num 88 | ).aio_first(n=first_num) 89 | 90 | if isinstance(rows, list): 91 | result = [r.num for r in rows] 92 | else: 93 | result = rows.num 94 | assert result == expected 95 | 96 | 97 | @dbs_all 98 | async def test_aio_scalar(db: AioDatabase) -> None: 99 | await IntegerTestModel.aio_create(num=1) 100 | await IntegerTestModel.aio_create(num=2) 101 | 102 | assert await IntegerTestModel.select(fn.MAX(IntegerTestModel.num)).aio_scalar() == 2 103 | 104 | assert await IntegerTestModel.select( 105 | fn.MAX(IntegerTestModel.num),fn.Min(IntegerTestModel.num) 106 | ).aio_scalar(as_tuple=True) == (2, 1) 107 | 108 | assert await IntegerTestModel.select( 109 | fn.MAX(IntegerTestModel.num).alias('max'), 110 | fn.Min(IntegerTestModel.num).alias('min') 111 | ).aio_scalar(as_dict=True) == {'max': 2, 'min': 1} 112 | 113 | assert await TestModel.select().aio_scalar() is None 114 | 115 | 116 | @dbs_all 117 | async def test_count_query(db: AioDatabase) -> None: 118 | 119 | for num in range(5): 120 | await IntegerTestModel.aio_create(num=num) 121 | count = await IntegerTestModel.select().limit(3).aio_count() 122 | assert count == 3 123 | 124 | 125 | @dbs_all 126 | async def test_count_query_clear_limit(db: AioDatabase) -> None: 127 | 128 | for num in range(5): 129 | await IntegerTestModel.aio_create(num=num) 130 | count = await IntegerTestModel.select().limit(3).aio_count(clear_limit=True) 131 | assert count == 5 132 | 133 | 134 | @dbs_all 135 | async def test_aio_delete_instance(db: AioDatabase) -> None: 136 | text = "Test %s" % uuid.uuid4() 137 | obj1 = await TestModel.aio_create(text=text) 138 | obj2 = await TestModel.aio_get(id=obj1.id) 139 | 140 | await obj2.aio_delete_instance() 141 | 142 | obj3 = await TestModel.aio_get_or_none(id=obj1.id) 143 | assert obj3 is None 144 | 145 | 146 | @dbs_all 147 | async def test_aio_delete_instance_with_fk(db: AioDatabase) -> None: 148 | alpha = await TestModelAlpha.aio_create(text="test") 149 | beta = await TestModelBeta.aio_create(alpha=alpha, text="test") 150 | 151 | await alpha.aio_delete_instance(recursive=True) 152 | 153 | assert await TestModelAlpha.aio_get_or_none(id=alpha.id) is None 154 | assert await TestModelBeta.aio_get_or_none(id=beta.id) is None 155 | 156 | 157 | @dbs_all 158 | async def test_aio_save(db: AioDatabase) -> None: 159 | t = TestModel(text="text", data="data") 160 | rows = await t.aio_save() 161 | assert rows == 1 162 | assert t.id is not None 163 | 164 | assert await TestModel.aio_get_or_none(text="text", data="data") is not None 165 | 166 | 167 | @dbs_all 168 | async def test_aio_save__force_insert(db: AioDatabase) -> None: 169 | t = await TestModel.aio_create(text="text", data="data") 170 | t.data = "data2" 171 | await t.aio_save() 172 | 173 | assert await TestModel.aio_get_or_none(text="text", data="data2") is not None 174 | 175 | with pytest.raises(peewee.IntegrityError): 176 | await t.aio_save(force_insert=True) 177 | 178 | 179 | @dbs_all 180 | async def test_aio_get_or_create__get(db: AioDatabase) -> None: 181 | t1 = await TestModel.aio_create(text="text", data="data") 182 | t2, created = await TestModel.aio_get_or_create(text="text") 183 | assert t1.id == t2.id 184 | assert created is False 185 | 186 | 187 | @dbs_all 188 | async def test_aio_get_or_create__created(db: AioDatabase) -> None: 189 | t2, created = await TestModel.aio_get_or_create(text="text") 190 | assert t2.text == "text" 191 | assert created is True 192 | 193 | 194 | @dbs_all 195 | async def test_aio_exists(db: AioDatabase) -> None: 196 | await TestModel.aio_create(text="text1", data="data") 197 | await TestModel.aio_create(text="text2", data="data") 198 | 199 | assert await TestModel.select().where(TestModel.data=="data").aio_exists() is True 200 | assert await TestModel.select().where(TestModel.data == "not_existed").aio_exists() is False 201 | 202 | 203 | @dbs_all 204 | @pytest.mark.parametrize( 205 | "prefetch_type", 206 | peewee.PREFETCH_TYPE.values() 207 | ) 208 | async def test_aio_prefetch(db: AioDatabase, prefetch_type: peewee.PREFETCH_TYPE) -> None: 209 | alpha_1 = await TestModelAlpha.aio_create(text='Alpha 1') 210 | alpha_2 = await TestModelAlpha.aio_create(text='Alpha 2') 211 | 212 | beta_11 = await TestModelBeta.aio_create(alpha=alpha_1, text='Beta 11') 213 | beta_12 = await TestModelBeta.aio_create(alpha=alpha_1, text='Beta 12') 214 | _ = await TestModelBeta.aio_create( 215 | alpha=alpha_2, text='Beta 21' 216 | ) 217 | _ = await TestModelBeta.aio_create( 218 | alpha=alpha_2, text='Beta 22' 219 | ) 220 | 221 | gamma_111 = await TestModelGamma.aio_create( 222 | beta=beta_11, text='Gamma 111' 223 | ) 224 | gamma_112 = await TestModelGamma.aio_create( 225 | beta=beta_11, text='Gamma 112' 226 | ) 227 | 228 | result = await TestModelAlpha.select().order_by(TestModelAlpha.id).aio_prefetch( 229 | TestModelBeta.select().order_by(TestModelBeta.id), 230 | TestModelGamma.select().order_by(TestModelGamma.id), 231 | prefetch_type=prefetch_type, 232 | ) 233 | assert tuple(result) == (alpha_1, alpha_2) 234 | assert tuple(result[0].betas) == (beta_11, beta_12) 235 | assert tuple(result[0].betas[0].gammas) == (gamma_111, gamma_112) 236 | -------------------------------------------------------------------------------- /tests/aio_model/test_updating.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from peewee_async.databases import AioDatabase 4 | from tests.conftest import dbs_all, dbs_postgres 5 | from tests.models import TestModel 6 | 7 | 8 | @dbs_all 9 | async def test_update__count(db: AioDatabase) -> None: 10 | for n in range(3): 11 | await TestModel.aio_create(text=f"{n}") 12 | count = await TestModel.update(data="new_data").aio_execute() 13 | 14 | assert count == 3 15 | 16 | 17 | @dbs_all 18 | async def test_update__field_updated(db: AioDatabase) -> None: 19 | text = "Test %s" % uuid.uuid4() 20 | obj1 = await TestModel.aio_create(text=text) 21 | await TestModel.update(text="Test update query").where(TestModel.id == obj1.id).aio_execute() 22 | 23 | obj2 = await TestModel.aio_get(id=obj1.id) 24 | assert obj2.text == "Test update query" 25 | 26 | 27 | @dbs_postgres 28 | async def test_update__returning_model(db: AioDatabase) -> None: 29 | await TestModel.aio_create(text="text1", data="data") 30 | await TestModel.aio_create(text="text2", data="data") 31 | new_data = "New_data" 32 | wrapper = await TestModel.update(data=new_data).where(TestModel.data == "data").returning(TestModel).aio_execute() 33 | 34 | result = [m.data for m in wrapper] 35 | assert [new_data, new_data] == result 36 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from typing import AsyncGenerator, Generator 4 | 5 | import pytest 6 | from peewee import sort_models 7 | 8 | from peewee_async.databases import AioDatabase 9 | from peewee_async.utils import aiopg, aiomysql, psycopg 10 | from tests.db_config import DB_CLASSES, DB_DEFAULTS 11 | from tests.models import ALL_MODELS 12 | 13 | 14 | @pytest.fixture 15 | def enable_debug_log_level() -> Generator[None, None, None]: 16 | logger = logging.getLogger('peewee.async') 17 | handler = logging.StreamHandler() 18 | logger.addHandler(handler) 19 | logger.setLevel(logging.DEBUG) 20 | 21 | yield 22 | 23 | logger.removeHandler(handler) 24 | logger.setLevel(logging.INFO) 25 | 26 | 27 | @pytest.fixture(scope="session", autouse=True) 28 | def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]: 29 | loop = asyncio.get_event_loop_policy().new_event_loop() 30 | yield loop 31 | loop.close() 32 | 33 | 34 | @pytest.fixture 35 | async def db(request: pytest.FixtureRequest) -> AsyncGenerator[AioDatabase, None]: 36 | db = request.param 37 | if db.startswith('postgres') and aiopg is None: 38 | pytest.skip("aiopg is not installed") 39 | if db.startswith('mysql') and aiomysql is None: 40 | pytest.skip("aiomysql is not installed") 41 | if db.startswith('psycopg') and psycopg is None: 42 | pytest.skip("psycopg is not installed") 43 | 44 | params = DB_DEFAULTS[db] 45 | database = DB_CLASSES[db](**params) 46 | 47 | with database.allow_sync(): 48 | for model in ALL_MODELS: 49 | model._meta.database = database 50 | model.create_table(True) 51 | 52 | yield database 53 | 54 | with database.allow_sync(): 55 | for model in reversed(sort_models(ALL_MODELS)): 56 | model.delete().execute() 57 | model._meta.database = None 58 | await database.aio_close() 59 | 60 | 61 | PG_DBS = [ 62 | "postgres-pool", 63 | "postgres-pool-ext", 64 | "psycopg-pool", 65 | ] 66 | 67 | MYSQL_DBS = ["mysql-pool"] 68 | 69 | 70 | dbs_mysql = pytest.mark.parametrize( 71 | "db", MYSQL_DBS, indirect=["db"] 72 | ) 73 | 74 | 75 | dbs_postgres = pytest.mark.parametrize( 76 | "db", PG_DBS, indirect=["db"] 77 | ) 78 | 79 | 80 | dbs_all = pytest.mark.parametrize( 81 | "db", PG_DBS + MYSQL_DBS, indirect=["db"] 82 | ) 83 | -------------------------------------------------------------------------------- /tests/db_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import peewee_async 3 | 4 | PG_DEFAULTS = { 5 | 'database': 'postgres', 6 | 'host': '127.0.0.1', 7 | 'port': int(os.environ.get('POSTGRES_PORT', 5432)), 8 | 'password': 'postgres', 9 | 'user': 'postgres', 10 | 'pool_params': { 11 | "minsize": 0, 12 | "maxsize": 5, 13 | "timeout": 30, 14 | 'pool_recycle': 1.5 15 | } 16 | } 17 | 18 | PSYCOPG_DEFAULTS = { 19 | 'database': 'postgres', 20 | 'host': '127.0.0.1', 21 | 'port': int(os.environ.get('POSTGRES_PORT', 5432)), 22 | 'password': 'postgres', 23 | 'user': 'postgres', 24 | 'pool_params': { 25 | "min_size": 0, 26 | "max_size": 5, 27 | 'max_lifetime': 15 28 | } 29 | } 30 | 31 | MYSQL_DEFAULTS = { 32 | 'database': 'mysql', 33 | 'host': '127.0.0.1', 34 | 'port': int(os.environ.get('MYSQL_PORT', 3306)), 35 | 'user': 'root', 36 | 'password': 'mysql', 37 | 'connect_timeout': 30, 38 | "pool_params": { 39 | "minsize": 0, 40 | "maxsize": 5, 41 | "pool_recycle": 2 42 | } 43 | } 44 | 45 | DB_DEFAULTS = { 46 | 'postgres-pool': PG_DEFAULTS, 47 | 'postgres-pool-ext': PG_DEFAULTS, 48 | 'psycopg-pool': PSYCOPG_DEFAULTS, 49 | 'mysql-pool': MYSQL_DEFAULTS 50 | } 51 | 52 | DB_CLASSES = { 53 | 'postgres-pool': peewee_async.PooledPostgresqlDatabase, 54 | 'postgres-pool-ext': peewee_async.PooledPostgresqlExtDatabase, 55 | 'psycopg-pool': peewee_async.PsycopgDatabase, 56 | 'mysql-pool': peewee_async.PooledMySQLDatabase 57 | } 58 | -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | import peewee 4 | import peewee_async 5 | 6 | 7 | class TestModel(peewee_async.AioModel): 8 | __test__ = False # disable pytest warnings 9 | text = peewee.CharField(max_length=100, unique=True) 10 | data = peewee.TextField(default='') 11 | 12 | def __str__(self) -> str: 13 | return '<%s id=%s> %s' % (self.__class__.__name__, self.id, self.text) 14 | 15 | 16 | class TestModelAlpha(peewee_async.AioModel): 17 | __test__ = False 18 | text = peewee.CharField() 19 | 20 | def __str__(self) -> str: 21 | return '<%s id=%s> %s' % (self.__class__.__name__, self.id, self.text) 22 | 23 | 24 | class TestModelBeta(peewee_async.AioModel): 25 | __test__ = False 26 | alpha = peewee.ForeignKeyField(TestModelAlpha, backref='betas') 27 | text = peewee.CharField() 28 | 29 | def __str__(self) -> str: 30 | return '<%s id=%s> %s' % (self.__class__.__name__, self.id, self.text) 31 | 32 | 33 | class TestModelGamma(peewee_async.AioModel): 34 | __test__ = False 35 | text = peewee.CharField() 36 | beta = peewee.ForeignKeyField(TestModelBeta, backref='gammas') 37 | 38 | def __str__(self) -> str: 39 | return '<%s id=%s> %s' % (self.__class__.__name__, self.id, self.text) 40 | 41 | 42 | class UUIDTestModel(peewee_async.AioModel): 43 | id = peewee.UUIDField(primary_key=True, default=uuid.uuid4) 44 | text = peewee.CharField() 45 | 46 | def __str__(self) -> str: 47 | return '<%s id=%s> %s' % (self.__class__.__name__, self.id, self.text) 48 | 49 | 50 | class CompositeTestModel(peewee_async.AioModel): 51 | """A simple "through" table for many-to-many relationship.""" 52 | task_id = peewee.IntegerField() 53 | product_type = peewee.CharField() 54 | 55 | class Meta: 56 | primary_key = peewee.CompositeKey('task_id', 'product_type') 57 | 58 | 59 | class IntegerTestModel(peewee_async.AioModel): 60 | __test__ = False # disable pytest warnings 61 | num = peewee.IntegerField() 62 | 63 | 64 | ALL_MODELS = ( 65 | TestModel, UUIDTestModel, TestModelAlpha, TestModelBeta, TestModelGamma, 66 | CompositeTestModel, IntegerTestModel 67 | ) 68 | -------------------------------------------------------------------------------- /tests/test_common.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import uuid 3 | from typing import Any, Dict, Type 4 | 5 | import peewee 6 | import pytest 7 | from pytest import LogCaptureFixture 8 | 9 | from peewee_async.databases import AioDatabase 10 | from tests.conftest import dbs_all 11 | from tests.db_config import DB_CLASSES, DB_DEFAULTS 12 | from tests.models import TestModel, CompositeTestModel 13 | 14 | 15 | @dbs_all 16 | async def test_composite_key(db: AioDatabase) -> None: 17 | task_id = 5 18 | product_type = "boots" 19 | comp = await CompositeTestModel.aio_create(task_id=task_id, product_type=product_type) 20 | assert comp.get_id() == (task_id, product_type) 21 | 22 | 23 | @dbs_all 24 | async def test_multiple_iterate_over_result(db: AioDatabase) -> None: 25 | 26 | obj1 = await TestModel.aio_create(text="Test 1") 27 | obj2 = await TestModel.aio_create(text="Test 2") 28 | 29 | result = await TestModel.select().order_by(TestModel.text).aio_execute() 30 | 31 | assert list(result) == [obj1, obj2] 32 | 33 | 34 | @dbs_all 35 | async def test_indexing_result(db: AioDatabase) -> None: 36 | 37 | await TestModel.aio_create(text="Test 1") 38 | obj = await TestModel.aio_create(text="Test 2") 39 | 40 | result = await TestModel.select().order_by(TestModel.text).aio_execute() 41 | assert obj == result[1] 42 | 43 | 44 | @pytest.mark.parametrize( 45 | "params, db_cls", 46 | [ 47 | (DB_DEFAULTS[name], db_cls) for name, db_cls in DB_CLASSES.items() 48 | ] 49 | ) 50 | async def test_proxy_database(params: Dict[str, Any], db_cls: Type[AioDatabase]) -> None: 51 | database = peewee.Proxy() 52 | TestModel._meta.database = database 53 | 54 | database.initialize(db_cls(**params)) 55 | 56 | with database.allow_sync(): 57 | TestModel.create_table(True) 58 | 59 | text = "Test %s" % uuid.uuid4() 60 | await TestModel.aio_create(text=text) 61 | await TestModel.aio_get(text=text) 62 | with database.allow_sync(): 63 | TestModel.drop_table(True) 64 | await database.aio_close() 65 | 66 | 67 | @dbs_all 68 | async def test_many_requests(db: AioDatabase) -> None: 69 | 70 | max_connections = getattr(dbs_all, 'max_connections', 1) 71 | text = "Test %s" % uuid.uuid4() 72 | obj = await TestModel.aio_create(text=text) 73 | n = 2 * max_connections # number of requests 74 | done, not_done = await asyncio.wait( 75 | {asyncio.create_task(TestModel.aio_get(id=obj.id)) for _ in range(n)} 76 | ) 77 | assert len(done) == n 78 | 79 | 80 | @dbs_all 81 | async def test_allow_sync(db: AioDatabase) -> None: 82 | with db.allow_sync(): 83 | TestModel.create(text="text") 84 | assert await TestModel.aio_get_or_none(text="text") is not None 85 | assert db.is_closed() is True 86 | 87 | 88 | @dbs_all 89 | async def test_allow_sync_is_reverted_for_exc(db: AioDatabase) -> None: 90 | try: 91 | with db.allow_sync(): 92 | ununique_text = "ununique_text" 93 | TestModel.create(text=ununique_text) 94 | TestModel.create(text=ununique_text) 95 | except peewee.IntegrityError: 96 | pass 97 | assert db._allow_sync is False 98 | 99 | 100 | @dbs_all 101 | async def test_logging(db: AioDatabase, caplog: LogCaptureFixture, enable_debug_log_level: None) -> None: 102 | 103 | await TestModel.aio_create(text="Test 1") 104 | 105 | assert 'INSERT INTO' in caplog.text 106 | assert 'testmodel' in caplog.text 107 | assert 'VALUES' in caplog.text 108 | -------------------------------------------------------------------------------- /tests/test_database.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | import pytest 4 | from peewee import OperationalError 5 | 6 | from peewee_async import connection_context 7 | from peewee_async.databases import AioDatabase 8 | from tests.conftest import dbs_all, MYSQL_DBS, PG_DBS, dbs_mysql 9 | from tests.db_config import DB_DEFAULTS, DB_CLASSES 10 | from tests.models import TestModel 11 | 12 | 13 | @dbs_all 14 | async def test_nested_connection(db: AioDatabase) -> None: 15 | async with db.aio_connection() as connection_1: 16 | async with connection_1.cursor() as cursor: 17 | await cursor.execute("SELECT 1") 18 | await TestModel.aio_get_or_none(id=5) 19 | async with db.aio_connection() as connection_2: 20 | assert connection_1 is connection_2 21 | _connection_context = connection_context.get() 22 | assert _connection_context is not None 23 | _connection = _connection_context.connection 24 | assert _connection is connection_2 25 | async with connection_2.cursor() as cursor: 26 | await cursor.execute("SELECT 1") 27 | assert connection_context.get() is None 28 | assert db.pool_backend.has_acquired_connections() is False 29 | 30 | 31 | @dbs_all 32 | async def test_db_should_connect_manually_after_close(db: AioDatabase) -> None: 33 | await TestModel.aio_create(text='test') 34 | 35 | await db.aio_close() 36 | with pytest.raises((RuntimeError, OperationalError)): 37 | await TestModel.aio_get_or_none(text='test') 38 | await db.aio_connect() 39 | 40 | assert await TestModel.aio_get_or_none(text='test') is not None 41 | 42 | 43 | @dbs_all 44 | async def test_is_connected(db: AioDatabase) -> None: 45 | assert db.is_connected is False 46 | 47 | await db.aio_connect() 48 | assert db.is_connected is True 49 | 50 | await db.aio_close() 51 | assert db.is_connected is False 52 | 53 | 54 | @dbs_all 55 | async def test_aio_close_idempotent(db: AioDatabase) -> None: 56 | assert db.is_connected is False 57 | 58 | await db.aio_close() 59 | assert db.is_connected is False 60 | 61 | await db.aio_close() 62 | assert db.is_connected is False 63 | 64 | 65 | @pytest.mark.parametrize('db_name', PG_DBS + MYSQL_DBS) 66 | async def test_deferred_init(db_name: str) -> None: 67 | database: AioDatabase = DB_CLASSES[db_name](None) 68 | 69 | with pytest.raises(Exception, match='Error, database must be initialized before creating a connection pool'): 70 | await database.aio_execute_sql(sql='SELECT 1;') 71 | 72 | db_params: Dict[str, Any] = DB_DEFAULTS[db_name] 73 | database.init(**db_params) 74 | 75 | await database.aio_execute_sql(sql='SELECT 1;') 76 | await database.aio_close() 77 | 78 | 79 | @pytest.mark.parametrize( 80 | 'db_name', 81 | [ 82 | "postgres-pool", 83 | "postgres-pool-ext", 84 | "mysql-pool" 85 | ] 86 | ) 87 | async def test_deprecated_min_max_connections_param(db_name: str) -> None: 88 | default_params = DB_DEFAULTS[db_name].copy() 89 | del default_params['pool_params'] 90 | default_params["min_connections"] = 1 91 | default_params["max_connections"] = 3 92 | db_cls = DB_CLASSES[db_name] 93 | database = db_cls(**default_params) 94 | await database.aio_connect() 95 | 96 | assert database.pool_backend.pool.minsize == 1 # type: ignore 97 | assert database.pool_backend.pool.maxsize == 3 # type: ignore 98 | 99 | await database.aio_close() 100 | 101 | 102 | @dbs_mysql 103 | async def test_mysql_params(db: AioDatabase) -> None: 104 | async with db.aio_connection() as connection_1: 105 | assert connection_1.autocommit_mode is True # type: ignore 106 | assert db.pool_backend.pool._recycle == 2 # type: ignore 107 | assert db.pool_backend.pool.minsize == 0 # type: ignore 108 | assert db.pool_backend.pool.maxsize == 5 # type: ignore 109 | 110 | 111 | @pytest.mark.parametrize( 112 | "db", 113 | ["postgres-pool"], indirect=["db"] 114 | ) 115 | async def test_pg_json_hstore__params(db: AioDatabase) -> None: 116 | await db.aio_connect() 117 | assert db.pool_backend.pool._enable_json is False # type: ignore 118 | assert db.pool_backend.pool._enable_hstore is False # type: ignore 119 | assert db.pool_backend.pool._timeout == 30 # type: ignore 120 | assert db.pool_backend.pool._recycle == 1.5 # type: ignore 121 | assert db.pool_backend.pool.minsize == 0 # type: ignore 122 | assert db.pool_backend.pool.maxsize == 5 # type: ignore 123 | 124 | 125 | @pytest.mark.parametrize( 126 | "db", 127 | ["postgres-pool-ext"], indirect=["db"] 128 | ) 129 | async def test_pg_ext_json_hstore__params(db: AioDatabase) -> None: 130 | await db.aio_connect() 131 | assert db.pool_backend.pool._enable_json is True # type: ignore 132 | assert db.pool_backend.pool._enable_hstore is False # type: ignore 133 | assert db.pool_backend.pool._timeout == 30 # type: ignore 134 | assert db.pool_backend.pool._recycle == 1.5 # type: ignore 135 | assert db.pool_backend.pool._recycle == 1.5 # type: ignore 136 | assert db.pool_backend.pool.minsize == 0 # type: ignore 137 | assert db.pool_backend.pool.maxsize == 5 # type: ignore 138 | 139 | 140 | @pytest.mark.parametrize( 141 | "db", 142 | ["psycopg-pool"], indirect=["db"] 143 | ) 144 | async def test_psycopg__params(db: AioDatabase) -> None: 145 | await db.aio_connect() 146 | assert db.pool_backend.pool.min_size == 0 # type: ignore 147 | assert db.pool_backend.pool.max_size == 5 # type: ignore 148 | assert db.pool_backend.pool.max_lifetime == 15 # type: ignore 149 | -------------------------------------------------------------------------------- /tests/test_transaction.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | from peewee import IntegrityError 5 | from pytest_mock import MockerFixture 6 | 7 | from peewee_async import Transaction 8 | from peewee_async.databases import AioDatabase 9 | from tests.conftest import dbs_all 10 | from tests.models import TestModel 11 | 12 | 13 | class FakeConnectionError(Exception): 14 | pass 15 | 16 | 17 | @dbs_all 18 | async def test_transaction_error_on_begin(db: AioDatabase, mocker: MockerFixture) -> None: 19 | mocker.patch.object(Transaction, "begin", side_effect=FakeConnectionError) 20 | with pytest.raises(FakeConnectionError): 21 | async with db.aio_atomic(): 22 | await TestModel.aio_create(text='FOO') 23 | assert db.pool_backend.has_acquired_connections() is False 24 | 25 | 26 | @dbs_all 27 | async def test_transaction_error_on_commit(db: AioDatabase, mocker: MockerFixture) -> None: 28 | mocker.patch.object(Transaction, "commit", side_effect=FakeConnectionError) 29 | with pytest.raises(FakeConnectionError): 30 | async with db.aio_atomic(): 31 | await TestModel.aio_create(text='FOO') 32 | assert db.pool_backend.has_acquired_connections() is False 33 | 34 | 35 | @dbs_all 36 | async def test_transaction_error_on_rollback(db: AioDatabase, mocker: MockerFixture) -> None: 37 | await TestModel.aio_create(text='FOO', data="") 38 | mocker.patch.object(Transaction, "rollback", side_effect=FakeConnectionError) 39 | with pytest.raises(FakeConnectionError): 40 | async with db.aio_atomic(): 41 | await TestModel.update(data="BAR").aio_execute() 42 | assert await TestModel.aio_get_or_none(data="BAR") is not None 43 | await TestModel.aio_create(text='FOO') 44 | 45 | assert db.pool_backend.has_acquired_connections() is False 46 | 47 | 48 | @dbs_all 49 | async def test_transaction_success(db: AioDatabase) -> None: 50 | async with db.aio_atomic(): 51 | await TestModel.aio_create(text='FOO') 52 | 53 | assert await TestModel.aio_get_or_none(text="FOO") is not None 54 | assert db.pool_backend.has_acquired_connections() is False 55 | 56 | 57 | @dbs_all 58 | async def test_transaction_rollback(db: AioDatabase) -> None: 59 | await TestModel.aio_create(text='FOO', data="") 60 | 61 | with pytest.raises(IntegrityError): 62 | async with db.aio_atomic(): 63 | await TestModel.update(data="BAR").aio_execute() 64 | assert await TestModel.aio_get_or_none(data="BAR") is not None 65 | await TestModel.aio_create(text='FOO') 66 | 67 | assert await TestModel.aio_get_or_none(data="BAR") is None 68 | assert db.pool_backend.has_acquired_connections() is False 69 | 70 | 71 | @dbs_all 72 | async def test_several_transactions(db: AioDatabase) -> None: 73 | """Run several transactions in parallel tasks. 74 | """ 75 | 76 | async def t1() -> None: 77 | async with db.aio_atomic(): 78 | await TestModel.aio_create(text='FOO1', data="") 79 | 80 | async def t2() -> None: 81 | async with db.aio_atomic(): 82 | await TestModel.aio_create(text='FOO2', data="") 83 | with pytest.raises(IntegrityError): 84 | async with db.aio_atomic(): 85 | await TestModel.aio_create(text='FOO2', data="not_created") 86 | 87 | async def t3() -> None: 88 | async with db.aio_atomic(): 89 | await TestModel.aio_create(text='FOO3', data="") 90 | async with db.aio_atomic(): 91 | await TestModel.update(data="BAR").where(TestModel.text == 'FOO3').aio_execute() 92 | 93 | await asyncio.gather(t1(), t2(), t3()) 94 | 95 | assert await TestModel.aio_get_or_none(text="FOO1") is not None 96 | assert await TestModel.aio_get_or_none(text="FOO2", data="") is not None 97 | assert await TestModel.aio_get_or_none(text="FOO3", data="BAR") is not None 98 | assert db.pool_backend.has_acquired_connections() is False 99 | 100 | 101 | @dbs_all 102 | async def test_transaction_manual_work(db: AioDatabase) -> None: 103 | async with db.aio_connection() as connection: 104 | tr = Transaction(connection) 105 | await tr.begin() 106 | await TestModel.aio_create(text='FOO') 107 | assert await TestModel.aio_get_or_none(text="FOO") is not None 108 | try: 109 | await TestModel.aio_create(text='FOO') 110 | except: 111 | await tr.rollback() 112 | else: 113 | await tr.commit() 114 | 115 | assert await TestModel.aio_get_or_none(text="FOO") is None 116 | assert db.pool_backend.has_acquired_connections() is False 117 | 118 | 119 | @dbs_all 120 | async def test_savepoint_success(db: AioDatabase) -> None: 121 | async with db.aio_atomic(): 122 | await TestModel.aio_create(text='FOO') 123 | 124 | async with db.aio_atomic(): 125 | await TestModel.update(text="BAR").aio_execute() 126 | 127 | assert await TestModel.aio_get_or_none(text="BAR") is not None 128 | assert db.pool_backend.has_acquired_connections() is False 129 | 130 | 131 | @dbs_all 132 | async def test_savepoint_rollback(db: AioDatabase) -> None: 133 | await TestModel.aio_create(text='FOO', data="") 134 | 135 | async with db.aio_atomic(): 136 | await TestModel.update(data="BAR").aio_execute() 137 | 138 | with pytest.raises(IntegrityError): 139 | async with db.aio_atomic(): 140 | await TestModel.aio_create(text='FOO') 141 | 142 | assert await TestModel.aio_get_or_none(data="BAR") is not None 143 | assert db.pool_backend.has_acquired_connections() is False 144 | 145 | 146 | @dbs_all 147 | async def test_savepoint_manual_work(db: AioDatabase) -> None: 148 | async with db.aio_connection() as connection: 149 | tr = Transaction(connection) 150 | await tr.begin() 151 | await TestModel.aio_create(text='FOO') 152 | assert await TestModel.aio_get_or_none(text="FOO") is not None 153 | 154 | savepoint = Transaction(connection, is_savepoint=True) 155 | await savepoint.begin() 156 | try: 157 | await TestModel.aio_create(text='FOO') 158 | except: 159 | await savepoint.rollback() 160 | else: 161 | await savepoint.commit() 162 | await tr.commit() 163 | 164 | assert await TestModel.aio_get_or_none(text="FOO") is not None 165 | assert db.pool_backend.has_acquired_connections() is False 166 | 167 | 168 | @dbs_all 169 | async def test_acid_when_connetion_has_been_broken(db: AioDatabase) -> None: 170 | async def restart_connections(event_for_lock: asyncio.Event) -> None: 171 | event_for_lock.set() 172 | await asyncio.sleep(0.05) 173 | 174 | # Using an event, we force tasks to wait until a certain coroutine 175 | # This is necessary to reproduce a case when connections reopened during transaction 176 | 177 | # Somebody decides to close all connections and open again 178 | event_for_lock.clear() 179 | 180 | await db.aio_close() 181 | await db.aio_connect() 182 | 183 | event_for_lock.set() 184 | return None 185 | 186 | async def insert_records(event_for_wait: asyncio.Event) -> None: 187 | await event_for_wait.wait() 188 | async with db.aio_atomic(): 189 | # BEGIN 190 | # INSERT 1 191 | await TestModel.aio_create(text="1") 192 | 193 | await asyncio.sleep(0.05) 194 | # wait for db close all connections and open again 195 | await event_for_wait.wait() 196 | 197 | # This row should not be inserted because the connection of the current transaction has been closed 198 | # # INSERT 2 199 | await TestModel.aio_create(text="2") 200 | 201 | return None 202 | 203 | event = asyncio.Event() 204 | 205 | await asyncio.gather( 206 | restart_connections(event), 207 | insert_records(event), 208 | return_exceptions=True, 209 | ) 210 | 211 | # The transaction has not been committed 212 | assert len(list(await TestModel.select().aio_execute())) in (0, 2) 213 | assert db.pool_backend.has_acquired_connections() is False 214 | 215 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any 2 | 3 | from peewee_async import AioModel 4 | 5 | 6 | def model_has_fields(model: AioModel, fields: Dict[str, Any]) -> bool: 7 | for field, value in fields.items(): 8 | if not getattr(model, field) == value: 9 | return False 10 | return True 11 | --------------------------------------------------------------------------------