├── mysql_tests ├── __init__.py ├── test_dialect.py ├── conftest.py ├── test_ext.py ├── test_bind.py ├── test_schema.py ├── test_execution_options.py ├── test_core.py ├── test_iterate.py ├── test_executemany.py ├── models.py └── test_bakery.py ├── src └── gino │ ├── ext │ ├── py.typed │ ├── __main__.py │ └── __init__.py │ ├── dialects │ └── __init__.py │ ├── exceptions.py │ ├── __init__.py │ ├── aiocontextvars.py │ └── strategies.py ├── tests ├── stub2.py ├── stub1.py ├── __init__.py ├── test_dialect.py ├── test_statement.py ├── conftest.py ├── test_prepared_stmt.py ├── test_bind.py ├── test_execution_options.py ├── test_schema.py ├── test_core.py ├── test_iterate.py ├── test_executemany.py ├── test_ext.py └── models.py ├── docs ├── .gitignore ├── reference │ ├── history.rst │ ├── api.rst │ ├── extensions.rst │ └── extensions │ │ ├── tornado.rst │ │ └── sanic.rst ├── theme │ ├── search.html │ ├── genindex.html │ ├── domainindex.html │ ├── theme.conf │ ├── page.html │ └── static │ │ ├── favicon.ico │ │ ├── images │ │ ├── OK.png │ │ ├── aha.png │ │ ├── hmm.png │ │ ├── fighting.png │ │ ├── language.svg │ │ ├── tutorials-icon.svg │ │ ├── icon-note.svg │ │ ├── box-bg-dec-2.svg │ │ ├── box-bg-dec.svg │ │ ├── icon-info.svg │ │ ├── box-bg-dec-3.svg │ │ ├── explanation-logo.svg │ │ ├── how-to-icon.svg │ │ ├── reference-logo.svg │ │ ├── icon-warning.svg │ │ └── icon-hint.svg │ │ └── js │ │ ├── documentation_options.js_t │ │ └── language_data.js_t ├── how-to │ ├── contributing.rst │ ├── crud.rst │ ├── pool.rst │ ├── alembic.rst │ ├── transaction.rst │ └── json-props.rst ├── images │ ├── docs.webp │ ├── engine.png │ ├── archlinux.webp │ ├── connection.png │ ├── python-gino.webp │ ├── happy-hacking.png │ ├── why_coroutine.png │ ├── why_multicore.png │ ├── why_throughput.png │ ├── exchangeratesapi.webp │ ├── why_single_task.png │ ├── why_multithreading.png │ ├── 263px-Minimum-Tonne.svg.png │ ├── pycharm.svg │ ├── explanation.svg │ ├── gino-fastapi-env.svg │ ├── community.svg │ ├── open-source.svg │ ├── gino-fastapi-poetry.svg │ ├── how-to.svg │ ├── python.svg │ ├── github.svg │ └── gino-fastapi-tests.svg ├── how-to.rst ├── reference.rst ├── explanation.rst ├── tutorials.rst ├── make.bat ├── Makefile ├── conf.py └── index.rst ├── pytest.ini ├── .codacy.yml ├── PATRONS.md ├── .coveragerc ├── .github ├── ISSUE_TEMPLATE │ ├── question.md │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── cd.yml ├── .editorconfig ├── SECURITY.md ├── .gitignore ├── LICENSE ├── AUTHORS.rst ├── Makefile ├── pyproject.toml ├── README.rst ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.rst /mysql_tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/gino/ext/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/stub2.py: -------------------------------------------------------------------------------- 1 | s2 = 222 2 | -------------------------------------------------------------------------------- /src/gino/dialects/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/stub1.py: -------------------------------------------------------------------------------- 1 | s1 = "111" 2 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | reference/api/ 2 | 3 | -------------------------------------------------------------------------------- /docs/reference/history.rst: -------------------------------------------------------------------------------- 1 | ../../HISTORY.rst -------------------------------------------------------------------------------- /docs/theme/search.html: -------------------------------------------------------------------------------- 1 |

search.html

2 | -------------------------------------------------------------------------------- /docs/how-to/contributing.rst: -------------------------------------------------------------------------------- 1 | ../../CONTRIBUTING.rst -------------------------------------------------------------------------------- /docs/theme/genindex.html: -------------------------------------------------------------------------------- 1 |

genindex.html

2 | -------------------------------------------------------------------------------- /docs/theme/domainindex.html: -------------------------------------------------------------------------------- 1 |

domainindex.html

2 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests mysql_tests 3 | -------------------------------------------------------------------------------- /docs/how-to/crud.rst: -------------------------------------------------------------------------------- 1 | ==== 2 | CRUD 3 | ==== 4 | 5 | **THIS IS A WIP** 6 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Unit test package for gino.""" 4 | -------------------------------------------------------------------------------- /.codacy.yml: -------------------------------------------------------------------------------- 1 | exclude_paths: 2 | - 'tests/**' 3 | - 'mysql_tests/**' 4 | - 'docs/**' 5 | -------------------------------------------------------------------------------- /docs/images/docs.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-gino/gino/HEAD/docs/images/docs.webp -------------------------------------------------------------------------------- /docs/images/engine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-gino/gino/HEAD/docs/images/engine.png -------------------------------------------------------------------------------- /docs/reference/api.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | .. toctree:: 5 | 6 | api/gino 7 | -------------------------------------------------------------------------------- /docs/theme/theme.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | inherit = none 3 | stylesheet = none 4 | pygments_style = monokai 5 | -------------------------------------------------------------------------------- /PATRONS.md: -------------------------------------------------------------------------------- 1 | Much thanks to these awesome patrons: 2 | 3 | * Sergey Bershadsky 4 | * Dima Veselov 5 | 6 | -------------------------------------------------------------------------------- /docs/how-to.rst: -------------------------------------------------------------------------------- 1 | How-to Guides 2 | ============= 3 | 4 | .. toctree:: 5 | :glob: 6 | 7 | how-to/* 8 | -------------------------------------------------------------------------------- /docs/images/archlinux.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-gino/gino/HEAD/docs/images/archlinux.webp -------------------------------------------------------------------------------- /docs/images/connection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-gino/gino/HEAD/docs/images/connection.png -------------------------------------------------------------------------------- /docs/images/python-gino.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-gino/gino/HEAD/docs/images/python-gino.webp -------------------------------------------------------------------------------- /docs/reference.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ========= 3 | 4 | .. toctree:: 5 | :glob: 6 | 7 | reference/* 8 | -------------------------------------------------------------------------------- /docs/images/happy-hacking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-gino/gino/HEAD/docs/images/happy-hacking.png -------------------------------------------------------------------------------- /docs/images/why_coroutine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-gino/gino/HEAD/docs/images/why_coroutine.png -------------------------------------------------------------------------------- /docs/images/why_multicore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-gino/gino/HEAD/docs/images/why_multicore.png -------------------------------------------------------------------------------- /docs/images/why_throughput.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-gino/gino/HEAD/docs/images/why_throughput.png -------------------------------------------------------------------------------- /docs/theme/page.html: -------------------------------------------------------------------------------- 1 | {%- extends "layout.html" %} 2 | {% block body %} 3 | {{ body }} 4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /docs/theme/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-gino/gino/HEAD/docs/theme/static/favicon.ico -------------------------------------------------------------------------------- /docs/explanation.rst: -------------------------------------------------------------------------------- 1 | Explanation 2 | =========== 3 | 4 | .. toctree:: 5 | :glob: 6 | 7 | explanation/* 8 | -------------------------------------------------------------------------------- /docs/images/exchangeratesapi.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-gino/gino/HEAD/docs/images/exchangeratesapi.webp -------------------------------------------------------------------------------- /docs/images/why_single_task.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-gino/gino/HEAD/docs/images/why_single_task.png -------------------------------------------------------------------------------- /docs/theme/static/images/OK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-gino/gino/HEAD/docs/theme/static/images/OK.png -------------------------------------------------------------------------------- /docs/theme/static/images/aha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-gino/gino/HEAD/docs/theme/static/images/aha.png -------------------------------------------------------------------------------- /docs/theme/static/images/hmm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-gino/gino/HEAD/docs/theme/static/images/hmm.png -------------------------------------------------------------------------------- /docs/images/why_multithreading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-gino/gino/HEAD/docs/images/why_multithreading.png -------------------------------------------------------------------------------- /docs/reference/extensions.rst: -------------------------------------------------------------------------------- 1 | Extensions 2 | ========== 3 | 4 | .. toctree:: 5 | :glob: 6 | 7 | extensions/* 8 | -------------------------------------------------------------------------------- /docs/reference/extensions/tornado.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Tornado Support 3 | =============== 4 | 5 | **THIS IS A WIP** 6 | -------------------------------------------------------------------------------- /docs/theme/static/images/fighting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-gino/gino/HEAD/docs/theme/static/images/fighting.png -------------------------------------------------------------------------------- /docs/images/263px-Minimum-Tonne.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-gino/gino/HEAD/docs/images/263px-Minimum-Tonne.svg.png -------------------------------------------------------------------------------- /docs/tutorials.rst: -------------------------------------------------------------------------------- 1 | Tutorials 2 | ========= 3 | 4 | .. toctree:: 5 | :glob: 6 | 7 | tutorials/announcement.rst 8 | tutorials/tutorial.rst 9 | tutorials/* 10 | -------------------------------------------------------------------------------- /tests/test_dialect.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from .models import Company 3 | 4 | pytestmark = pytest.mark.asyncio 5 | 6 | 7 | async def test_225_large_binary(bind): 8 | c = await Company.create(logo=b"SVG LOGO") 9 | assert c.logo == b"SVG LOGO" 10 | -------------------------------------------------------------------------------- /mysql_tests/test_dialect.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from .models import Company 3 | 4 | pytestmark = pytest.mark.asyncio 5 | 6 | 7 | async def test_225_large_binary(bind): 8 | c = await Company.create(logo=b"SVG LOGO") 9 | assert c.logo == b"SVG LOGO" 10 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = 3 | ./src 4 | omit = 5 | ./src/gino/aiocontextvars.py 6 | [report] 7 | exclude_lines = 8 | pragma: no cover 9 | Should not reach here 10 | def __repr__ 11 | raise NotImplementedError 12 | except ImportError 13 | if gino.__version__ >= '0.8.0': 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Please use GitHub Discussions Q&A instead 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | We are stopping to use GitHub Issues for questions, please go to the GitHub [Discussions Q&A](https://github.com/python-gino/gino/discussions?discussions_q=category%3AQ%26A) instead. 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | 23 | [.github/workflows/*.yml] 24 | indent_size = 2 25 | -------------------------------------------------------------------------------- /src/gino/exceptions.py: -------------------------------------------------------------------------------- 1 | class GinoException(Exception): 2 | pass 3 | 4 | 5 | class NoSuchRowError(GinoException): 6 | pass 7 | 8 | 9 | class UninitializedError(GinoException): 10 | pass 11 | 12 | 13 | class InitializedError(GinoException): 14 | pass 15 | 16 | 17 | class UnknownJSONPropertyError(GinoException): 18 | pass 19 | 20 | 21 | class MultipleResultsFound(GinoException): 22 | pass 23 | 24 | 25 | class NoResultFound(GinoException): 26 | pass 27 | -------------------------------------------------------------------------------- /docs/theme/static/js/documentation_options.js_t: -------------------------------------------------------------------------------- 1 | var DOCUMENTATION_OPTIONS = { 2 | URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), 3 | VERSION: '{{ release|e }}', 4 | LANGUAGE: '{{ language }}', 5 | COLLAPSE_INDEX: false, 6 | BUILDER: '{{ builder }}', 7 | FILE_SUFFIX: '{{ file_suffix }}', 8 | LINK_SUFFIX: '{{ link_suffix }}', 9 | HAS_SOURCE: {{ has_source|lower }}, 10 | SOURCELINK_SUFFIX: '{{ sourcelink_suffix }}', 11 | NAVIGATION_WITH_KEYS: {{ 'true' if theme_navigation_with_keys|tobool else 'false'}} 12 | }; 13 | -------------------------------------------------------------------------------- /tests/test_statement.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .models import PG_URL, User 4 | 5 | pytestmark = pytest.mark.asyncio 6 | 7 | 8 | async def test_anonymous(sa_engine): 9 | import gino 10 | 11 | e = await gino.create_engine(PG_URL, statement_cache_size=0) 12 | async with e.acquire() as conn: 13 | # noinspection PyProtectedMember 14 | assert conn.raw_connection._stmt_cache.get_max_size() == 0 15 | await conn.first(User.query.where(User.id == 1)) 16 | # anonymous statement should not be closed 17 | await conn.first(User.query.where(User.id == 1)) 18 | await e.close() 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /docs/theme/static/js/language_data.js_t: -------------------------------------------------------------------------------- 1 | /* 2 | * language_data.js 3 | * ~~~~~~~~~~~~~~~~ 4 | * 5 | * This script contains the language-specific data used by searchtools.js, 6 | * namely the list of stopwords, stemmer, scorer and splitter. 7 | * 8 | * :copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS. 9 | * :license: BSD, see LICENSE for details. 10 | * 11 | */ 12 | 13 | var stopwords = {{ search_language_stop_words }}; 14 | 15 | {% if search_language_stemming_code %} 16 | /* Non-minified version JS is _stemmer.js if file is provided */ {% endif -%} 17 | {{ search_language_stemming_code|safe }} 18 | 19 | {% if search_scorer_tool %} 20 | {{ search_scorer_tool|safe }} 21 | {% endif -%} 22 | 23 | {% if search_word_splitter_code %} 24 | {{ search_word_splitter_code }} 25 | {% endif -%} 26 | -------------------------------------------------------------------------------- /docs/how-to/pool.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Connection Pool 3 | =============== 4 | 5 | Other than the default connection pool, alternative pools can be used in 6 | their own use cases. 7 | There are options from dialects (currently only 8 | :class:`~gino.dialects.asyncpg.NullPool`), and users can define their own pools. 9 | The base class should be :class:`~gino.dialects.base.Pool`. 10 | 11 | To use non-default pools in raw GINO:: 12 | 13 | from gino.dialects.asyncpg import NullPool 14 | create_engine('postgresql://...', pool_class=NullPool) 15 | 16 | To use non-default pools in extensions (taking Sanic as an example):: 17 | 18 | from gino.dialects.asyncpg import NullPool 19 | from gino.ext.sanic import Gino 20 | 21 | app = sanic.Sanic() 22 | app.config.DB_HOST = 'localhost' 23 | app.config.DB_KWARGS = dict( 24 | pool_class=NullPool, 25 | ) 26 | db = Gino() 27 | db.init_app(app) 28 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | As of Dec 9, 2020: 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 2.0.x | :dart: Planned with breaking changes based on 1.4.x and SQLAlchemy 2.0 | 10 | | 1.4.x | :building_construction: Actively developing, next major upgrade with SQLAlchemy 1.4 | 11 | | 1.1.x | :building_construction: Actively developing, next Stable, 1.1.0-beta2 is released | 12 | | 1.0.x | :white_check_mark: Current Stable, security updates only | 13 | | 0.8.x | :x: a.k.a. 1.0-beta4, no longer supported since Oct 2020 | 14 | | < 0.8 | :x: Older versions are not supported | 15 | 16 | ## Reporting a Vulnerability 17 | 18 | Please report (suspected) security vulnerabilities to security@python-gino.org. 19 | You will receive a response from us within 48 hours. If the issue is confirmed, 20 | we will release a patch as soon as possible depending on complexity. 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Please provide a self-contained script to reproduce the bug if possible. 15 | ```python 16 | from gino import Gino 17 | 18 | db = Gino() 19 | 20 | async def main(): 21 | async with db.with_bind("postgresql:///"): 22 | ... 23 | ``` 24 | 25 | **Expected result** 26 | ``` 27 | The expected output or behavior of the script (when the bug is fixed). 28 | ``` 29 | 30 | **Actual result** 31 | ``` 32 | The actual output or behavior of the script (before a bugfix). 33 | ``` 34 | 35 | **Environment (please complete the following information):** 36 | - GINO: [e.g. 1.0.1] 37 | - SQLAlchemy: [e.g. 1.3.10] 38 | - Other: [e.g. Linux, macOS, PostgreSQL 9.6, asyncpg 0.18, aiomysql 0.0.20] 39 | 40 | **Additional context** 41 | Add any other context about the problem here. 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | .pytest_cache/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | # pyenv python configuration file 63 | .python-version 64 | 65 | # PyCharm 66 | .idea 67 | 68 | # Mac 69 | .DS_Store 70 | 71 | # VS Code 72 | .vscode 73 | 74 | # extension stub files 75 | src/gino/ext/*.pyi 76 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | 3 | import asyncpg 4 | import pytest 5 | import sqlalchemy 6 | 7 | import gino 8 | from .models import db, DB_ARGS, PG_URL, random_name 9 | 10 | ECHO = False 11 | 12 | 13 | @pytest.fixture(scope="module") 14 | def sa_engine(): 15 | rv = sqlalchemy.create_engine(PG_URL, echo=ECHO) 16 | db.create_all(rv) 17 | yield rv 18 | db.drop_all(rv) 19 | rv.dispose() 20 | 21 | 22 | @pytest.fixture 23 | async def engine(sa_engine): 24 | e = await gino.create_engine(PG_URL, echo=ECHO) 25 | yield e 26 | await e.close() 27 | sa_engine.execute("DELETE FROM gino_user_settings") 28 | sa_engine.execute("DELETE FROM gino_users") 29 | 30 | 31 | # noinspection PyUnusedLocal,PyShadowingNames 32 | @pytest.fixture 33 | async def bind(sa_engine): 34 | async with db.with_bind(PG_URL, echo=ECHO) as e: 35 | yield e 36 | sa_engine.execute("DELETE FROM gino_user_settings") 37 | sa_engine.execute("DELETE FROM gino_users") 38 | 39 | 40 | # noinspection PyUnusedLocal,PyShadowingNames 41 | @pytest.fixture 42 | async def asyncpg_pool(sa_engine): 43 | async with asyncpg.create_pool(**DB_ARGS) as rv: 44 | yield rv 45 | await rv.execute("DELETE FROM gino_user_settings") 46 | await rv.execute("DELETE FROM gino_users") 47 | 48 | 49 | @pytest.fixture 50 | def ssl_ctx(): 51 | ctx = ssl.create_default_context() 52 | ctx.check_hostname = False 53 | ctx.verify_mode = ssl.CERT_NONE 54 | return ctx 55 | -------------------------------------------------------------------------------- /docs/theme/static/images/language.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 4 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | BSD License 3 | 4 | Copyright (c) 2017-present, Fantix King 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, this 14 | list of conditions and the following disclaimer in the documentation and/or 15 | other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from this 19 | software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 24 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 25 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 26 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 28 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 29 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 30 | OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | -------------------------------------------------------------------------------- /src/gino/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from .api import Gino # NOQA 4 | from .bakery import Bakery 5 | from .engine import GinoEngine, GinoConnection # NOQA 6 | from .exceptions import * # NOQA 7 | from .strategies import GinoStrategy # NOQA 8 | 9 | rootlogger = logging.getLogger("gino") 10 | if rootlogger.level == logging.NOTSET: 11 | rootlogger.setLevel(logging.WARN) 12 | 13 | 14 | def create_engine(*args, **kwargs): 15 | """ 16 | Shortcut for :func:`sqlalchemy.create_engine` with ``strategy="gino"``. 17 | 18 | .. versionchanged:: 1.1 19 | Added the ``bakery`` keyword argument, please see :class:`~.bakery.Bakery`. 20 | 21 | .. versionchanged:: 1.1 22 | Added the ``prebake`` keyword argument to choose when to create the prepared 23 | statements for the queries in the bakery: 24 | 25 | * **Pre-bake** immediately when connected to the database (default). 26 | * No **pre-bake** but create prepared statements lazily when needed for the first 27 | time. 28 | 29 | Note: ``prebake`` has no effect in aiomysql 30 | """ 31 | 32 | from sqlalchemy import create_engine 33 | 34 | kwargs.setdefault("strategy", "gino") 35 | return create_engine(*args, **kwargs) 36 | 37 | 38 | def get_version(): 39 | """Get current GINO version.""" 40 | 41 | try: 42 | from importlib.metadata import version 43 | except ImportError: 44 | from importlib_metadata import version 45 | return version("gino") 46 | 47 | 48 | # noinspection PyBroadException 49 | try: 50 | __version__ = get_version() 51 | except Exception: 52 | pass 53 | -------------------------------------------------------------------------------- /mysql_tests/conftest.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | 3 | import aiomysql 4 | import pytest 5 | import sqlalchemy 6 | 7 | import gino 8 | from .models import db, DB_ARGS, MYSQL_URL, random_name 9 | 10 | ECHO = False 11 | 12 | 13 | @pytest.fixture(scope="module") 14 | def sa_engine(): 15 | rv = sqlalchemy.create_engine( 16 | MYSQL_URL.replace("mysql://", "mysql+pymysql://"), echo=ECHO 17 | ) 18 | db.create_all(rv) 19 | yield rv 20 | db.drop_all(rv) 21 | rv.dispose() 22 | 23 | 24 | @pytest.fixture 25 | async def engine(sa_engine): 26 | e = await gino.create_engine(MYSQL_URL, echo=ECHO, minsize=10) 27 | yield e 28 | await e.close() 29 | sa_engine.execute("DELETE FROM gino_user_settings") 30 | sa_engine.execute("DELETE FROM gino_users") 31 | 32 | 33 | # noinspection PyUnusedLocal,PyShadowingNames 34 | @pytest.fixture 35 | async def bind(sa_engine): 36 | async with db.with_bind(MYSQL_URL, echo=ECHO, minsize=10) as e: 37 | yield e 38 | sa_engine.execute("DELETE FROM gino_user_settings") 39 | sa_engine.execute("DELETE FROM gino_users") 40 | 41 | 42 | # noinspection PyUnusedLocal,PyShadowingNames 43 | @pytest.fixture 44 | async def aiomysql_pool(sa_engine): 45 | async with aiomysql.create_pool(**DB_ARGS) as rv: 46 | yield rv 47 | async with rv.acquire() as conn: 48 | await conn.query("DELETE FROM gino_user_settings") 49 | await conn.query("DELETE FROM gino_users") 50 | 51 | 52 | @pytest.fixture 53 | def ssl_ctx(): 54 | ctx = ssl.create_default_context() 55 | ctx.check_hostname = False 56 | ctx.verify_mode = ssl.CERT_NONE 57 | return ctx 58 | -------------------------------------------------------------------------------- /tests/test_prepared_stmt.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import pytest 4 | from .models import db, User 5 | 6 | pytestmark = pytest.mark.asyncio 7 | 8 | 9 | async def test_compiled_and_bindparam(bind): 10 | async with db.acquire() as conn: 11 | # noinspection PyArgumentList 12 | ins = await conn.prepare( 13 | User.insert().returning(*User).execution_options(loader=User) 14 | ) 15 | users = {} 16 | for name in "12345": 17 | u = await ins.first(name=name) 18 | assert u.nickname == name 19 | users[u.id] = u 20 | get = await conn.prepare(User.query.where(User.id == db.bindparam("uid"))) 21 | for key in users: 22 | u = await get.first(uid=key) 23 | assert u.nickname == users[key].nickname 24 | assert (await get.all(uid=key))[0].nickname == u.nickname 25 | 26 | assert await get.scalar(uid=-1) is None 27 | 28 | with pytest.raises(ValueError, match="does not support multiple"): 29 | await get.all([dict(uid=1), dict(uid=2)]) 30 | 31 | delete = await conn.prepare( 32 | User.delete.where(User.nickname == db.bindparam("name")) 33 | ) 34 | for name in "12345": 35 | msg = await delete.status(name=name) 36 | assert msg == "DELETE 1" 37 | 38 | 39 | async def test_statement(engine): 40 | async with engine.acquire() as conn: 41 | stmt = await conn.prepare("SELECT now()") 42 | last = None 43 | for i in range(5): 44 | now = await stmt.scalar() 45 | assert isinstance(now, datetime) 46 | assert last != now 47 | last = now 48 | -------------------------------------------------------------------------------- /docs/theme/static/images/tutorials-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= -D language='en' -A GAID='xxx' -A VERSION='refs/heads/master' 7 | TX ?= tx 8 | SPHINXBUILD ?= sphinx-build 9 | SPHINXINTL ?= sphinx-intl 10 | SPHINXAUTOBUILD ?= sphinx-autobuild 11 | SOURCEDIR = . 12 | BUILDDIR = _build 13 | LOC ?= 14 | PROJECT ?= 15 | 16 | # Put it first so that "make" without argument is like "make help". 17 | help: 18 | @echo " serve to run development server with auto-reload" 19 | @echo " update to prepate .pot files for PROJECT" 20 | @echo " push to push .pot files to Transifex" 21 | @echo " pull to pull .po files from Transifex for LOC or all" 22 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 23 | 24 | serve: 25 | @$(SPHINXAUTOBUILD) "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS) 26 | 27 | update: 28 | @$(SPHINXBUILD) -M gettext "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) 29 | @$(SPHINXINTL) update-txconfig-resources --pot-dir "$(BUILDDIR)/gettext" --transifex-project-name "$(PROJECT)" 30 | 31 | push: 32 | @$(TX) push -s 33 | 34 | pull: 35 | ifeq ($(LOC),) 36 | @$(TX) pull 37 | else 38 | @$(TX) pull -l "$(LOC)" 39 | endif 40 | 41 | clean: 42 | rm -r _build 43 | rm -r reference/api 44 | 45 | .PHONY: help serve update push pull clean Makefile 46 | 47 | # Catch-all target: route all unknown targets to Sphinx using the new 48 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 49 | %: Makefile 50 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 51 | -------------------------------------------------------------------------------- /docs/theme/static/images/icon-note.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 编组 2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * Fantix King 9 | 10 | Maintainers 11 | ----------- 12 | 13 | * Tony Wang 14 | 15 | Contributors 16 | ------------ 17 | 18 | * Neal Wang 19 | * Binghan Li 20 | * Vladimir Goncharov 21 | * Kinware 22 | * Kentoseth 23 | * Ádám Barancsuk 24 | * Sergey Kovalev 25 | * jonahfang 26 | * Yurii Shtrikker 27 | * Nicolas Crocfer 28 | * Denys Badzo 29 | * Pavol Vargovcik 30 | * Mykyta Holubakha 31 | * Jekel 32 | * Martin Zaťko 33 | * Pascal van Kooten 34 | * Michał Dziewulski 35 | * Simeon J Morgan 36 | * Julio Lacerda 37 | * qulaz 38 | * Jim O'Brien 39 | * Ilaï Deutel 40 | * Roald Storm 41 | * Tiago Requeijo 42 | * Olexiy 43 | * Galden 44 | * Iuliia Volkova 45 | * Roman Averchenkov 46 | * AustinPena 47 | * Yurii Karabas <1998uriyyo@gmail.com> 48 | 49 | 50 | Special thanks to my wife Daisy and her outsourcing company `DecentFoX Studio`_, 51 | for offering me the opportunity to build this project. We are open for global 52 | software project outsourcing on Python, iOS and Android development. 53 | 54 | .. _DecentFoX Studio: https://decentfox.com/ 55 | -------------------------------------------------------------------------------- /docs/theme/static/images/box-bg-dec-2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Path 135 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /mysql_tests/test_ext.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import importlib 3 | import sys 4 | import pytest 5 | 6 | 7 | def installed(): 8 | rv = 0 9 | for finder in sys.meta_path: 10 | if type(finder).__name__ == "_GinoExtensionCompatFinder": 11 | rv += 1 12 | return rv 13 | 14 | 15 | def test_install(): 16 | from gino import ext 17 | 18 | importlib.reload(ext) 19 | 20 | assert installed() == 1 21 | 22 | ext._GinoExtensionCompatFinder().install() 23 | assert installed() == 1 24 | 25 | ext._GinoExtensionCompatFinder.uninstall() 26 | assert not installed() 27 | 28 | ext._GinoExtensionCompatFinder().uninstall() 29 | assert not installed() 30 | 31 | ext._GinoExtensionCompatFinder().install() 32 | assert installed() == 1 33 | 34 | ext._GinoExtensionCompatFinder().install() 35 | assert installed() == 1 36 | 37 | 38 | def test_import(mocker): 39 | from gino import ext 40 | 41 | importlib.reload(ext) 42 | 43 | EntryPoint = collections.namedtuple("EntryPoint", ["name", "value"]) 44 | mocker.patch( 45 | "gino.ext.entry_points", 46 | new=lambda: { 47 | "gino.extensions": [ 48 | EntryPoint("demo", "tests.stub1"), 49 | EntryPoint("demo2", "tests.stub2"), 50 | ] 51 | }, 52 | ) 53 | ext._GinoExtensionCompatFinder().install() 54 | from gino.ext import demo 55 | 56 | assert sys.modules["tests.stub1"] is sys.modules["gino.ext.demo"] is demo 57 | 58 | from tests import stub2 59 | from gino.ext import demo2 60 | 61 | assert sys.modules["tests.stub2"] is sys.modules["gino.ext.demo2"] is demo2 is stub2 62 | 63 | 64 | def test_import_error(): 65 | with pytest.raises(ImportError, match="gino-nonexist"): 66 | # noinspection PyUnresolvedReferences 67 | from gino.ext import nonexist 68 | -------------------------------------------------------------------------------- /docs/theme/static/images/box-bg-dec.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Path 135 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/theme/static/images/icon-info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 编组 3 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean clean-test clean-pyc clean-build help 2 | .DEFAULT_GOAL := help 3 | define BROWSER_PYSCRIPT 4 | import os, webbrowser, sys 5 | try: 6 | from urllib import pathname2url 7 | except: 8 | from urllib.request import pathname2url 9 | 10 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 11 | endef 12 | export BROWSER_PYSCRIPT 13 | 14 | define PRINT_HELP_PYSCRIPT 15 | import re, sys 16 | 17 | for line in sys.stdin: 18 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 19 | if match: 20 | target, help = match.groups() 21 | print("%-20s %s" % (target, help)) 22 | endef 23 | export PRINT_HELP_PYSCRIPT 24 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 25 | 26 | help: 27 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 28 | 29 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts 30 | 31 | 32 | clean-build: ## remove build artifacts 33 | rm -fr build/ 34 | rm -fr dist/ 35 | rm -fr .eggs/ 36 | find . -name '*.egg-info' -exec rm -fr {} + 37 | find . -name '*.egg' -exec rm -f {} + 38 | 39 | clean-pyc: ## remove Python file artifacts 40 | find . -name '*.pyc' -exec rm -f {} + 41 | find . -name '*.pyo' -exec rm -f {} + 42 | find . -name '*~' -exec rm -f {} + 43 | find . -name '__pycache__' -exec rm -fr {} + 44 | 45 | clean-test: ## remove test and coverage artifacts 46 | rm -f .coverage 47 | rm -fr htmlcov/ 48 | 49 | lint: ## check style with black 50 | black --check src 51 | 52 | test: lint ## run tests quickly with the default Python 53 | pytest --cov --cov-fail-under=95 --no-cov-on-fail 54 | 55 | coverage: ## check code coverage quickly with the default Python 56 | pytest --cov --cov-report html 57 | $(BROWSER) htmlcov/index.html 58 | 59 | release: clean build ## package and upload a release 60 | poetry publish 61 | 62 | dist: clean ## builds source and wheel package 63 | poetry build 64 | ls -l dist 65 | 66 | install: clean ## install the package to the active Python's site-packages 67 | poetry install -E pg -E mysql 68 | -------------------------------------------------------------------------------- /tests/test_bind.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | import pytest 4 | from gino.exceptions import UninitializedError 5 | from sqlalchemy.engine.url import make_url 6 | 7 | from .models import db, PG_URL, User 8 | 9 | pytestmark = pytest.mark.asyncio 10 | 11 | 12 | # noinspection PyUnusedLocal 13 | async def test_create(bind): 14 | nickname = "test_create_{}".format(random.random()) 15 | u = await User.create(nickname=nickname) 16 | assert u.id is not None 17 | assert u.nickname == nickname 18 | return u 19 | 20 | 21 | async def test_get(bind): 22 | u1 = await test_create(bind) 23 | u2 = await User.get(u1.id) 24 | assert u1.id == u2.id 25 | assert u1.nickname == u2.nickname 26 | assert u1 is not u2 27 | 28 | 29 | # noinspection PyUnusedLocal 30 | async def test_unbind(asyncpg_pool): 31 | await db.set_bind(PG_URL) 32 | await test_create(None) 33 | await db.pop_bind().close() 34 | db.bind = None 35 | with pytest.raises(UninitializedError): 36 | await test_create(None) 37 | # test proper exception when engine is not initialized 38 | with pytest.raises(UninitializedError): 39 | db.bind.first = lambda x: 1 40 | 41 | 42 | async def test_db_api(bind, random_name): 43 | assert ( 44 | await db.scalar(User.insert().values(name=random_name).returning(User.nickname)) 45 | == random_name 46 | ) 47 | assert ( 48 | await db.first(User.query.where(User.nickname == random_name)) 49 | ).nickname == random_name 50 | assert len(await db.all(User.query.where(User.nickname == random_name))) == 1 51 | assert (await db.status(User.delete.where(User.nickname == random_name)))[ 52 | 0 53 | ] == "DELETE 1" 54 | stmt, params = db.compile(User.query.where(User.id == 3)) 55 | assert params[0] == 3 56 | 57 | 58 | async def test_bind_url(): 59 | url = make_url(PG_URL) 60 | assert url.drivername == "postgresql" 61 | await db.set_bind(PG_URL) 62 | assert url.drivername == "postgresql" 63 | await db.pop_bind().close() 64 | -------------------------------------------------------------------------------- /mysql_tests/test_bind.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | import pytest 4 | from gino.exceptions import UninitializedError 5 | from sqlalchemy.engine.url import make_url 6 | 7 | from .models import db, MYSQL_URL, User 8 | 9 | pytestmark = pytest.mark.asyncio 10 | 11 | 12 | # noinspection PyUnusedLocal 13 | async def test_create(bind): 14 | nickname = "test_create_{}".format(random.random()) 15 | u = await User.create(nickname=nickname) 16 | assert u.id is not None 17 | assert u.nickname == nickname 18 | return u 19 | 20 | 21 | async def test_get(bind): 22 | u1 = await test_create(bind) 23 | u2 = await User.get(u1.id) 24 | assert u1.id == u2.id 25 | assert u1.nickname == u2.nickname 26 | assert u1 is not u2 27 | 28 | 29 | # noinspection PyUnusedLocal 30 | async def test_unbind(aiomysql_pool): 31 | await db.set_bind(MYSQL_URL) 32 | await test_create(None) 33 | await db.pop_bind().close() 34 | db.bind = None 35 | with pytest.raises(UninitializedError): 36 | await test_create(None) 37 | # test proper exception when engine is not initialized 38 | with pytest.raises(UninitializedError): 39 | db.bind.first = lambda x: 1 40 | 41 | 42 | async def test_db_api(bind, random_name): 43 | result = await db.first(User.insert().values(name=random_name)) 44 | assert result is None 45 | r = await db.scalar(User.select('nickname').where(User.nickname == random_name)) 46 | assert r == random_name 47 | assert ( 48 | await db.first(User.query.where(User.nickname == random_name)) 49 | ).nickname == random_name 50 | assert len(await db.all(User.query.where(User.nickname == random_name))) == 1 51 | assert (await db.status(User.delete.where(User.nickname == random_name)))[ 52 | 0 53 | ] == 1 54 | stmt, params = db.compile(User.query.where(User.id == 3)) 55 | assert params[0] == 3 56 | 57 | 58 | async def test_bind_url(): 59 | url = make_url(MYSQL_URL) 60 | assert url.drivername == "mysql" 61 | await db.set_bind(MYSQL_URL) 62 | assert url.drivername == "mysql" 63 | await db.pop_bind().close() 64 | -------------------------------------------------------------------------------- /docs/theme/static/images/box-bg-dec-3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Path 10 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/theme/static/images/explanation-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 编组 6 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /mysql_tests/test_schema.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | import pytest 4 | 5 | import gino 6 | from gino.dialects.aiomysql import AsyncEnum 7 | 8 | pytestmark = pytest.mark.asyncio 9 | db = gino.Gino() 10 | 11 | 12 | class MyEnum(Enum): 13 | ONE = "one" 14 | TWO = "two" 15 | 16 | 17 | class Blog(db.Model): 18 | __tablename__ = "s_blog" 19 | 20 | id = db.Column(db.BigInteger(), primary_key=True) 21 | title = db.Column(db.Unicode(255), index=True, comment="Title Comment") 22 | visits = db.Column(db.BigInteger(), default=0) 23 | comment_id = db.Column(db.ForeignKey("s_comment.id")) 24 | number = db.Column(db.Enum(MyEnum), nullable=False, default=MyEnum.TWO) 25 | number2 = db.Column(AsyncEnum(MyEnum), nullable=False, default=MyEnum.TWO) 26 | 27 | 28 | class Comment(db.Model): 29 | __tablename__ = "s_comment" 30 | 31 | id = db.Column(db.BigInteger(), primary_key=True) 32 | blog_id = db.Column(db.ForeignKey("s_blog.id", name="blog_id_fk")) 33 | 34 | 35 | blog_seq = db.Sequence("blog_seq", metadata=db, schema="schema_test") 36 | 37 | 38 | async def test(engine, define=True): 39 | async with engine.acquire() as conn: 40 | assert not await engine.dialect.has_table(conn, "non_exist") 41 | Blog.__table__.comment = "Blog Comment" 42 | db.bind = engine 43 | await db.gino.create_all() 44 | await Blog.number.type.create_async(engine, checkfirst=True) 45 | await Blog.number2.type.create_async(engine, checkfirst=True) 46 | await db.gino.create_all(tables=[Blog.__table__], checkfirst=True) 47 | await blog_seq.gino.create(checkfirst=True) 48 | await Blog.__table__.gino.create(checkfirst=True) 49 | await db.gino.drop_all() 50 | await db.gino.drop_all(tables=[Blog.__table__], checkfirst=True) 51 | await Blog.__table__.gino.drop(checkfirst=True) 52 | await blog_seq.gino.drop(checkfirst=True) 53 | 54 | if define: 55 | 56 | class Comment2(db.Model): 57 | __tablename__ = "s_comment_2" 58 | 59 | id = db.Column(db.BigInteger(), primary_key=True) 60 | blog_id = db.Column(db.ForeignKey("s_blog.id")) 61 | 62 | await db.gino.create_all() 63 | await db.gino.drop_all() 64 | -------------------------------------------------------------------------------- /docs/theme/static/images/how-to-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/gino/aiocontextvars.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import contextvars 3 | import sys 4 | import types 5 | 6 | 7 | def patch_asyncio(): 8 | """Patches asyncio to support :mod:`contextvars`. 9 | 10 | This is automatically called when :mod:`gino` is imported. If Python version is 3.7 11 | or greater, this function is a no-op. 12 | """ 13 | 14 | if not sys.version_info < (3, 7): 15 | return 16 | 17 | def _get_context(): 18 | state = _get_state() 19 | ctx = getattr(state, "context", None) 20 | if ctx is None: 21 | ctx = contextvars.Context() 22 | state.context = ctx 23 | return ctx 24 | 25 | def _set_context(ctx): 26 | state = _get_state() 27 | state.context = ctx 28 | 29 | def _get_state(): 30 | loop = asyncio._get_running_loop() 31 | if loop is None: 32 | return contextvars._state 33 | task = asyncio.Task.current_task(loop=loop) 34 | return contextvars._state if task is None else task 35 | 36 | contextvars._get_context = _get_context 37 | contextvars._set_context = _set_context 38 | 39 | def create_task(loop, coro): 40 | task = loop._orig_create_task(coro) 41 | if task._source_traceback: 42 | del task._source_traceback[-1] 43 | task.context = contextvars.copy_context() 44 | return task 45 | 46 | def _patch_loop(loop): 47 | if loop and not hasattr(loop, "_orig_create_task"): 48 | loop._orig_create_task = loop.create_task 49 | loop.create_task = types.MethodType(create_task, loop) 50 | return loop 51 | 52 | def get_event_loop(): 53 | return _patch_loop(_get_event_loop()) 54 | 55 | def set_event_loop(loop): 56 | return _set_event_loop(_patch_loop(loop)) 57 | 58 | def new_event_loop(): 59 | return _patch_loop(_new_event_loop()) 60 | 61 | _get_event_loop = asyncio.get_event_loop 62 | _set_event_loop = asyncio.set_event_loop 63 | _new_event_loop = asyncio.new_event_loop 64 | 65 | asyncio.get_event_loop = asyncio.events.get_event_loop = get_event_loop 66 | asyncio.set_event_loop = asyncio.events.set_event_loop = set_event_loop 67 | asyncio.new_event_loop = asyncio.events.new_event_loop = new_event_loop 68 | -------------------------------------------------------------------------------- /src/gino/ext/__main__.py: -------------------------------------------------------------------------------- 1 | """Generate typing stubs for extensions. 2 | 3 | $ python -m gino.ext 4 | 5 | """ 6 | import sys 7 | import os 8 | 9 | try: 10 | from importlib.metadata import entry_points 11 | except ImportError: 12 | from importlib_metadata import entry_points 13 | 14 | 15 | if __name__ == "__main__": 16 | base_dir = os.path.dirname(os.path.abspath(__file__)) 17 | cmd = sys.argv[1] if len(sys.argv) == 2 else "" 18 | eps = list(entry_points().get("gino.extensions", [])) 19 | 20 | if cmd == "stub": 21 | added = False 22 | for ep in eps: 23 | path = os.path.join(base_dir, ep.name + ".pyi") 24 | if not os.path.exists(path): 25 | added = True 26 | print("Adding " + path) 27 | with open(path, "w") as f: 28 | f.write("from " + ep.value + " import *") 29 | if not added: 30 | print("Stub files are up to date.") 31 | 32 | elif cmd == "clean": 33 | removed = False 34 | for filename in os.listdir(base_dir): 35 | if filename.endswith(".pyi"): 36 | removed = True 37 | path = os.path.join(base_dir, filename) 38 | print("Removing " + path) 39 | os.remove(path) 40 | if not removed: 41 | print("No stub files found.") 42 | 43 | elif cmd == "list": 44 | if eps: 45 | name_size = max(len(ep.name) for ep in eps) 46 | value_size = max(len(ep.value) for ep in eps) 47 | for ep in eps: 48 | path = os.path.join(base_dir, ep.name + ".pyi") 49 | if not os.path.exists(path): 50 | path = "no stub file" 51 | print( 52 | "%s -> gino.ext.%s (%s)" 53 | % (ep.value.ljust(value_size), ep.name.ljust(name_size), path) 54 | ) 55 | 56 | else: 57 | print("Manages GINO extensions:") 58 | print() 59 | print(" python -m gino.ext COMMAND") 60 | print() 61 | print("Available commands:") 62 | print() 63 | print(" stub Generate gino/ext/*.pyi stub files for type checking.") 64 | print(" clean Remove the generated stub files.") 65 | print(" list List installed GINO extensions.") 66 | -------------------------------------------------------------------------------- /docs/theme/static/images/reference-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 2 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/images/pycharm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 11 | 14 | 15 | 16 | 17 | 19 | 21 | 24 | 26 | 30 | 32 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /mysql_tests/test_execution_options.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | from .models import db, User, UserType 6 | 7 | pytestmark = pytest.mark.asyncio 8 | 9 | 10 | async def test(bind): 11 | await User.create(nickname="test") 12 | assert isinstance(await User.query.gino.first(), User) 13 | bind.update_execution_options(return_model=False) 14 | assert not isinstance(await User.query.gino.first(), User) 15 | async with db.acquire() as conn: 16 | assert isinstance( 17 | await conn.execution_options(return_model=True).first(User.query), User 18 | ) 19 | assert not isinstance( 20 | await User.query.execution_options(return_model=False).gino.first(), User 21 | ) 22 | assert isinstance( 23 | await User.query.execution_options(return_model=True).gino.first(), User 24 | ) 25 | assert not isinstance(await User.query.gino.first(), User) 26 | bind.update_execution_options(return_model=True) 27 | assert isinstance(await User.query.gino.first(), User) 28 | 29 | 30 | # noinspection PyProtectedMember 31 | async def test_compiled_first_not_found(bind): 32 | async with bind.acquire() as conn: 33 | with pytest.raises(LookupError, match="No such execution option"): 34 | result = conn._execute("SELECT NOW()", (), {}) 35 | result.context._compiled_first_opt("nonexist") 36 | 37 | 38 | # noinspection PyUnusedLocal 39 | async def test_query_ext(bind): 40 | q = User.query 41 | assert q.gino.query is q 42 | 43 | u = await User.create(nickname="test") 44 | assert isinstance(await User.query.gino.first(), User) 45 | 46 | row = await User.query.gino.return_model(False).first() 47 | assert not isinstance(row, User) 48 | assert row == ( 49 | u.id, 50 | "test", 51 | {"age": 18, "birthday": "1970-01-01T00:00:00.000000"}, 52 | UserType.USER, 53 | None, 54 | ) 55 | 56 | row = await User.query.gino.model(None).first() 57 | assert not isinstance(row, User) 58 | assert row == ( 59 | u.id, 60 | "test", 61 | {"age": 18, "birthday": "1970-01-01T00:00:00.000000"}, 62 | UserType.USER, 63 | None, 64 | ) 65 | 66 | row = await db.select([User.id, User.nickname, User.type]).gino.first() 67 | assert not isinstance(row, User) 68 | assert row == (u.id, "test", UserType.USER) 69 | 70 | user = await db.select([User.id, User.nickname, User.type]).gino.model(User).first() 71 | assert isinstance(user, User) 72 | assert user.id is not None 73 | assert user.nickname == "test" 74 | assert user.type == UserType.USER 75 | 76 | with pytest.raises(asyncio.TimeoutError): 77 | await db.select([db.func.SLEEP(1), User.id]).gino.timeout(0.1).status() 78 | -------------------------------------------------------------------------------- /tests/test_execution_options.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | from .models import db, User, UserType 6 | 7 | pytestmark = pytest.mark.asyncio 8 | 9 | 10 | async def test(bind): 11 | await User.create(nickname="test") 12 | assert isinstance(await User.query.gino.first(), User) 13 | bind.update_execution_options(return_model=False) 14 | assert not isinstance(await User.query.gino.first(), User) 15 | async with db.acquire() as conn: 16 | assert isinstance( 17 | await conn.execution_options(return_model=True).first(User.query), User 18 | ) 19 | assert not isinstance( 20 | await User.query.execution_options(return_model=False).gino.first(), User 21 | ) 22 | assert isinstance( 23 | await User.query.execution_options(return_model=True).gino.first(), User 24 | ) 25 | assert not isinstance(await User.query.gino.first(), User) 26 | bind.update_execution_options(return_model=True) 27 | assert isinstance(await User.query.gino.first(), User) 28 | 29 | 30 | # noinspection PyProtectedMember 31 | async def test_compiled_first_not_found(bind): 32 | async with bind.acquire() as conn: 33 | with pytest.raises(LookupError, match="No such execution option"): 34 | result = conn._execute("SELECT NOW()", (), {}) 35 | result.context._compiled_first_opt("nonexist") 36 | 37 | 38 | # noinspection PyUnusedLocal 39 | async def test_query_ext(bind): 40 | q = User.query 41 | assert q.gino.query is q 42 | 43 | u = await User.create(nickname="test") 44 | assert isinstance(await User.query.gino.first(), User) 45 | 46 | row = await User.query.gino.return_model(False).first() 47 | assert not isinstance(row, User) 48 | assert row == ( 49 | u.id, 50 | "test", 51 | {"age": 18, "birthday": "1970-01-01T00:00:00.000000"}, 52 | {"height": 170}, 53 | UserType.USER, 54 | None, 55 | ) 56 | 57 | row = await User.query.gino.model(None).first() 58 | assert not isinstance(row, User) 59 | assert row == ( 60 | u.id, 61 | "test", 62 | {"age": 18, "birthday": "1970-01-01T00:00:00.000000"}, 63 | {"height": 170}, 64 | UserType.USER, 65 | None, 66 | ) 67 | 68 | row = await db.select([User.id, User.nickname, User.type]).gino.first() 69 | assert not isinstance(row, User) 70 | assert row == (u.id, "test", UserType.USER) 71 | 72 | user = await db.select([User.id, User.nickname, User.type]).gino.model(User).first() 73 | assert isinstance(user, User) 74 | assert user.id is not None 75 | assert user.nickname == "test" 76 | assert user.type == UserType.USER 77 | 78 | with pytest.raises(asyncio.TimeoutError): 79 | await db.select([db.func.pg_sleep(1), User.id]).gino.timeout(0.1).status() 80 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "gino" 3 | version = "1.1.0-rc.1" 4 | description = "GINO Is Not ORM - a Python asyncio ORM on SQLAlchemy core." 5 | license = "BSD-3-Clause" 6 | authors = ["Fantix King "] 7 | maintainers = ["Tony Wang ", "Fantix King "] 8 | readme = "README.rst" 9 | homepage = "https://python-gino.org" 10 | repository = "https://github.com/python-gino/gino" 11 | documentation = "https://python-gino.org/docs/" 12 | keywords = ["orm", "asyncio", "sqlalchemy", "asyncpg", "python3"] 13 | classifiers = [ 14 | "Development Status :: 5 - Production/Stable", 15 | "Intended Audience :: Developers", 16 | "License :: OSI Approved :: BSD License", 17 | "Natural Language :: English", 18 | "Programming Language :: Python :: 3", 19 | "Programming Language :: Python :: 3.6", 20 | "Programming Language :: Python :: 3.7", 21 | "Programming Language :: Python :: 3.8", 22 | "Programming Language :: Python :: 3.9", 23 | ] 24 | 25 | [tool.poetry.dependencies] 26 | python = "^3.6" 27 | SQLAlchemy = ">=1.3,<1.4" 28 | 29 | # drivers 30 | asyncpg = { version = ">=0.18,<1.0", optional = true } 31 | aiomysql = "^0.0.22" 32 | 33 | # compatibility 34 | contextvars = { version = "^2.4", python = "<3.7" } 35 | importlib_metadata = { version = "^2.0.0", python = "<3.8" } 36 | 37 | # extensions 38 | gino-starlette = { version = "^0.1.1", optional = true, python = "^3.6" } 39 | gino-aiohttp = {version = "^0.2.0", optional = true, python = "^3.6"} 40 | gino-tornado = { version = "^0.1.0", optional = true, python = "^3.5.2" } 41 | gino-sanic = { version = "^0.1.0", optional = true, python = "^3.6" } 42 | gino-quart = { version = "^0.1.0", optional = true, python = "^3.7" } 43 | 44 | [tool.poetry.extras] 45 | postgresql = ["asyncpg"] 46 | postgres = ["asyncpg"] 47 | pg = ["asyncpg"] 48 | asyncpg = ["asyncpg"] 49 | mysql = ["aiomysql"] 50 | aiomysql = ["aiomysql"] 51 | starlette = ["gino-starlette"] 52 | aiohttp = ["gino-aiohttp"] 53 | tornado = ["gino-tornado"] 54 | sanic = ["gino-sanic"] 55 | quart = ["gino-quart"] 56 | 57 | [tool.poetry.dev-dependencies] 58 | psycopg2-binary = "^2.9.3" 59 | click = "^8.0.3" 60 | 61 | # tests 62 | pytest = "^7.0.1" 63 | pytest-asyncio = "^0.16.0" 64 | pytest-mock = "^3.6.0" 65 | pytest-cov = "^3.0.0" 66 | black = { version = "^22.1.0", python = ">=3.6.2" } 67 | mypy = "^0.931" 68 | 69 | # docs 70 | sphinx = "^4.3.0" 71 | sphinx-rtd-theme = "^1.0.0" 72 | sphinxcontrib-apidoc = "^0.3.0" 73 | sphinx-autobuild = "^2021.3.14" 74 | sphinx-intl = {extras = ["transifex"], version = "^2.0.1"} 75 | 76 | [tool.poetry.plugins."sqlalchemy.dialects"] 77 | "postgresql.asyncpg" = "gino.dialects.asyncpg:AsyncpgDialect" 78 | "asyncpg" = "gino.dialects.asyncpg:AsyncpgDialect" 79 | "mysql.aiomysql" = "gino.dialects.aiomysql:AiomysqlDialect" 80 | "aiomysql" = "gino.dialects.aiomysql:AiomysqlDialect" 81 | 82 | [build-system] 83 | requires = ["poetry>=1.0"] 84 | build-backend = "poetry.masonry.api" 85 | -------------------------------------------------------------------------------- /src/gino/ext/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Namespace package for GINO extensions. 3 | 4 | This namespace package didn't use any of the `3 official solutions 5 | `__. Instead, 6 | ``gino.ext`` adds a hook into :data:`sys.meta_path`, and utilize the :ref:`entry-points` 7 | to find the extensions. 8 | 9 | Any GINO extension package should provide an entry point like this:: 10 | 11 | [gino.extensions] 12 | starlette = gino_starlette 13 | 14 | So that the Python package ``gino_starlette`` will also be importable through 15 | ``gino.ext.starlette``. 16 | """ 17 | 18 | import sys 19 | from importlib.abc import MetaPathFinder, Loader 20 | from importlib.machinery import ModuleSpec 21 | from importlib.util import find_spec 22 | 23 | try: 24 | from importlib.metadata import entry_points 25 | except ImportError: 26 | from importlib_metadata import entry_points 27 | 28 | 29 | class _GinoExtensionCompatProxyLoader(Loader): 30 | def __init__(self, fullname, loader): 31 | self._fullname = fullname 32 | self._loader = loader 33 | 34 | def create_module(self, spec): 35 | return self._loader.create_module(spec) 36 | 37 | def exec_module(self, mod): 38 | sys.modules[self._fullname] = mod 39 | return self._loader.exec_module(mod) 40 | 41 | 42 | class _GinoExtensionCompatNoopLoader(Loader): 43 | def __init__(self, mod): 44 | self._mod = mod 45 | 46 | def create_module(self, spec): 47 | return self._mod 48 | 49 | def exec_module(self, mod): 50 | pass 51 | 52 | 53 | class _GinoExtensionCompatFinder(MetaPathFinder): 54 | def __init__(self): 55 | self._redirects = { 56 | __name__ + "." + ep.name: ep.value 57 | for ep in entry_points().get("gino.extensions", []) 58 | } 59 | 60 | # noinspection PyUnusedLocal 61 | def find_spec(self, fullname, path, target=None): 62 | target = self._redirects.get(fullname) 63 | if target: 64 | mod = sys.modules.get(target) 65 | if mod is None: 66 | spec = find_spec(target) 67 | spec.loader = _GinoExtensionCompatProxyLoader(fullname, spec.loader) 68 | return spec 69 | else: 70 | return ModuleSpec(fullname, _GinoExtensionCompatNoopLoader(mod)) 71 | elif fullname.startswith(__name__): 72 | raise ImportError( 73 | "Cannot import {} - is gino-{} a valid extension and installed?".format( 74 | fullname, fullname[len(__name__) + 1 :] 75 | ) 76 | ) 77 | 78 | @classmethod 79 | def uninstall(cls): 80 | if sys.meta_path: 81 | for i in range(len(sys.meta_path) - 1, -1, -1): 82 | if type(sys.meta_path[i]).__name__ == cls.__name__: 83 | del sys.meta_path[i] 84 | 85 | def install(self): 86 | self.uninstall() 87 | sys.meta_path.append(self) 88 | 89 | 90 | _GinoExtensionCompatFinder().install() 91 | -------------------------------------------------------------------------------- /src/gino/strategies.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from copy import copy 3 | 4 | from sqlalchemy.engine import url 5 | from sqlalchemy import util 6 | from sqlalchemy.engine.strategies import EngineStrategy 7 | 8 | from .engine import GinoEngine 9 | 10 | 11 | class GinoStrategy(EngineStrategy): 12 | """A SQLAlchemy engine strategy for GINO. 13 | 14 | This strategy is initialized automatically as :mod:`gino` is imported. 15 | 16 | If :func:`sqlalchemy.create_engine` uses ``strategy="gino"``, it will return a 17 | :class:`~collections.abc.Coroutine`, and treat URL prefix ``postgresql://`` or 18 | ``postgres://`` as ``postgresql+asyncpg://``. 19 | """ 20 | 21 | name = "gino" 22 | engine_cls = GinoEngine 23 | 24 | async def create(self, name_or_url, loop=None, **kwargs): 25 | engine_cls = self.engine_cls 26 | u = url.make_url(name_or_url) 27 | if loop is None: 28 | loop = asyncio.get_event_loop() 29 | if u.drivername in {"postgresql", "postgres"}: 30 | u = copy(u) 31 | u.drivername = "postgresql+asyncpg" 32 | elif u.drivername in {"mysql"}: 33 | u = copy(u) 34 | u.drivername = "mysql+aiomysql" 35 | 36 | dialect_cls = u.get_dialect() 37 | 38 | pop_kwarg = kwargs.pop 39 | 40 | dialect_args = {} 41 | # consume dialect arguments from kwargs 42 | for k in util.get_cls_kwargs(dialect_cls).union( 43 | getattr(dialect_cls, "init_kwargs", set()) 44 | ): 45 | if k in kwargs: 46 | dialect_args[k] = pop_kwarg(k) 47 | 48 | kwargs.pop("module", None) # unused 49 | dbapi_args = {} 50 | for k in util.get_func_kwargs(dialect_cls.dbapi): 51 | if k in kwargs: 52 | dbapi_args[k] = pop_kwarg(k) 53 | dbapi = dialect_cls.dbapi(**dbapi_args) 54 | dialect_args["dbapi"] = dbapi 55 | 56 | dialect = dialect_cls(**dialect_args) 57 | pool_class = kwargs.pop("pool_class", None) 58 | pool = await dialect.init_pool(u, loop, pool_class=pool_class) 59 | 60 | engine_args = dict(loop=loop) 61 | for k in util.get_cls_kwargs(engine_cls): 62 | if k in kwargs: 63 | engine_args[k] = pop_kwarg(k) 64 | 65 | # all kwargs should be consumed 66 | if kwargs: 67 | await pool.close() 68 | raise TypeError( 69 | "Invalid argument(s) %s sent to create_engine(), " 70 | "using configuration %s/%s. Please check that the " 71 | "keyword arguments are appropriate for this combination " 72 | "of components." 73 | % ( 74 | ",".join("'%s'" % k for k in kwargs), 75 | dialect_cls.__name__, 76 | engine_cls.__name__, 77 | ) 78 | ) 79 | 80 | engine = engine_cls(dialect, pool, **engine_args) 81 | 82 | dialect_cls.engine_created(engine) 83 | 84 | return engine 85 | 86 | 87 | GinoStrategy() 88 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | from os import path 7 | 8 | import gino 9 | 10 | # -- Path setup -------------------------------------------------------------- 11 | 12 | # If extensions (or modules to document with autodoc) are in another directory, 13 | # add these directories to sys.path here. If the directory is relative to the 14 | # documentation root, use os.path.abspath to make it absolute, like shown here. 15 | # 16 | # import os 17 | # import sys 18 | # sys.path.insert(0, os.path.abspath('.')) 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = "GINO" 24 | copyright = "2017-present, Fantix King" 25 | author = "Fantix King " 26 | 27 | # The full version, including alpha/beta/rc tags 28 | 29 | release = gino.__version__ 30 | 31 | # -- General configuration --------------------------------------------------- 32 | 33 | # Add any Sphinx extension module names here, as strings. They can be 34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 35 | # ones. 36 | extensions = [ 37 | "sphinxcontrib.apidoc", 38 | "sphinx.ext.intersphinx", 39 | ] 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ["_templates"] 43 | 44 | # List of patterns, relative to source directory, that match files and 45 | # directories to ignore when looking for source files. 46 | # This pattern also affects html_static_path and html_extra_path. 47 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 48 | 49 | # -- Options for HTML output ------------------------------------------------- 50 | 51 | # The theme to use for HTML and HTML Help pages. See the documentation for 52 | # a list of builtin themes. 53 | # 54 | html_theme = 'python-gino' 55 | 56 | # Add any paths that contain custom static files (such as style sheets) here, 57 | # relative to this directory. They are copied after the builtin static files, 58 | # so a file named "default.css" will overwrite the builtin "default.css". 59 | html_static_path = ["_static"] 60 | html_css_files = ["gino.css"] 61 | # html_logo = "images/logo.png" 62 | 63 | # sphinxcontrib.apidoc 64 | apidoc_module_dir = "../src" 65 | apidoc_output_dir = "reference/api" 66 | apidoc_separate_modules = True 67 | apidoc_toc_file = False 68 | 69 | # sphinx.ext.intersphinx 70 | intersphinx_mapping = { 71 | "sqlalchemy": ("https://docs.sqlalchemy.org/en/latest/", None), 72 | "asyncpg": ("https://magicstack.github.io/asyncpg/current/", None), 73 | "python": ("https://docs.python.org/3", None), 74 | } 75 | 76 | locale_dirs = ["locale/"] # path is example but recommended. 77 | gettext_compact = False # optional. 78 | master_doc = "index" 79 | 80 | 81 | def setup(app): 82 | app.add_html_theme( 83 | "python-gino", path.abspath(path.join(path.dirname(__file__), "theme")) 84 | ) 85 | -------------------------------------------------------------------------------- /docs/images/explanation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ex-icon 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/test_schema.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | import pytest 4 | from asyncpg import UndefinedTableError 5 | 6 | import gino 7 | from gino.dialects.asyncpg import AsyncEnum 8 | 9 | pytestmark = pytest.mark.asyncio 10 | 11 | 12 | class MyEnum(Enum): 13 | ONE = "one" 14 | TWO = "two" 15 | 16 | 17 | async def test(engine, define=True): 18 | db = gino.Gino() 19 | 20 | class Blog(db.Model): 21 | __tablename__ = "s_blog" 22 | 23 | id = db.Column(db.BigInteger(), primary_key=True) 24 | title = db.Column(db.Unicode(), index=True, comment="Title Comment") 25 | visits = db.Column(db.BigInteger(), default=0) 26 | comment_id = db.Column(db.ForeignKey("s_comment.id")) 27 | number = db.Column(db.Enum(MyEnum), nullable=False, default=MyEnum.TWO) 28 | number2 = db.Column(AsyncEnum(MyEnum), nullable=False, default=MyEnum.TWO) 29 | 30 | class Comment(db.Model): 31 | __tablename__ = "s_comment" 32 | 33 | id = db.Column(db.BigInteger(), primary_key=True) 34 | blog_id = db.Column(db.ForeignKey("s_blog.id", name="blog_id_fk")) 35 | 36 | blog_seq = db.Sequence("blog_seq", metadata=db, schema="schema_test") 37 | 38 | try: 39 | async with engine.acquire() as conn: 40 | assert not await engine.dialect.has_schema(conn, "schema_test") 41 | assert not await engine.dialect.has_table(conn, "non_exist") 42 | assert not await engine.dialect.has_sequence(conn, "non_exist") 43 | assert not await engine.dialect.has_type(conn, "non_exist") 44 | assert not await engine.dialect.has_type( 45 | conn, "non_exist", schema="schema_test" 46 | ) 47 | await engine.status("create schema schema_test") 48 | Blog.__table__.schema = "schema_test" 49 | Blog.__table__.comment = "Blog Comment" 50 | Comment.__table__.schema = "schema_test" 51 | db.bind = engine 52 | await db.gino.create_all() 53 | await Blog.number.type.create_async(engine, checkfirst=True) 54 | await Blog.number2.type.create_async(engine, checkfirst=True) 55 | await db.gino.create_all(tables=[Blog.__table__], checkfirst=True) 56 | await blog_seq.gino.create(checkfirst=True) 57 | await Blog.__table__.gino.create(checkfirst=True) 58 | await db.gino.drop_all() 59 | await db.gino.drop_all(tables=[Blog.__table__], checkfirst=True) 60 | await Blog.__table__.gino.drop(checkfirst=True) 61 | await blog_seq.gino.drop(checkfirst=True) 62 | 63 | if define: 64 | 65 | class Comment2(db.Model): 66 | __tablename__ = "s_comment_2" 67 | 68 | id = db.Column(db.BigInteger(), primary_key=True) 69 | blog_id = db.Column(db.ForeignKey("s_blog.id")) 70 | 71 | Comment2.__table__.schema = "schema_test" 72 | await db.gino.create_all() 73 | await db.gino.drop_all() 74 | finally: 75 | await engine.status("drop schema schema_test cascade") 76 | 77 | 78 | async def test_no_alter(engine, mocker): 79 | engine.dialect.supports_alter = False 80 | with pytest.raises(UndefinedTableError): 81 | await test(engine, define=False) 82 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ====== 2 | |GINO| 3 | ====== 4 | 5 | .. image:: https://img.shields.io/pypi/v/gino?logo=python&logoColor=white 6 | :alt: PyPI Release Version 7 | :target: https://pypi.python.org/pypi/gino 8 | 9 | .. image:: https://img.shields.io/pypi/dm/gino?logo=pypi&logoColor=white 10 | :alt: PyPI Monthly Downloads 11 | :target: https://pypi.python.org/pypi/gino 12 | 13 | .. image:: https://img.shields.io/github/workflow/status/python-gino/gino/CI?label=CI&logo=github 14 | :alt: GitHub Workflow Status for CI 15 | :target: https://github.com/python-gino/gino/actions?query=workflow%3ACI 16 | 17 | .. image:: https://img.shields.io/codacy/grade/b6a59cdf5ca64eab9104928d4f9bbb97?logo=codacy 18 | :alt: Codacy Code Quality 19 | :target: https://app.codacy.com/gh/python-gino/gino/dashboard 20 | 21 | .. image:: https://img.shields.io/codacy/coverage/b6a59cdf5ca64eab9104928d4f9bbb97?logo=codacy 22 | :alt: Codacy coverage 23 | :target: https://app.codacy.com/gh/python-gino/gino/dashboard 24 | 25 | 26 | GINO - GINO Is Not ORM - is a lightweight asynchronous ORM built on top of 27 | SQLAlchemy_ core for Python asyncio_. GINO 1.1 supports PostgreSQL_ with asyncpg_, 28 | and MySQL with aiomysql_. 29 | 30 | * Free software: BSD license 31 | * Requires: Python 3.6 32 | * GINO is developed proudly with |PyCharm|. 33 | 34 | 35 | Home 36 | ---- 37 | 38 | `python-gino.org `__ 39 | 40 | 41 | Documentation 42 | ------------- 43 | 44 | * English_ 45 | * Chinese_ 46 | 47 | 48 | Installation 49 | ------------ 50 | 51 | .. code-block:: console 52 | 53 | $ pip install gino 54 | 55 | 56 | Features 57 | -------- 58 | 59 | * Robust SQLAlchemy-asyncpg bi-translator with no hard hack 60 | * Asynchronous SQLAlchemy-alike engine and connection 61 | * Asynchronous dialect API 62 | * Asynchronous-friendly CRUD objective models 63 | * Well-considered contextual connection and transaction management 64 | * Reusing native SQLAlchemy core to build queries with grammar sugars 65 | * Support SQLAlchemy ecosystem, e.g. Alembic_ for migration 66 | * `Community support `_ for Starlette_/FastAPI_, aiohttp_, Sanic_, Tornado_ and Quart_ 67 | * Rich PostgreSQL JSONB support 68 | 69 | 70 | .. _SQLAlchemy: https://www.sqlalchemy.org/ 71 | .. _asyncpg: https://github.com/MagicStack/asyncpg 72 | .. _PostgreSQL: https://www.postgresql.org/ 73 | .. _asyncio: https://docs.python.org/3/library/asyncio.html 74 | .. _Alembic: https://bitbucket.org/zzzeek/alembic 75 | .. _Sanic: https://github.com/channelcat/sanic 76 | .. _Tornado: http://www.tornadoweb.org/ 77 | .. _Quart: https://gitlab.com/pgjones/quart/ 78 | .. _English: https://python-gino.org/docs/en/ 79 | .. _Chinese: https://python-gino.org/docs/zh/ 80 | .. _aiohttp: https://github.com/aio-libs/aiohttp 81 | .. _Starlette: https://www.starlette.io/ 82 | .. _FastAPI: https://fastapi.tiangolo.com/ 83 | .. _aiomysql: https://github.com/aio-libs/aiomysql 84 | .. |PyCharm| image:: ./docs/images/pycharm.svg 85 | :height: 20px 86 | :target: https://www.jetbrains.com/?from=GINO 87 | 88 | .. |GINO| image:: ./docs/theme/static/logo.svg 89 | :alt: GINO 90 | :height: 64px 91 | :target: https://python-gino.org/ 92 | -------------------------------------------------------------------------------- /tests/test_core.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy import Table, Column, Integer, String, MetaData, ForeignKey 3 | from sqlalchemy.engine.result import RowProxy 4 | 5 | from .models import PG_URL 6 | 7 | pytestmark = pytest.mark.asyncio 8 | 9 | 10 | async def test_engine_only(): 11 | import gino 12 | from gino.schema import GinoSchemaVisitor 13 | 14 | metadata = MetaData() 15 | 16 | users = Table( 17 | "users", 18 | metadata, 19 | Column("id", Integer, primary_key=True), 20 | Column("name", String), 21 | Column("fullname", String), 22 | ) 23 | 24 | Table( 25 | "addresses", 26 | metadata, 27 | Column("id", Integer, primary_key=True), 28 | Column("user_id", None, ForeignKey("users.id")), 29 | Column("email_address", String, nullable=False), 30 | ) 31 | 32 | engine = await gino.create_engine(PG_URL) 33 | await GinoSchemaVisitor(metadata).create_all(engine) 34 | try: 35 | ins = users.insert().values(name="jack", fullname="Jack Jones") 36 | await engine.status(ins) 37 | res = await engine.all(users.select()) 38 | assert isinstance(res[0], RowProxy) 39 | finally: 40 | await GinoSchemaVisitor(metadata).drop_all(engine) 41 | 42 | 43 | async def test_core(): 44 | from gino import Gino 45 | 46 | db = Gino() 47 | 48 | users = db.Table( 49 | "users", 50 | db, 51 | db.Column("id", db.Integer, primary_key=True), 52 | db.Column("name", db.String), 53 | db.Column("fullname", db.String), 54 | ) 55 | 56 | db.Table( 57 | "addresses", 58 | db, 59 | db.Column("id", db.Integer, primary_key=True), 60 | db.Column("user_id", None, db.ForeignKey("users.id")), 61 | db.Column("email_address", db.String, nullable=False), 62 | ) 63 | 64 | async with db.with_bind(PG_URL): 65 | await db.gino.create_all() 66 | try: 67 | await users.insert().values( 68 | name="jack", fullname="Jack Jones", 69 | ).gino.status() 70 | res = await users.select().gino.all() 71 | assert isinstance(res[0], RowProxy) 72 | finally: 73 | await db.gino.drop_all() 74 | 75 | 76 | async def test_orm(): 77 | from gino import Gino 78 | 79 | db = Gino() 80 | 81 | class User(db.Model): 82 | __tablename__ = "users" 83 | 84 | id = db.Column(db.Integer, primary_key=True) 85 | name = db.Column(db.String) 86 | fullname = db.Column(db.String) 87 | 88 | class Address(db.Model): 89 | __tablename__ = "addresses" 90 | 91 | id = db.Column(db.Integer, primary_key=True) 92 | user_id = db.Column(None, db.ForeignKey("users.id")) 93 | email_address = db.Column(db.String, nullable=False) 94 | 95 | async with db.with_bind(PG_URL): 96 | await db.gino.create_all() 97 | try: 98 | await User.create(name="jack", fullname="Jack Jones") 99 | res = await User.query.gino.all() 100 | assert isinstance(res[0], User) 101 | finally: 102 | await db.gino.drop_all() 103 | -------------------------------------------------------------------------------- /docs/images/gino-fastapi-env.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
.env
.env
gino-fastapi-demo
gino-...
Viewer does not support full SVG 1.1
-------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at gino@decentfox.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /docs/images/community.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | co-icon 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /mysql_tests/test_core.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy import Table, Column, Integer, String, MetaData, ForeignKey 3 | from sqlalchemy.engine.result import RowProxy 4 | 5 | from .models import MYSQL_URL 6 | 7 | pytestmark = pytest.mark.asyncio 8 | 9 | 10 | async def test_engine_only(): 11 | import gino 12 | from gino.schema import GinoSchemaVisitor 13 | 14 | metadata = MetaData() 15 | 16 | users = Table( 17 | "users", 18 | metadata, 19 | Column("id", Integer, primary_key=True), 20 | Column("name", String(255)), 21 | Column("fullname", String(255)), 22 | ) 23 | 24 | Table( 25 | "addresses", 26 | metadata, 27 | Column("id", Integer, primary_key=True), 28 | Column("user_id", None, ForeignKey("users.id")), 29 | Column("email_address", String(255), nullable=False), 30 | ) 31 | 32 | engine = await gino.create_engine(MYSQL_URL) 33 | await GinoSchemaVisitor(metadata).create_all(engine) 34 | try: 35 | ins = users.insert().values(name="jack", fullname="Jack Jones") 36 | await engine.status(ins) 37 | res = await engine.all(users.select()) 38 | assert isinstance(res[0], RowProxy) 39 | finally: 40 | await GinoSchemaVisitor(metadata).drop_all(engine) 41 | await engine.close() 42 | 43 | 44 | async def test_core(): 45 | from gino import Gino 46 | 47 | db = Gino() 48 | 49 | users = db.Table( 50 | "users", 51 | db, 52 | db.Column("id", db.Integer, primary_key=True), 53 | db.Column("name", db.String(255)), 54 | db.Column("fullname", db.String(255)), 55 | ) 56 | 57 | db.Table( 58 | "addresses", 59 | db, 60 | db.Column("id", db.Integer, primary_key=True), 61 | db.Column("user_id", None, db.ForeignKey("users.id")), 62 | db.Column("email_address", db.String(255), nullable=False), 63 | ) 64 | 65 | async with db.with_bind(MYSQL_URL): 66 | await db.gino.create_all() 67 | try: 68 | await users.insert().values( 69 | name="jack", fullname="Jack Jones", 70 | ).gino.status() 71 | res = await users.select().gino.all() 72 | assert isinstance(res[0], RowProxy) 73 | finally: 74 | await db.gino.drop_all() 75 | 76 | 77 | async def test_orm(): 78 | from gino import Gino 79 | 80 | db = Gino() 81 | 82 | class User(db.Model): 83 | __tablename__ = "users" 84 | 85 | id = db.Column(db.Integer, primary_key=True) 86 | name = db.Column(db.String(255)) 87 | fullname = db.Column(db.String(255)) 88 | 89 | class Address(db.Model): 90 | __tablename__ = "addresses" 91 | 92 | id = db.Column(db.Integer, primary_key=True) 93 | user_id = db.Column(None, db.ForeignKey("users.id")) 94 | email_address = db.Column(db.String(255), nullable=False) 95 | 96 | async with db.with_bind(MYSQL_URL): 97 | await db.gino.create_all() 98 | try: 99 | await User.create(name="jack", fullname="Jack Jones") 100 | res = await User.query.gino.all() 101 | assert isinstance(res[0], User) 102 | finally: 103 | await db.gino.drop_all() 104 | -------------------------------------------------------------------------------- /docs/images/open-source.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | bsd 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to GINO's documentation! 2 | ================================ 3 | 4 | .. image:: https://img.shields.io/pypi/v/gino?logo=python&logoColor=white&color=3E6CDE&style=flat-square 5 | :alt: PyPI Release Version 6 | :target: https://pypi.python.org/pypi/gino 7 | 8 | .. image:: https://img.shields.io/github/workflow/status/python-gino/gino/test?label=test&logo=github&color=3E6CDE&style=flat-square 9 | :alt: GitHub Workflow Status for tests 10 | :target: https://github.com/python-gino/gino/actions?query=workflow%3Atest 11 | 12 | .. image:: https://img.shields.io/codacy/coverage/b6a59cdf5ca64eab9104928d4f9bbb97?logo=codacy&color=3E6CDE&style=flat-square 13 | :alt: Codacy coverage 14 | :target: https://app.codacy.com/gh/python-gino/gino/dashboard 15 | 16 | .. image:: https://img.shields.io/badge/Dependabot-active-brightgreen?logo=dependabot&color=3E6CDE&style=flat-square 17 | :target: https://app.dependabot.com/accounts/python-gino/projects/129260 18 | :alt: Dependabot 19 | 20 | 21 | GINO - GINO Is Not ORM - is a lightweight asynchronous ORM built on top of 22 | SQLAlchemy_ core for Python asyncio_. Now (early 2020) GINO supports only one 23 | dialect asyncpg_. 24 | 25 | .. _asyncio: https://docs.python.org/3/library/asyncio.html 26 | .. _SQLAlchemy: https://www.sqlalchemy.org/ 27 | .. _asyncpg: https://github.com/MagicStack/asyncpg 28 | 29 | 30 | .. cssclass:: boxed-nav 31 | 32 | * .. image:: images/tutorials.svg 33 | 34 | :doc:`tutorials` 35 | 36 | Lessons for the newcomer to get started 37 | 38 | * .. image:: images/how-to.svg 39 | 40 | :doc:`how-to` 41 | 42 | Solve specific problems by steps 43 | 44 | * .. image:: images/explanation.svg 45 | 46 | :doc:`explanation` 47 | 48 | Explains the background and context 49 | 50 | * .. image:: images/reference.svg 51 | 52 | :doc:`reference` 53 | 54 | Describes the software as it is 55 | 56 | 57 | Useful Links 58 | ------------ 59 | 60 | .. cssclass:: boxed-nav 61 | 62 | * .. image:: images/github.svg 63 | 64 | `Source Code `_ 65 | 66 | https://github.com/python-gino/gino 67 | 68 | * .. image:: images/community.svg 69 | 70 | `Community `_ 71 | 72 | https://gitter.im/python-gino/Lobby 73 | 74 | * .. image:: images/open-source.svg 75 | 76 | `BSD license `_ 77 | 78 | GINO is free software 79 | 80 | * .. image:: images/python.svg 81 | 82 | `Download `_ 83 | 84 | Download GINO from PyPI 85 | 86 | 87 | .. cssclass:: divio 88 | 89 | Sections by `Divio `_. 90 | 91 | .. toctree:: 92 | :caption: Tutorials 93 | :maxdepth: 1 94 | :glob: 95 | :hidden: 96 | 97 | tutorials 98 | tutorials/announcement.rst 99 | tutorials/tutorial.rst 100 | tutorials/* 101 | 102 | .. toctree:: 103 | :caption: How-to Guides 104 | :maxdepth: 1 105 | :glob: 106 | :hidden: 107 | 108 | how-to 109 | how-to/* 110 | 111 | .. toctree:: 112 | :caption: Explanation 113 | :maxdepth: 1 114 | :glob: 115 | :hidden: 116 | 117 | explanation 118 | explanation/* 119 | 120 | .. toctree:: 121 | :caption: Reference 122 | :maxdepth: 1 123 | :glob: 124 | :hidden: 125 | 126 | reference 127 | reference/* 128 | -------------------------------------------------------------------------------- /mysql_tests/test_iterate.py: -------------------------------------------------------------------------------- 1 | from gino import UninitializedError 2 | import pytest 3 | 4 | from .models import db, User 5 | 6 | pytestmark = pytest.mark.asyncio 7 | 8 | 9 | @pytest.fixture 10 | def names(sa_engine): 11 | rv = {"11", "22", "33"} 12 | sa_engine.execute(User.__table__.insert(), [dict(name=name) for name in rv]) 13 | yield rv 14 | sa_engine.execute("DELETE FROM gino_users") 15 | 16 | 17 | # noinspection PyUnusedLocal,PyShadowingNames 18 | async def test_bind(bind, names): 19 | with pytest.raises(ValueError, match="No Connection in context"): 20 | async for u in User.query.gino.iterate(): 21 | assert False, "Should not reach here" 22 | with pytest.raises(ValueError, match="No Connection in context"): 23 | await User.query.gino.iterate() 24 | with pytest.raises(ValueError, match="No Connection in context"): 25 | await db.iterate(User.query) 26 | 27 | result = set() 28 | async with bind.transaction(): 29 | async for u in User.query.gino.iterate(): 30 | result.add(u.nickname) 31 | assert names == result 32 | 33 | result = set() 34 | async with bind.transaction(): 35 | async for u in db.iterate(User.query): 36 | result.add(u.nickname) 37 | assert names == result 38 | 39 | result = set() 40 | async with bind.transaction(): 41 | cursor = await User.query.gino.iterate() 42 | result.add((await cursor.next()).nickname) 43 | assert names != result 44 | result.update([u.nickname for u in await cursor.many(1)]) 45 | assert names != result 46 | result.update([u.nickname for u in await cursor.many(2)]) 47 | assert names == result 48 | result.update([u.nickname for u in await cursor.many(2)]) 49 | assert names == result 50 | assert await cursor.next() is None 51 | 52 | with pytest.raises(ValueError, match="too many multiparams"): 53 | async with bind.transaction(): 54 | await db.iterate( 55 | User.insert(), 56 | [dict(nickname="444"), dict(nickname="555"), dict(nickname="666"),], 57 | ) 58 | 59 | result = set() 60 | async with bind.transaction(): 61 | cursor = await User.query.gino.iterate() 62 | await cursor.forward(1) 63 | result.add((await cursor.next()).nickname) 64 | assert names != result 65 | result.update([u.nickname for u in await cursor.many(1)]) 66 | assert names != result 67 | result.update([u.nickname for u in await cursor.many(2)]) 68 | assert names != result 69 | assert await cursor.next() is None 70 | 71 | 72 | # noinspection PyUnusedLocal,PyShadowingNames 73 | async def test_basic(engine, names): 74 | result = set() 75 | async with engine.transaction() as tx: 76 | with pytest.raises(UninitializedError): 77 | await db.iterate(User.query) 78 | result = set() 79 | async for u in tx.connection.iterate(User.query): 80 | result.add(u.nickname) 81 | async for u in tx.connection.execution_options(timeout=1).iterate(User.query): 82 | result.add(u.nickname) 83 | assert names == result 84 | 85 | result = set() 86 | cursor = await tx.connection.iterate(User.query) 87 | result.update([u.nickname for u in await cursor.many(2)]) 88 | assert names != result 89 | result.update([u.nickname for u in await cursor.many(2)]) 90 | assert names == result 91 | -------------------------------------------------------------------------------- /tests/test_iterate.py: -------------------------------------------------------------------------------- 1 | from gino import UninitializedError 2 | import pytest 3 | 4 | from .models import db, User 5 | 6 | pytestmark = pytest.mark.asyncio 7 | 8 | 9 | @pytest.fixture 10 | def names(sa_engine): 11 | rv = {"11", "22", "33"} 12 | sa_engine.execute(User.__table__.insert(), [dict(name=name) for name in rv]) 13 | yield rv 14 | sa_engine.execute("DELETE FROM gino_users") 15 | 16 | 17 | # noinspection PyUnusedLocal,PyShadowingNames 18 | async def test_bind(bind, names): 19 | with pytest.raises(ValueError, match="No Connection in context"): 20 | async for u in User.query.gino.iterate(): 21 | assert False, "Should not reach here" 22 | with pytest.raises(ValueError, match="No Connection in context"): 23 | await User.query.gino.iterate() 24 | with pytest.raises(ValueError, match="No Connection in context"): 25 | await db.iterate(User.query) 26 | 27 | result = set() 28 | async with bind.transaction(): 29 | async for u in User.query.gino.iterate(): 30 | result.add(u.nickname) 31 | assert names == result 32 | 33 | result = set() 34 | async with bind.transaction(): 35 | async for u in db.iterate(User.query): 36 | result.add(u.nickname) 37 | assert names == result 38 | 39 | result = set() 40 | async with bind.transaction(): 41 | cursor = await User.query.gino.iterate() 42 | result.add((await cursor.next()).nickname) 43 | assert names != result 44 | result.update([u.nickname for u in await cursor.many(1)]) 45 | assert names != result 46 | result.update([u.nickname for u in await cursor.many(2)]) 47 | assert names == result 48 | result.update([u.nickname for u in await cursor.many(2)]) 49 | assert names == result 50 | assert await cursor.next() is None 51 | 52 | with pytest.raises(ValueError, match="too many multiparams"): 53 | async with bind.transaction(): 54 | await db.iterate( 55 | User.insert().returning(User.nickname), 56 | [dict(nickname="444"), dict(nickname="555"), dict(nickname="666"),], 57 | ) 58 | 59 | result = set() 60 | async with bind.transaction(): 61 | cursor = await User.query.gino.iterate() 62 | await cursor.forward(1) 63 | result.add((await cursor.next()).nickname) 64 | assert names != result 65 | result.update([u.nickname for u in await cursor.many(1)]) 66 | assert names != result 67 | result.update([u.nickname for u in await cursor.many(2)]) 68 | assert names != result 69 | assert await cursor.next() is None 70 | 71 | 72 | # noinspection PyUnusedLocal,PyShadowingNames 73 | async def test_basic(engine, names): 74 | result = set() 75 | async with engine.transaction() as tx: 76 | with pytest.raises(UninitializedError): 77 | await db.iterate(User.query) 78 | result = set() 79 | async for u in tx.connection.iterate(User.query): 80 | result.add(u.nickname) 81 | async for u in tx.connection.execution_options(timeout=1).iterate(User.query): 82 | result.add(u.nickname) 83 | assert names == result 84 | 85 | result = set() 86 | cursor = await tx.connection.iterate(User.query) 87 | result.update([u.nickname for u in await cursor.many(2)]) 88 | assert names != result 89 | result.update([u.nickname for u in await cursor.many(2)]) 90 | assert names == result 91 | -------------------------------------------------------------------------------- /mysql_tests/test_executemany.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from gino import MultipleResultsFound, NoResultFound 4 | from .models import db, User 5 | 6 | pytestmark = pytest.mark.asyncio 7 | 8 | 9 | # noinspection PyUnusedLocal 10 | async def test_status(bind): 11 | statement, params = db.compile( 12 | User.insert(), [dict(name="1"), dict(name="2")]) 13 | assert statement == ( 14 | "INSERT INTO gino_users (name, props, type) " "VALUES (%s, %s, %s)") 15 | assert params == (("1", '"{}"', "USER"), ("2", '"{}"', "USER")) 16 | result = await User.insert().gino.status(dict(name="1"), dict(name="2")) 17 | assert result is None 18 | assert len(await User.query.gino.all()) == 2 19 | 20 | 21 | # noinspection PyUnusedLocal 22 | async def test_all(bind): 23 | result = await User.insert().gino.all(dict(name="1"), dict(name="2")) 24 | assert result is None 25 | rows = await User.query.gino.all() 26 | assert len(rows) == 2 27 | assert set(u.nickname for u in rows) == {"1", "2"} 28 | 29 | result = await User.insert().gino.all(dict(name="3"), dict(name="4")) 30 | assert result is None 31 | rows = await User.query.gino.all() 32 | assert len(rows) == 4 33 | assert set(u.nickname for u in rows) == {"1", "2", "3", "4"} 34 | 35 | 36 | # noinspection PyUnusedLocal 37 | async def test_first(bind): 38 | result = await User.insert().gino.first(dict(name="1"), dict(name="2")) 39 | assert result is None 40 | rows = await User.query.gino.all() 41 | assert len(await User.query.gino.all()) == 2 42 | assert set(u.nickname for u in rows) == {"1", "2"} 43 | 44 | result = await User.insert().gino.first(dict(name="3"), dict(name="4")) 45 | assert result is None 46 | rows = await User.query.gino.all() 47 | assert len(rows) == 4 48 | assert set(u.nickname for u in rows) == {"1", "2", "3", "4"} 49 | 50 | 51 | # noinspection PyUnusedLocal 52 | async def test_one_or_none(bind): 53 | row = await User.query.gino.one_or_none() 54 | assert row is None 55 | 56 | await User.create(nickname="0") 57 | row = await User.query.gino.one_or_none() 58 | assert row.nickname == "0" 59 | 60 | result = ( 61 | await User.insert() 62 | .gino.one_or_none(dict(name="1"), dict(name="2")) 63 | ) 64 | assert result is None 65 | rows = await User.query.gino.all() 66 | assert len(await User.query.gino.all()) == 3 67 | assert set(u.nickname for u in rows) == {"0", "1", "2"} 68 | 69 | with pytest.raises(MultipleResultsFound): 70 | row = await User.query.gino.one_or_none() 71 | 72 | 73 | # noinspection PyUnusedLocal 74 | async def test_one(bind): 75 | with pytest.raises(NoResultFound): 76 | row = await User.query.gino.one() 77 | 78 | await User.create(nickname="0") 79 | row = await User.query.gino.one() 80 | assert row.nickname == "0" 81 | 82 | with pytest.raises(NoResultFound): 83 | await User.insert().gino.one(dict(name="1"), dict(name="2")) 84 | rows = await User.query.gino.all() 85 | assert len(await User.query.gino.all()) == 3 86 | assert set(u.nickname for u in rows) == {"0", "1", "2"} 87 | 88 | with pytest.raises(MultipleResultsFound): 89 | row = await User.query.gino.one() 90 | 91 | 92 | # noinspection PyUnusedLocal 93 | async def test_scalar(bind): 94 | result = ( 95 | await User.insert() 96 | .gino.scalar(dict(name="1"), dict(name="2")) 97 | ) 98 | assert result is None 99 | rows = await User.query.gino.all() 100 | assert len(await User.query.gino.all()) == 2 101 | assert set(u.nickname for u in rows) == {"1", "2"} 102 | 103 | result = await User.insert().gino.scalar(dict(name="3"), dict(name="4")) 104 | assert result is None 105 | rows = await User.query.gino.all() 106 | assert len(rows) == 4 107 | assert set(u.nickname for u in rows) == {"1", "2", "3", "4"} 108 | -------------------------------------------------------------------------------- /docs/how-to/alembic.rst: -------------------------------------------------------------------------------- 1 | Use Alembic 2 | =========== 3 | 4 | Alembic is a lightweight database migration tool for usage with the SQLAlchemy Database 5 | Toolkit for Python. It’s also possible to use with GINO. 6 | 7 | To add migrations to project first of all, add alembic as dependency: 8 | 9 | .. code-block:: console 10 | 11 | $ pip install --user alembic 12 | 13 | When you need to set up alembic for your project. 14 | 15 | Prepare sample project. We will have a structure: 16 | 17 | 18 | .. code-block:: console 19 | 20 | alembic_sample/ 21 | my_app/ 22 | models.py 23 | 24 | Inside ``models.py`` define simple DB Model with GINO: 25 | 26 | .. code-block:: python 27 | 28 | from gino import Gino 29 | 30 | db = Gino() 31 | 32 | class User(db.Model): 33 | __tablename__ = 'users' 34 | 35 | id = db.Column(db.Integer(), primary_key=True) 36 | nickname = db.Column(db.Unicode(), default='noname') 37 | 38 | 39 | Set up Alembic 40 | ^^^^^^^^^^^^^^ 41 | 42 | This will need to be done only once. Go to the main folder of your project 43 | ``alembic_sample`` and run: 44 | 45 | .. code-block:: console 46 | 47 | $ alembic init alembic 48 | 49 | 50 | Alembic will create a bunch of files and folders in your project directory. One of them 51 | will be ``alembic.ini``. Open ``alembic.ini`` (you can find it in the main project 52 | folder ``alembic_sample``). Now change property ``sqlalchemy.url =`` with your DB 53 | credentials. Like this: 54 | 55 | .. code-block:: ini 56 | 57 | sqlalchemy.url = postgres://{{username}}:{{password}}@{{address}}/{{db_name}} 58 | 59 | 60 | Next go to folder ``alembic/`` and open ``env.py`` file. Inside the ``env.py`` file you 61 | need to import the ``db`` object. In our case ``db`` object is ``db`` from ``models`` 62 | modules. This is a variable that links to your ``Gino()`` instance. 63 | 64 | Inside ``alembic/env.py``:: 65 | 66 | from main_app.models import db 67 | 68 | 69 | And change ``target_metadata =`` to:: 70 | 71 | target_metadata = db 72 | 73 | That’s it. We finished setting up Alembic for a project. 74 | 75 | .. note:: 76 | 77 | All ``alembic`` commands must be run always from the folder that contains the 78 | ``alembic.ini`` file. 79 | 80 | 81 | Create first migration revision 82 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 83 | 84 | Same commands you must run each time when you make some changes in DB Models and want to 85 | apply these changes to your DB Schema. 86 | 87 | .. code-block:: console 88 | 89 | $ alembic revision -m "first migration" --autogenerate --head head 90 | 91 | If you have any problems relative to package imports similar to this example: 92 | 93 | .. code-block:: console 94 | 95 | File "alembic/env.py", line 7, in 96 | from main_app.models import db 97 | ModuleNotFoundError: No module named 'main_app' 98 | 99 | Either install your project locally with ``pip install -e .``, ``poetry install`` or 100 | ``python setup.py develop``, or add you package to PYTHONPATH, like this: 101 | 102 | .. code-block:: console 103 | 104 | $ export PYTHONPATH=$PYTHONPATH:/full_path/to/alembic_sample 105 | 106 | After the successful run of ``alembic revision`` in folder ``alembic/versions`` you will 107 | see a file with new migration. 108 | 109 | 110 | Apply migration on DB 111 | ^^^^^^^^^^^^^^^^^^^^^ 112 | 113 | Now time to apply migration to DB. It will create tables based on you DB Models. 114 | 115 | .. code-block:: console 116 | 117 | $ alembic upgrade head 118 | 119 | Great. Now you apply your first migration. Congratulations! 120 | 121 | Next time, when you will make any changes in DB models just do: 122 | 123 | .. code-block:: console 124 | 125 | $ alembic revision -m "your migration description" --autogenerate --head head 126 | 127 | And 128 | 129 | .. code-block:: console 130 | 131 | alembic upgrade head 132 | 133 | 134 | Full documentation about how to work with Alembic migrations, downgrades and other 135 | things - you can find in official docs https://alembic.sqlalchemy.org 136 | -------------------------------------------------------------------------------- /tests/test_executemany.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from gino import MultipleResultsFound, NoResultFound 4 | from .models import db, User 5 | 6 | pytestmark = pytest.mark.asyncio 7 | 8 | 9 | # noinspection PyUnusedLocal 10 | async def test_status(bind): 11 | statement, params = db.compile(User.insert(), [dict(name="1"), dict(name="2")]) 12 | assert statement == ("INSERT INTO gino_users (name, type) " "VALUES ($1, $2)") 13 | assert params == (("1", "USER"), ("2", "USER")) 14 | result = await User.insert().gino.status(dict(name="1"), dict(name="2")) 15 | assert result is None 16 | assert len(await User.query.gino.all()) == 2 17 | 18 | 19 | # noinspection PyUnusedLocal 20 | async def test_all(bind): 21 | result = ( 22 | await User.insert() 23 | .returning(User.nickname) 24 | .gino.all(dict(name="1"), dict(name="2")) 25 | ) 26 | assert result is None 27 | rows = await User.query.gino.all() 28 | assert len(rows) == 2 29 | assert set(u.nickname for u in rows) == {"1", "2"} 30 | 31 | result = await User.insert().gino.all(dict(name="3"), dict(name="4")) 32 | assert result is None 33 | rows = await User.query.gino.all() 34 | assert len(rows) == 4 35 | assert set(u.nickname for u in rows) == {"1", "2", "3", "4"} 36 | 37 | 38 | # noinspection PyUnusedLocal 39 | async def test_first(bind): 40 | result = ( 41 | await User.insert() 42 | .returning(User.nickname) 43 | .gino.first(dict(name="1"), dict(name="2")) 44 | ) 45 | assert result is None 46 | rows = await User.query.gino.all() 47 | assert len(await User.query.gino.all()) == 2 48 | assert set(u.nickname for u in rows) == {"1", "2"} 49 | 50 | result = await User.insert().gino.first(dict(name="3"), dict(name="4")) 51 | assert result is None 52 | rows = await User.query.gino.all() 53 | assert len(rows) == 4 54 | assert set(u.nickname for u in rows) == {"1", "2", "3", "4"} 55 | 56 | 57 | # noinspection PyUnusedLocal 58 | async def test_one_or_none(bind): 59 | row = await User.query.gino.one_or_none() 60 | assert row is None 61 | 62 | await User.create(nickname="0") 63 | row = await User.query.gino.one_or_none() 64 | assert row.nickname == "0" 65 | 66 | result = ( 67 | await User.insert() 68 | .returning(User.nickname) 69 | .gino.one_or_none(dict(name="1"), dict(name="2")) 70 | ) 71 | assert result is None 72 | rows = await User.query.gino.all() 73 | assert len(await User.query.gino.all()) == 3 74 | assert set(u.nickname for u in rows) == {"0", "1", "2"} 75 | 76 | with pytest.raises(MultipleResultsFound): 77 | row = await User.query.gino.one_or_none() 78 | 79 | 80 | # noinspection PyUnusedLocal 81 | async def test_one(bind): 82 | with pytest.raises(NoResultFound): 83 | row = await User.query.gino.one() 84 | 85 | await User.create(nickname="0") 86 | row = await User.query.gino.one() 87 | assert row.nickname == "0" 88 | 89 | with pytest.raises(NoResultFound): 90 | await User.insert().returning(User.nickname).gino.one( 91 | dict(name="1"), dict(name="2") 92 | ) 93 | rows = await User.query.gino.all() 94 | assert len(await User.query.gino.all()) == 3 95 | assert set(u.nickname for u in rows) == {"0", "1", "2"} 96 | 97 | with pytest.raises(MultipleResultsFound): 98 | row = await User.query.gino.one() 99 | 100 | 101 | # noinspection PyUnusedLocal 102 | async def test_scalar(bind): 103 | result = ( 104 | await User.insert() 105 | .returning(User.nickname) 106 | .gino.scalar(dict(name="1"), dict(name="2")) 107 | ) 108 | assert result is None 109 | rows = await User.query.gino.all() 110 | assert len(await User.query.gino.all()) == 2 111 | assert set(u.nickname for u in rows) == {"1", "2"} 112 | 113 | result = await User.insert().gino.scalar(dict(name="3"), dict(name="4")) 114 | assert result is None 115 | rows = await User.query.gino.all() 116 | assert len(rows) == 4 117 | assert set(u.nickname for u in rows) == {"1", "2", "3", "4"} 118 | -------------------------------------------------------------------------------- /docs/theme/static/images/icon-warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 编组 9 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/theme/static/images/icon-hint.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 编组 6 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - "master" 7 | - "ci" 8 | tags: 9 | - "v[0-9]+.[0-9]+" 10 | - "v[0-9]+.[0-9]+.[0-9]+" 11 | 12 | jobs: 13 | create-virtualenv: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: source code 17 | uses: actions/checkout@v2 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v2 21 | 22 | - name: virtualenv cache 23 | uses: syphar/restore-virtualenv@v1.2 24 | id: cache-virtualenv 25 | 26 | - name: pip cache 27 | uses: syphar/restore-pip-download-cache@v1 28 | if: steps.cache-virtualenv.outputs.cache-hit != 'true' 29 | 30 | - name: Install Python dependencies 31 | if: steps.cache-virtualenv.outputs.cache-hit != 'true' 32 | env: 33 | POETRY_VERSION: 1.1.13 34 | run: | 35 | pip install pip==22.0.3 setuptools==60.8.2 36 | curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - 37 | source $HOME/.poetry/env 38 | poetry install --no-interaction -E pg -E mysql 39 | 40 | - name: Log currently installed packages and versions 41 | run: pip list 42 | 43 | build-docs: 44 | needs: create-virtualenv 45 | runs-on: ubuntu-latest 46 | strategy: 47 | matrix: 48 | language: [ 'en', 'zh' ] 49 | steps: 50 | - name: source code 51 | uses: actions/checkout@v2 52 | 53 | - name: Set up Python 54 | uses: actions/setup-python@v2 55 | 56 | - name: virtualenv cache 57 | uses: syphar/restore-virtualenv@v1.2 58 | 59 | - name: Download latest translations 60 | if: matrix.language != 'en' 61 | run: | 62 | sphinx-intl create-transifexrc 63 | make -C docs -e LOC="${{ matrix.language }}" pull 64 | env: 65 | SPHINXINTL_TRANSIFEX_USERNAME: api 66 | SPHINXINTL_TRANSIFEX_PASSWORD: ${{ secrets.TRANSIFEX_TOKEN }} 67 | LOC: ${{ matrix.language }} 68 | 69 | - name: Build the documentation 70 | run: | 71 | make -C docs -e SPHINXOPTS="-D language='${{ matrix.language }}' -A GAID='${{ secrets.GAID }}' -A VERSION='${{ github.ref }}'" html 72 | 73 | - name: Add current version to versions.json 74 | shell: python 75 | env: 76 | LOC: ${{ matrix.language }} 77 | run: | 78 | import os, json 79 | try: 80 | with open('docs/versions.json') as f: 81 | versions = json.load(f) 82 | except Exception: 83 | versions = {} 84 | by_loc = versions.setdefault(os.environ['LOC'], []) 85 | by_loc.append(os.environ['GITHUB_REF'].split('/')[-1]) 86 | by_loc.sort() 87 | with open('docs/versions.json', 'w') as f: 88 | json.dump(versions, f) 89 | print(versions) 90 | 91 | - name: Publish to GitHub Pages 92 | if: github.ref != 'refs/heads/ci' 93 | uses: python-gino/ghaction-github-pages@master 94 | with: 95 | repo: python-gino/python-gino.org 96 | target_branch: master 97 | target_path: docs/${{ matrix.language }}/${{ github.ref }} 98 | keep_history: true 99 | allow_empty_commit: true 100 | build_dir: docs/_build/html 101 | commit_message: Update docs/${{ matrix.language }}/${{ github.ref }} @ ${{ github.sha }} 102 | env: 103 | GITHUB_PAT: ${{ secrets.GITHUB_PAT }} 104 | 105 | release: 106 | runs-on: ubuntu-latest 107 | steps: 108 | - name: source code 109 | if: startsWith(github.ref, 'refs/tags/') 110 | uses: actions/checkout@v2 111 | 112 | - name: Set up Python 113 | if: startsWith(github.ref, 'refs/tags/') 114 | uses: actions/setup-python@v2 115 | 116 | - name: Release to PyPI 117 | if: startsWith(github.ref, 'refs/tags/') 118 | env: 119 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 120 | POETRY_VERSION: 1.1.4 121 | run: | 122 | pip install pip==22.0.3 setuptools==60.8.2 123 | curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - 124 | source $HOME/.poetry/env 125 | poetry build 126 | poetry publish --username __token__ --password ${{ secrets.PYPI_TOKEN }} 127 | -------------------------------------------------------------------------------- /docs/images/gino-fastapi-poetry.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
gino-fastapi-demo
gino-...
pyproject.toml
pyp...
poetry.lock
poe...
Viewer does not support full SVG 1.1
-------------------------------------------------------------------------------- /docs/reference/extensions/sanic.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Sanic Support 3 | ============= 4 | 5 | **THIS IS A WIP** 6 | 7 | 8 | Work with Sanic 9 | --------------- 10 | 11 | Using the Sanic extension, the request handler acquires a lazy connection on each request, 12 | and return the connection when the response finishes by default. 13 | 14 | The lazy connection is actually established if necessary, i.e. just before first access to db. 15 | 16 | This behavior is controlled by app.config.DB_USE_CONNECTION_FOR_REQUEST, which is True by default. 17 | 18 | Supported configurations: 19 | 20 | - DB_HOST 21 | - DB_PORT 22 | - DB_USER 23 | - DB_PASSWORD 24 | - DB_DATABASE 25 | - DB_ECHO 26 | - DB_POOL_MIN_SIZE 27 | - DB_POOL_MAX_SIZE 28 | - DB_SSL 29 | - DB_USE_CONNECTION_FOR_REQUEST 30 | - DB_KWARGS 31 | 32 | An example server: 33 | 34 | :: 35 | 36 | from sanic import Sanic 37 | from sanic.exceptions import abort 38 | from sanic.response import json 39 | from gino.ext.sanic import Gino 40 | 41 | app = Sanic() 42 | app.config.DB_HOST = 'localhost' 43 | app.config.DB_DATABASE = 'gino' 44 | db = Gino() 45 | db.init_app(app) 46 | 47 | 48 | class User(db.Model): 49 | __tablename__ = 'users' 50 | 51 | id = db.Column(db.BigInteger(), primary_key=True) 52 | nickname = db.Column(db.Unicode()) 53 | 54 | def __repr__(self): 55 | return '{}<{}>'.format(self.nickname, self.id) 56 | 57 | 58 | @app.route("/users/") 59 | async def get_user(request, user_id): 60 | if not user_id.isdigit(): 61 | abort(400, 'invalid user id') 62 | user = await User.get_or_404(int(user_id)) 63 | return json({'name': user.nickname}) 64 | 65 | 66 | if __name__ == '__main__': 67 | app.run(debug=True) 68 | 69 | 70 | Sanic Support 71 | ------------- 72 | 73 | To integrate with Sanic, a few configurations needs to be set in 74 | ``app.config`` (with default value though): 75 | 76 | - DB_HOST: if not set, ``localhost`` 77 | - DB_PORT: if not set, ``5432`` 78 | - DB_USER: if not set, ``postgres`` 79 | - DB_PASSWORD: if not set, empty string 80 | - DB_DATABASE: if not set, ``postgres`` 81 | - DB_ECHO: if not set, ``False`` 82 | - DB_POOL_MIN_SIZE: if not set, 5 83 | - DB_POOL_MAX_SIZE: if not set, 10 84 | - DB_SSL: if not set, ``None`` 85 | - DB_KWARGS; if not set, empty dictionary 86 | 87 | An example: 88 | 89 | .. code-block:: python 90 | 91 | from sanic import Sanic 92 | from gino.ext.sanic import Gino 93 | 94 | app = Sanic() 95 | app.config.DB_HOST = 'localhost' 96 | app.config.DB_USER = 'postgres' 97 | 98 | db = Gino() 99 | db.init_app(app) 100 | 101 | 102 | After ``db.init_app``, a connection pool with configured settings shall be 103 | created and bound to ``db`` when Sanic server is started, and closed on stop. 104 | Furthermore, a lazy connection context is created on each request, and released 105 | on response. That is to say, within Sanic request handlers, you can directly 106 | access db by e.g. ``User.get(1)``, everything else is settled: database pool is 107 | created on server start, connection is lazily borrowed from pool on the first 108 | database access and shared within the rest of the same request handler, and 109 | automatically returned to the pool on response. 110 | 111 | Please be noted that, in the async world, ``await`` may block unpredictably for 112 | a long time. When this world is crossing RDBMS pools and transactions, it is 113 | a very dangerous bite for performance, even causing disasters sometimes. 114 | Therefore we recommend, during the time enjoying fast development, do pay 115 | special attention to the scope of transactions and borrowed connections, make 116 | sure that transactions are closed as soon as possible, and connections are not 117 | taken for unnecessarily long time. As for the Sanic support, if you want to 118 | release the concrete connection in the request context before response is 119 | reached, just do it like this: 120 | 121 | .. code-block:: python 122 | 123 | await request['connection'].release() 124 | 125 | 126 | Or if you prefer not to use the contextual lazy connection in certain handlers, 127 | prefer explicitly manage the connection lifetime, you can always borrow a new 128 | connection by setting ``reuse=False``: 129 | 130 | .. code-block:: python 131 | 132 | async with db.acquire(reuse=False): 133 | # new connection context is created 134 | 135 | 136 | Or if you prefer not to use the builtin request-scoped lazy connection at all, 137 | you can simply turn it off: 138 | 139 | .. code-block:: python 140 | 141 | app.config.DB_USE_CONNECTION_FOR_REQUEST = False 142 | 143 | 144 | -------------------------------------------------------------------------------- /docs/images/how-to.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | guide 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/test_ext.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import importlib 3 | import sys 4 | import pytest 5 | import runpy 6 | from mypy.build import build 7 | from mypy.modulefinder import BuildSource 8 | from mypy.options import Options 9 | 10 | 11 | def installed(): 12 | rv = 0 13 | for finder in sys.meta_path: 14 | if type(finder).__name__ == "_GinoExtensionCompatFinder": 15 | rv += 1 16 | return rv 17 | 18 | 19 | def test_install(): 20 | from gino import ext 21 | 22 | importlib.reload(ext) 23 | 24 | assert installed() == 1 25 | 26 | ext._GinoExtensionCompatFinder().install() 27 | assert installed() == 1 28 | 29 | ext._GinoExtensionCompatFinder.uninstall() 30 | assert not installed() 31 | 32 | ext._GinoExtensionCompatFinder().uninstall() 33 | assert not installed() 34 | 35 | ext._GinoExtensionCompatFinder().install() 36 | assert installed() == 1 37 | 38 | ext._GinoExtensionCompatFinder().install() 39 | assert installed() == 1 40 | 41 | 42 | def test_import(mocker): 43 | from gino import ext 44 | 45 | importlib.reload(ext) 46 | 47 | EntryPoint = collections.namedtuple("EntryPoint", ["name", "value"]) 48 | mocker.patch( 49 | "gino.ext.entry_points", 50 | new=lambda: { 51 | "gino.extensions": [ 52 | EntryPoint("demo", "tests.stub1"), 53 | EntryPoint("demo2", "tests.stub2"), 54 | ] 55 | }, 56 | ) 57 | ext._GinoExtensionCompatFinder().install() 58 | from gino.ext import demo 59 | 60 | assert sys.modules["tests.stub1"] is sys.modules["gino.ext.demo"] is demo 61 | 62 | from tests import stub2 63 | from gino.ext import demo2 64 | 65 | assert sys.modules["tests.stub2"] is sys.modules["gino.ext.demo2"] is demo2 is stub2 66 | 67 | 68 | def test_import_error(): 69 | with pytest.raises(ImportError, match="gino-nonexist"): 70 | # noinspection PyUnresolvedReferences 71 | from gino.ext import nonexist 72 | 73 | 74 | @pytest.fixture 75 | def extensions(mocker): 76 | EntryPoint = collections.namedtuple("EntryPoint", ["name", "value"]) 77 | importlib_metadata = mocker.Mock() 78 | importlib_metadata.entry_points = lambda: { 79 | "gino.extensions": [ 80 | EntryPoint("demo1", "tests.stub1"), 81 | EntryPoint("demo2", "tests.stub2"), 82 | ] 83 | } 84 | mocker.patch.dict("sys.modules", {"importlib.metadata": importlib_metadata}) 85 | 86 | 87 | def test_list(mocker, extensions): 88 | mocker.patch("sys.argv", ["", "list"]) 89 | stdout = mocker.patch("sys.stdout.write") 90 | runpy.run_module("gino.ext", run_name="__main__") 91 | out = "".join(args[0][0] for args in stdout.call_args_list) 92 | assert "tests.stub1" in out 93 | assert "tests.stub2" in out 94 | assert "gino.ext.demo1" in out 95 | assert "gino.ext.demo2" in out 96 | assert out.count("no stub file") == 2 97 | 98 | mocker.patch("sys.argv", [""]) 99 | runpy.run_module("gino.ext", run_name="__main__") 100 | 101 | 102 | def test_list_empty(mocker): 103 | mocker.patch("sys.argv", ["", "list"]) 104 | stdout = mocker.patch("sys.stdout.write") 105 | runpy.run_module("gino.ext", run_name="__main__") 106 | out = "".join(args[0][0] for args in stdout.call_args_list) 107 | assert not out 108 | 109 | 110 | @pytest.mark.xfail # mypy stopped working for locally-installed package? 111 | def test_type_check(mocker, extensions): 112 | mocker.patch("sys.argv", ["", "clean"]) 113 | runpy.run_module("gino.ext", run_name="__main__") 114 | 115 | result = build( 116 | [BuildSource(None, None, "from gino.ext.demo3 import s3")], Options() 117 | ) 118 | assert result.errors 119 | 120 | result = build( 121 | [BuildSource(None, None, "from gino.ext.demo1 import s1")], Options() 122 | ) 123 | assert result.errors 124 | 125 | mocker.patch("sys.argv", ["", "stub"]) 126 | runpy.run_module("gino.ext", run_name="__main__") 127 | runpy.run_module("gino.ext", run_name="__main__") 128 | 129 | try: 130 | result = build( 131 | [BuildSource(None, None, "from gino.ext.demo1 import s1")], Options() 132 | ) 133 | assert not result.errors 134 | 135 | result = build( 136 | [BuildSource(None, None, "from gino.ext.demo1 import s2")], Options() 137 | ) 138 | assert result.errors 139 | 140 | result = build( 141 | [BuildSource(None, None, "from gino.ext.demo2 import s2")], Options() 142 | ) 143 | assert not result.errors 144 | 145 | result = build( 146 | [BuildSource(None, None, "from gino.ext.demo2 import s1")], Options() 147 | ) 148 | assert result.errors 149 | finally: 150 | mocker.patch("sys.argv", ["", "clean"]) 151 | runpy.run_module("gino.ext", run_name="__main__") 152 | -------------------------------------------------------------------------------- /tests/models.py: -------------------------------------------------------------------------------- 1 | import os 2 | import enum 3 | import random 4 | import string 5 | from datetime import datetime 6 | 7 | import pytest 8 | 9 | from gino import Gino 10 | from gino.dialects.asyncpg import JSONB 11 | 12 | DB_ARGS = dict( 13 | host=os.getenv("DB_HOST", "localhost"), 14 | port=os.getenv("DB_PORT", 5432), 15 | user=os.getenv("DB_USER", "postgres"), 16 | password=os.getenv("DB_PASS", ""), 17 | database=os.getenv("DB_NAME", "postgres"), 18 | ) 19 | PG_URL = "postgresql://{user}:{password}@{host}:{port}/{database}".format(**DB_ARGS) 20 | db = Gino() 21 | 22 | 23 | @pytest.fixture 24 | def random_name(length=8) -> str: 25 | return _random_name(length) 26 | 27 | 28 | def _random_name(length=8): 29 | return "".join(random.choice(string.ascii_letters) for _ in range(length)) 30 | 31 | 32 | class UserType(enum.Enum): 33 | USER = "USER" 34 | 35 | 36 | class User(db.Model): 37 | __tablename__ = "gino_users" 38 | 39 | id = db.Column(db.BigInteger(), primary_key=True) 40 | nickname = db.Column("name", db.Unicode(), default=_random_name) 41 | profile = db.Column("props", JSONB(), nullable=False, server_default="{}") 42 | parameter = db.Column("params", JSONB(), nullable=False, server_default="{}") 43 | type = db.Column(db.Enum(UserType), nullable=False, default=UserType.USER,) 44 | realname = db.StringProperty() 45 | age = db.IntegerProperty(default=18) 46 | balance = db.IntegerProperty(default=0) 47 | birthday = db.DateTimeProperty(default=lambda i: datetime.utcfromtimestamp(0)) 48 | team_id = db.Column(db.ForeignKey("gino_teams.id")) 49 | weight = db.IntegerProperty(prop_name='parameter') 50 | height = db.IntegerProperty(default=170, prop_name='parameter') 51 | bio = db.StringProperty(prop_name='parameter') 52 | 53 | @balance.after_get 54 | def balance(self, val): 55 | if val is None: 56 | return 0.0 57 | return float(val) 58 | 59 | def __repr__(self): 60 | return "{}<{}>".format(self.nickname, self.id) 61 | 62 | 63 | class Friendship(db.Model): 64 | __tablename__ = "gino_friendship" 65 | 66 | my_id = db.Column(db.BigInteger(), primary_key=True) 67 | friend_id = db.Column(db.BigInteger(), primary_key=True) 68 | 69 | def __repr__(self): 70 | return "Friends<{}, {}>".format(self.my_id, self.friend_id) 71 | 72 | 73 | class Relation(db.Model): 74 | __tablename__ = "gino_relation" 75 | 76 | name = db.Column(db.Text(), primary_key=True) 77 | 78 | 79 | class Team(db.Model): 80 | __tablename__ = "gino_teams" 81 | 82 | id = db.Column(db.BigInteger(), primary_key=True) 83 | name = db.Column(db.Unicode(), default=_random_name) 84 | parent_id = db.Column(db.ForeignKey("gino_teams.id")) 85 | company_id = db.Column(db.ForeignKey("gino_companies.id")) 86 | 87 | def __init__(self, **kw): 88 | super().__init__(**kw) 89 | self._members = set() 90 | 91 | @property 92 | def members(self): 93 | return self._members 94 | 95 | @members.setter 96 | def add_member(self, user): 97 | self._members.add(user) 98 | 99 | 100 | class TeamWithDefaultCompany(Team): 101 | company = Team(name="DEFAULT") 102 | 103 | 104 | class TeamWithoutMembersSetter(Team): 105 | def add_member(self, user): 106 | self._members.add(user) 107 | 108 | 109 | class Company(db.Model): 110 | __tablename__ = "gino_companies" 111 | 112 | id = db.Column(db.BigInteger(), primary_key=True) 113 | name = db.Column(db.Unicode(), default=_random_name) 114 | logo = db.Column(db.LargeBinary()) 115 | 116 | def __init__(self, **kw): 117 | super().__init__(**kw) 118 | self._teams = set() 119 | 120 | @property 121 | def teams(self): 122 | return self._teams 123 | 124 | @teams.setter 125 | def add_team(self, team): 126 | self._teams.add(team) 127 | 128 | 129 | class CompanyWithoutTeamsSetter(Company): 130 | def add_team(self, team): 131 | self._teams.add(team) 132 | 133 | 134 | class UserSetting(db.Model): 135 | __tablename__ = "gino_user_settings" 136 | 137 | # No constraints defined on columns 138 | id = db.Column(db.BigInteger()) 139 | user_id = db.Column(db.BigInteger()) 140 | setting = db.Column(db.Text()) 141 | value = db.Column(db.Text()) 142 | col1 = db.Column(db.Integer, default=1) 143 | col2 = db.Column(db.Integer, default=2) 144 | 145 | # Define indexes and constraints inline 146 | id_pkey = db.PrimaryKeyConstraint("id") 147 | user_id_fk = db.ForeignKeyConstraint(["user_id"], ["gino_users.id"]) 148 | user_id_setting_unique = db.UniqueConstraint("user_id", "setting") 149 | col1_check = db.CheckConstraint("col1 >= 1 AND col1 <= 5") 150 | col2_idx = db.Index("col2_idx", "col2") 151 | 152 | 153 | def qsize(engine): 154 | # noinspection PyProtectedMember 155 | return engine.raw_pool._queue.qsize() 156 | -------------------------------------------------------------------------------- /mysql_tests/models.py: -------------------------------------------------------------------------------- 1 | import os 2 | import enum 3 | import random 4 | import string 5 | from datetime import datetime 6 | 7 | import aiomysql 8 | import asyncpg 9 | import pytest 10 | 11 | from gino import Gino 12 | from gino.dialects.aiomysql import JSON 13 | 14 | DB_ARGS = dict( 15 | host=os.getenv("MYSQL_DB_HOST", "localhost"), 16 | port=os.getenv("MYSQL_DB_PORT", 3306), 17 | user=os.getenv("MYSQL_DB_USER", "root"), 18 | password=os.getenv("MYSQL_DB_PASS", ""), 19 | db=os.getenv("MYSQL_DB_NAME", "mysql"), 20 | ) 21 | MYSQL_URL = "mysql://{user}:{password}@{host}:{port}/{db}".format(**DB_ARGS) 22 | db = Gino() 23 | 24 | 25 | @pytest.fixture 26 | def random_name(length=8) -> str: 27 | return _random_name(length) 28 | 29 | 30 | def _random_name(length=8): 31 | return "".join(random.choice(string.ascii_letters) for _ in range(length)) 32 | 33 | 34 | class UserType(enum.Enum): 35 | USER = "USER" 36 | 37 | 38 | class User(db.Model): 39 | __tablename__ = "gino_users" 40 | 41 | id = db.Column(db.BigInteger(), primary_key=True) 42 | nickname = db.Column("name", db.Unicode(255), default=_random_name) 43 | profile = db.Column("props", JSON(), nullable=False, default="{}") 44 | type = db.Column(db.Enum(UserType), nullable=False, default=UserType.USER) 45 | realname = db.StringProperty() 46 | age = db.IntegerProperty(default=18) 47 | balance = db.IntegerProperty(default=0) 48 | birthday = db.DateTimeProperty(default=lambda i: datetime.utcfromtimestamp(0)) 49 | team_id = db.Column(db.ForeignKey("gino_teams.id")) 50 | 51 | @balance.after_get 52 | def balance(self, val): 53 | if val is None: 54 | return 0.0 55 | return float(val) 56 | 57 | def __repr__(self): 58 | return "{}<{}>".format(self.nickname, self.id) 59 | 60 | 61 | class Friendship(db.Model): 62 | __tablename__ = "gino_friendship" 63 | 64 | my_id = db.Column(db.BigInteger(), primary_key=True) 65 | friend_id = db.Column(db.BigInteger(), primary_key=True) 66 | 67 | def __repr__(self): 68 | return "Friends<{}, {}>".format(self.my_id, self.friend_id) 69 | 70 | 71 | class Relation(db.Model): 72 | __tablename__ = "gino_relation" 73 | 74 | name = db.Column(db.VARCHAR(255), primary_key=True) 75 | 76 | 77 | class Team(db.Model): 78 | __tablename__ = "gino_teams" 79 | 80 | id = db.Column(db.BigInteger(), primary_key=True) 81 | name = db.Column(db.Unicode(255), default=_random_name) 82 | parent_id = db.Column(db.ForeignKey("gino_teams.id", ondelete='CASCADE')) 83 | company_id = db.Column(db.ForeignKey("gino_companies.id")) 84 | 85 | def __init__(self, **kw): 86 | super().__init__(**kw) 87 | self._members = set() 88 | 89 | @property 90 | def members(self): 91 | return self._members 92 | 93 | @members.setter 94 | def add_member(self, user): 95 | self._members.add(user) 96 | 97 | 98 | class TeamWithDefaultCompany(Team): 99 | company = Team(name="DEFAULT") 100 | 101 | 102 | class TeamWithoutMembersSetter(Team): 103 | def add_member(self, user): 104 | self._members.add(user) 105 | 106 | 107 | class Company(db.Model): 108 | __tablename__ = "gino_companies" 109 | 110 | id = db.Column(db.BigInteger(), primary_key=True) 111 | name = db.Column(db.Unicode(255), default=_random_name) 112 | logo = db.Column(db.LargeBinary()) 113 | 114 | def __init__(self, **kw): 115 | super().__init__(**kw) 116 | self._teams = set() 117 | 118 | @property 119 | def teams(self): 120 | return self._teams 121 | 122 | @teams.setter 123 | def add_team(self, team): 124 | self._teams.add(team) 125 | 126 | 127 | class CompanyWithoutTeamsSetter(Company): 128 | def add_team(self, team): 129 | self._teams.add(team) 130 | 131 | 132 | class UserSetting(db.Model): 133 | __tablename__ = "gino_user_settings" 134 | 135 | # No constraints defined on columns 136 | id = db.Column(db.BigInteger()) 137 | user_id = db.Column(db.BigInteger()) 138 | setting = db.Column(db.VARCHAR(255)) 139 | value = db.Column(db.Text()) 140 | col1 = db.Column(db.Integer, default=1) 141 | col2 = db.Column(db.Integer, default=2) 142 | 143 | # Define indexes and constraints inline 144 | id_pkey = db.PrimaryKeyConstraint("id") 145 | user_id_fk = db.ForeignKeyConstraint(["user_id"], ["gino_users.id"]) 146 | user_id_setting_unique = db.UniqueConstraint("user_id", "setting") 147 | col1_check = db.CheckConstraint("col1 >= 1 AND col1 <= 5") 148 | col2_idx = db.Index("col2_idx", "col2") 149 | 150 | 151 | def qsize(engine): 152 | if isinstance(engine.raw_pool, aiomysql.pool.Pool): 153 | return engine.raw_pool.freesize 154 | if isinstance(engine.raw_pool, asyncpg.pool.Pool): 155 | # noinspection PyProtectedMember 156 | return engine.raw_pool._queue.qsize() 157 | raise Exception('Unknown pool') 158 | -------------------------------------------------------------------------------- /docs/images/python.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | download 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/how-to/transaction.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | Transaction 3 | =========== 4 | 5 | It is crucial to correctly manage transactions in an asynchronous program, 6 | because you never know how much time an ``await`` will actually wait for, it 7 | will cause disasters if transactions are on hold for too long. GINO enforces 8 | explicit transaction management to help dealing with it. 9 | 10 | 11 | Basic usage 12 | ----------- 13 | 14 | Transactions belong to :class:`~gino.engine.GinoConnection`. The most common 15 | way to use transactions is through an ``async with`` statement:: 16 | 17 | async with connection.transaction() as tx: 18 | await connection.status('INSERT INTO mytable VALUES(1, 2, 3)') 19 | 20 | This guarantees a transaction is opened when entering the ``async with`` block, 21 | and closed when exiting the block - committed if exits normally, or rolled back 22 | by exception. The underlying transaction instance from the database driver is 23 | available at :attr:`~gino.transaction.GinoTransaction.raw_transaction`, but in 24 | most cases you don't need to touch it. 25 | 26 | GINO provides two convenient shortcuts to end the transaction early: 27 | 28 | * :meth:`tx.raise_commit() ` 29 | * :meth:`tx.raise_rollback() ` 30 | 31 | They will raise an internal exception to correspondingly commit or rollback the 32 | transaction, thus the code within the ``async with`` block after 33 | :meth:`~gino.transaction.GinoTransaction.raise_commit` or 34 | :meth:`~gino.transaction.GinoTransaction.raise_rollback` is skipped. The 35 | internal exception is inherited from :exc:`BaseException` so that normal ``try 36 | ... except Exception`` block can't trap it. This exception stops propagating at 37 | the end of ``async with`` block, so you don't need to worry about handling it. 38 | 39 | Transactions can also be started on a :class:`~gino.engine.GinoEngine`:: 40 | 41 | async with engine.transaction() as tx: 42 | await engine.status('INSERT INTO mytable VALUES(1, 2, 3)') 43 | 44 | Here a :class:`~gino.engine.GinoConnection` is borrowed implicitly before 45 | entering the transaction, and guaranteed to be returned after transaction is 46 | done. The :class:`~gino.engine.GinoConnection` instance is accessible at 47 | :attr:`tx.connection `. Other than 48 | that, everything else is the same. 49 | 50 | .. important:: 51 | 52 | The implicit connection is by default borrowed with ``reuse=True``. That 53 | means using :meth:`~gino.engine.GinoEngine.transaction` of 54 | :class:`~gino.engine.GinoEngine` within a connection context is the same as 55 | calling :meth:`~gino.engine.GinoConnection.transaction` of the current 56 | connection without having to reference it, no separate connection shall be 57 | created. 58 | 59 | Similarly, if your :class:`~gino.api.Gino` instance has a bind, you may also do 60 | the same on it:: 61 | 62 | async with db.transaction() as tx: 63 | await db.status('INSERT INTO mytable VALUES(1, 2, 3)') 64 | 65 | 66 | Nested Transactions 67 | ------------------- 68 | 69 | Transactions can be nested, nested transaction will create a `savepoint 70 | `_ as for 71 | now on asyncpg. A similar example from asyncpg doc:: 72 | 73 | async with connection.transaction() as tx1: 74 | await connection.status('CREATE TABLE mytab (a int)') 75 | 76 | # Create a nested transaction: 77 | async with connection.transaction() as tx2: 78 | await connection.status('INSERT INTO mytab (a) VALUES (1), (2)') 79 | # Rollback the nested transaction: 80 | tx2.raise_rollback() 81 | 82 | # Because the nested transaction was rolled back, there 83 | # will be nothing in `mytab`. 84 | assert await connection.all('SELECT a FROM mytab') == [] 85 | 86 | As you can see, the :meth:`~gino.transaction.GinoTransaction.raise_rollback` 87 | breaks only the ``async with`` block of the specified ``tx2``, the outer 88 | transaction ``tx1`` just continued. What if we break the outer transaction from 89 | within the inner transaction? The inner transaction context won't trap the 90 | internal exception because it recognizes the exception is not created upon 91 | itself. Instead, the inner transaction context only follows the behavior to 92 | either commit or rollback, and lets the exception propagate. 93 | 94 | Because of the default reusing behavior, transactions on engine or ``db`` 95 | follows the same nesting rules. Please see 96 | :class:`~gino.transactions.GinoTransaction` for more information. 97 | 98 | 99 | Manual Control 100 | -------------- 101 | 102 | Other than using ``async with``, you can also manually control the 103 | transaction:: 104 | 105 | tx = await connection.transaction() 106 | try: 107 | await db.status('INSERT INTO mytable VALUES(1, 2, 3)') 108 | await tx.commit() 109 | except Exception: 110 | await tx.rollback() 111 | raise 112 | 113 | You can't use :meth:`~gino.transaction.GinoTransaction.raise_commit` or 114 | :meth:`~gino.transaction.GinoTransaction.raise_rollback` here, similarly it is 115 | prohibited to use :meth:`~gino.transaction.GinoTransaction.commit` and 116 | :meth:`~gino.transaction.GinoTransaction.rollback` in an ``async with`` block. 117 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: console 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Contributions are welcome, and they are greatly appreciated! Every 8 | little bit helps, and credit will always be given. 9 | 10 | You can contribute in many ways: 11 | 12 | Types of Contributions 13 | ---------------------- 14 | 15 | Report Bugs 16 | ~~~~~~~~~~~ 17 | 18 | Report bugs at https://github.com/python-gino/gino/issues. 19 | 20 | If you are reporting a bug, please include: 21 | 22 | * Your operating system name and version. 23 | * Any details about your local setup that might be helpful in troubleshooting. 24 | * Detailed steps to reproduce the bug. 25 | 26 | Fix Bugs 27 | ~~~~~~~~ 28 | 29 | Look through the GitHub issues for bugs. Anything tagged with "bug" 30 | and "help wanted" is open to whoever wants to implement it. 31 | 32 | Implement Features 33 | ~~~~~~~~~~~~~~~~~~ 34 | 35 | Look through the GitHub issues for features. Anything tagged with "enhancement" 36 | and "help wanted" is open to whoever wants to implement it. 37 | 38 | Write Documentation 39 | ~~~~~~~~~~~~~~~~~~~ 40 | 41 | GINO could always use more documentation, whether as part of the 42 | official GINO docs, in docstrings, or even on the web in blog posts, 43 | articles, and such. 44 | 45 | Submit Feedback 46 | ~~~~~~~~~~~~~~~ 47 | 48 | The best way to send feedback is to file an issue at https://github.com/python-gino/gino/issues. 49 | 50 | If you are proposing a feature: 51 | 52 | * Explain in detail how it would work. 53 | * Keep the scope as narrow as possible, to make it easier to implement. 54 | * Remember that this is a volunteer-driven project, and that contributions 55 | are welcome :) 56 | 57 | Get Started! 58 | ------------ 59 | 60 | Ready to contribute? Here's how to set up `gino` for local development. 61 | 62 | 1. Fork the `gino` repo on GitHub. 63 | 2. Clone your fork locally:: 64 | 65 | $ git clone git@github.com:your_name_here/gino.git 66 | 67 | 3. Create a branch for local development:: 68 | 69 | $ cd gino/ 70 | $ git checkout -b name-of-your-bugfix-or-feature 71 | 72 | Now you can make your changes locally. 73 | 74 | 4. Create virtual environment. Example for virtualenvwrapper:: 75 | 76 | $ mkvirtualenv gino 77 | 78 | 5. Activate the environment and install requirements:: 79 | 80 | $ pip install -r requirements_dev.txt 81 | 82 | 6. When you're done making changes, check that your changes pass syntax checks:: 83 | 84 | $ flake8 gino tests 85 | 86 | 7. And tests (including other Python versions with tox). 87 | For tests you you will need running database server (see "Tips" section below for configuration details):: 88 | 89 | $ pytest tests 90 | $ tox 91 | 92 | 8. For docs run:: 93 | 94 | $ make docs 95 | 96 | It will build and open up docs in your browser. 97 | 98 | 9. Commit your changes and push your branch to GitHub:: 99 | 100 | $ git add . 101 | $ git commit -m "Your detailed description of your changes." 102 | $ git push origin name-of-your-bugfix-or-feature 103 | 104 | 10. Submit a pull request through the GitHub website. 105 | 106 | Pull Request Guidelines 107 | ----------------------- 108 | 109 | Before you submit a pull request, check that it meets these guidelines: 110 | 111 | 1. The pull request should include tests. 112 | 2. If the pull request adds functionality, the docs should be updated. Put 113 | your new functionality into a function with a docstring, and add the 114 | feature to the list in README.rst. 115 | 3. The pull request should work for Python 3.6, 3.7, 3.8 and 3.9. Check 116 | https://github.com/python-gino/gino/actions?query=event%3Apull_request 117 | and make sure that the tests pass for all supported Python versions. 118 | 119 | Tips 120 | ---- 121 | 122 | To run a subset of tests:: 123 | 124 | $ py.test -svx tests.test_gino 125 | 126 | By default the tests run against a default installed postgres database. If you 127 | wish to run against a separate database for the tests you can do this by first 128 | creating a new database and user using 'psql' or similar:: 129 | 130 | CREATE ROLE gino WITH LOGIN ENCRYPTED PASSWORD 'gino'; 131 | CREATE DATABASE gino WITH OWNER = gino; 132 | 133 | Then run the tests like so:: 134 | 135 | $ export DB_USER=gino DB_PASS=gino DB_NAME=gino 136 | $ py.test 137 | 138 | Here is an example for db server in docker. Some tests require ssl so you will need to run postgres with ssl enabled. 139 | Terminal 1 (server):: 140 | 141 | $ openssl req -new -text -passout pass:abcd -subj /CN=localhost -out server.req -keyout privkey.pem 142 | $ openssl rsa -in privkey.pem -passin pass:abcd -out server.key 143 | $ openssl req -x509 -in server.req -text -key server.key -out server.crt 144 | $ chmod 600 server.key 145 | $ docker run --name gino_db --rm -it -p 5433:5432 -v "$(pwd)/server.crt:/var/lib/postgresql/server.crt:ro" -v "$(pwd)/server.key:/var/lib/postgresql/server.key:ro" postgres:12-alpine -c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key 146 | 147 | Terminal 2 (client):: 148 | 149 | $ export DB_USER=gino DB_PASS=gino DB_NAME=gino DB_PORT=5433 150 | $ docker exec gino_db psql -U postgres -c "CREATE ROLE $DB_USER WITH LOGIN ENCRYPTED PASSWORD '$DB_PASS'" 151 | $ docker exec gino_db psql -U postgres -c "CREATE DATABASE $DB_NAME WITH OWNER = $DB_USER;" 152 | $ pytest tests/test_aiohttp.py 153 | -------------------------------------------------------------------------------- /docs/images/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | sourcecode 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /mysql_tests/test_bakery.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sqlalchemy 3 | 4 | from gino import UninitializedError, create_engine, InitializedError 5 | from gino.bakery import Bakery, BakedQuery 6 | from .models import db, User, MYSQL_URL 7 | 8 | pytestmark = pytest.mark.asyncio 9 | 10 | 11 | @pytest.mark.parametrize( 12 | "query", 13 | [ 14 | User.query.where(User.id == db.bindparam("uid")), 15 | sqlalchemy.text("SELECT * FROM gino_users WHERE id = :uid"), 16 | "SELECT * FROM gino_users WHERE id = :uid", 17 | lambda: User.query.where(User.id == db.bindparam("uid")), 18 | lambda: sqlalchemy.text("SELECT * FROM gino_users WHERE id = :uid"), 19 | lambda: "SELECT * FROM gino_users WHERE id = :uid", 20 | ], 21 | ) 22 | @pytest.mark.parametrize("options", [dict(return_model=False), dict(loader=User)]) 23 | @pytest.mark.parametrize("api", [True, False]) 24 | @pytest.mark.parametrize("timeout", [None, 1]) 25 | async def test(query, options, sa_engine, api, timeout): 26 | uid = sa_engine.execute(User.insert()).lastrowid 27 | if timeout: 28 | options["timeout"] = timeout 29 | 30 | if api: 31 | b = db._bakery 32 | qs = [db.bake(query, **options)] 33 | if callable(query): 34 | qs.append(db.bake(**options)(query)) 35 | else: 36 | b = Bakery() 37 | qs = [b.bake(query, **options)] 38 | if callable(query): 39 | qs.append(b.bake(**options)(query)) 40 | 41 | for q in qs: 42 | assert isinstance(q, BakedQuery) 43 | assert q in list(b) 44 | assert q.sql is None 45 | assert q.compiled_sql is None 46 | 47 | with pytest.raises(UninitializedError): 48 | q.bind.first() 49 | with pytest.raises(UninitializedError): 50 | await q.first() 51 | 52 | for k, v in options.items(): 53 | assert q.query.get_execution_options()[k] == v 54 | 55 | if api: 56 | e = await db.set_bind(MYSQL_URL, minsize=1) 57 | else: 58 | e = await create_engine(MYSQL_URL, bakery=b, minsize=1) 59 | 60 | with pytest.raises(InitializedError): 61 | b.bake("SELECT now()") 62 | 63 | with pytest.raises(InitializedError): 64 | await create_engine(MYSQL_URL, bakery=b, minsize=0) 65 | 66 | try: 67 | for q in qs: 68 | assert q.sql is not None 69 | assert q.compiled_sql is not None 70 | 71 | if api: 72 | assert q.bind is e 73 | else: 74 | with pytest.raises(UninitializedError): 75 | q.bind.first() 76 | with pytest.raises(UninitializedError): 77 | await q.first() 78 | 79 | if api: 80 | rv = await q.first(uid=uid) 81 | else: 82 | rv = await e.first(q, uid=uid) 83 | 84 | if options.get("return_model", True): 85 | assert isinstance(rv, User) 86 | assert rv.id == uid 87 | else: 88 | assert rv[0] == rv[User.id] == rv["id"] == uid 89 | 90 | eq = q.execution_options(return_model=True, loader=User) 91 | assert eq is not q 92 | assert isinstance(eq, BakedQuery) 93 | assert type(eq) is not BakedQuery 94 | assert eq in list(b) 95 | assert eq.sql == q.sql 96 | assert eq.compiled_sql is not q.compiled_sql 97 | 98 | if api: 99 | assert q.bind is e 100 | else: 101 | with pytest.raises(UninitializedError): 102 | eq.bind.first() 103 | with pytest.raises(UninitializedError): 104 | await eq.first() 105 | 106 | assert eq.query.get_execution_options()["return_model"] 107 | assert eq.query.get_execution_options()["loader"] is User 108 | 109 | if api: 110 | rv = await eq.first(uid=uid) 111 | non = await eq.first(uid=uid + 1) 112 | rvl = await eq.all(uid=uid) 113 | else: 114 | rv = await e.first(eq, uid=uid) 115 | non = await e.first(eq, uid=uid + 1) 116 | rvl = await e.all(eq, uid=uid) 117 | 118 | assert isinstance(rv, User) 119 | assert rv.id == uid 120 | 121 | assert non is None 122 | 123 | assert len(rvl) == 1 124 | assert rvl[0].id == uid 125 | 126 | # original query is not affected 127 | if api: 128 | rv = await q.first(uid=uid) 129 | else: 130 | rv = await e.first(q, uid=uid) 131 | 132 | if options.get("return_model", True): 133 | assert isinstance(rv, User) 134 | assert rv.id == uid 135 | else: 136 | assert rv[0] == rv[User.id] == rv["id"] == uid 137 | 138 | finally: 139 | if api: 140 | await db.pop_bind().close() 141 | else: 142 | await e.close() 143 | 144 | 145 | async def test_class_level_bake(): 146 | class BakeOnClass(db.Model): 147 | __tablename__ = "bake_on_class_test" 148 | 149 | name = db.Column(db.String(255), primary_key=True) 150 | 151 | @db.bake 152 | def getter(cls): 153 | return cls.query.where(cls.name == db.bindparam("name")) 154 | 155 | async with db.with_bind(MYSQL_URL, prebake=False): 156 | await db.gino.create_all() 157 | try: 158 | await BakeOnClass.create(name="exist") 159 | assert (await BakeOnClass.getter.one(name="exist")).name == "exist" 160 | assert (await BakeOnClass.getter.one_or_none(name="nonexist")) is None 161 | finally: 162 | await db.gino.drop_all() 163 | -------------------------------------------------------------------------------- /docs/images/gino-fastapi-tests.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
gino-fastapi-demo
gino-...
tests
tests
test_users.py
tes...
conftest.py
con...
Viewer does not support full SVG 1.1
-------------------------------------------------------------------------------- /docs/how-to/json-props.rst: -------------------------------------------------------------------------------- 1 | JSON Property 2 | ============= 3 | 4 | GINO provides additional support to leverage native JSON type in the database as 5 | flexible GINO model fields. 6 | 7 | Quick Start 8 | ----------- 9 | 10 | :: 11 | 12 | from gino import Gino 13 | from sqlalchemy.dialects.postgresql import JSONB 14 | 15 | db = Gino() 16 | 17 | class User(db.Model): 18 | __tablename__ = "users" 19 | 20 | id = db.Column(db.Integer, primary_key=True) 21 | name = db.Column(db.String) 22 | profile = db.Column(JSONB, nullable=False, server_default="{}") 23 | 24 | age = db.IntegerProperty() 25 | birthday = db.DateTimeProperty() 26 | 27 | The ``age`` and ``birthday`` are JSON properties stored in the ``profile`` column. You 28 | may use them the same way as a normal GINO model field:: 29 | 30 | u = await User.create(name="daisy", age=18) 31 | print(u.name, u.age) # daisy 18 32 | 33 | .. note:: 34 | 35 | ``profile`` is the default column name for all JSON properties in a model. If you 36 | need a different column name for some JSON properties, you'll need to specify 37 | explicitly:: 38 | 39 | audit_profile = db.Column(JSON, nullable=False, server_default="{}") 40 | 41 | access_log = db.ArrayProperty(prop_name="audit_profile") 42 | abnormal_detected = db.BooleanProperty(prop_name="audit_profile") 43 | 44 | Using JSON properties in queries is supported:: 45 | 46 | await User.query.where(User.age > 16).gino.all() 47 | 48 | This is simply translated into a native JSON query like this: 49 | 50 | .. code-block:: plpgsql 51 | 52 | SELECT users.id, users.name, users.profile 53 | FROM users 54 | WHERE CAST((users.profile ->> $1) AS INTEGER) > $2; -- ('age', 16) 55 | 56 | Datetime type is very much the same:: 57 | 58 | from datetime import datetime 59 | 60 | await User.query.where(User.birthday > datetime(1990, 1, 1)).gino.all() 61 | 62 | And the generated SQL: 63 | 64 | .. code-block:: plpgsql 65 | 66 | SELECT users.id, users.name, users.profile 67 | FROM users 68 | WHERE CAST((users.profile ->> $1) AS TIMESTAMP WITHOUT TIME ZONE) > $2 69 | -- ('birthday', datetime.datetime(1990, 1, 1, 0, 0)) 70 | 71 | Here's a list of all the supported JSON properties: 72 | 73 | +----------------------------+-----------------------------+-------------+---------------+ 74 | | JSON Property | Python type | JSON type | Database Type | 75 | +============================+=============================+=============+===============+ 76 | | :class:`.StringProperty` | :class:`str` | ``string`` | ``text`` | 77 | +----------------------------+-----------------------------+-------------+---------------+ 78 | | :class:`.IntegerProperty` | :class:`int` | ``number`` | ``int`` | 79 | +----------------------------+-----------------------------+-------------+---------------+ 80 | | :class:`.BooleanProperty` | :class:`bool` | ``boolean`` | ``boolean`` | 81 | +----------------------------+-----------------------------+-------------+---------------+ 82 | | :class:`.DateTimeProperty` | :class:`~datetime.datetime` | ``string`` | ``text`` | 83 | +----------------------------+-----------------------------+-------------+---------------+ 84 | | :class:`.ObjectProperty` | :class:`dict` | ``object`` | JSON | 85 | +----------------------------+-----------------------------+-------------+---------------+ 86 | | :class:`.ArrayProperty` | :class:`list` | ``array`` | JSON | 87 | +----------------------------+-----------------------------+-------------+---------------+ 88 | 89 | 90 | Hooks 91 | ----- 92 | 93 | JSON property provides 2 instance-level hooks to customize the data:: 94 | 95 | class User(db.Model): 96 | __tablename__ = "users" 97 | 98 | id = db.Column(db.Integer, primary_key=True) 99 | profile = db.Column(JSONB, nullable=False, server_default="{}") 100 | 101 | age = db.IntegerProperty() 102 | 103 | @age.before_set 104 | def age(self, val): 105 | return val - 1 106 | 107 | @age.after_get 108 | def age(self, val): 109 | return val + 1 110 | 111 | u = await User.create(name="daisy", age=18) 112 | print(u.name, u.profile, u.age) # daisy {'age': 17} 18 113 | 114 | And 1 class-level hook to customize the SQLAlchemy expression of the property:: 115 | 116 | class User(db.Model): 117 | __tablename__ = "users" 118 | 119 | id = db.Column(db.Integer, primary_key=True) 120 | profile = db.Column(JSONB, nullable=False, server_default="{}") 121 | 122 | height = db.JSONProperty() 123 | 124 | @height.expression 125 | def height(cls, exp): 126 | return exp.cast(db.Float) # CAST(profile -> 'height' AS FLOAT) 127 | 128 | 129 | Create Index on JSON Properties 130 | ------------------------------- 131 | 132 | We'll need to use :meth:`~gino.declarative.declared_attr` to wait until the model class 133 | is initialized. The rest is very much the same as defining a usual index:: 134 | 135 | class User(db.Model): 136 | __tablename__ = "users" 137 | 138 | id = db.Column(db.Integer, primary_key=True) 139 | profile = db.Column(JSONB, nullable=False, server_default="{}") 140 | 141 | age = db.IntegerProperty() 142 | 143 | @db.declared_attr 144 | def age_idx(cls): 145 | return db.Index("age_idx", cls.age) 146 | 147 | This will lead to the SQL below executed if you run ``db.gino.create_all()``: 148 | 149 | .. code-block:: plpgsql 150 | 151 | CREATE INDEX age_idx ON users (CAST(profile ->> 'age' AS INTEGER)); 152 | 153 | .. warning:: 154 | 155 | Alembic doesn't support auto-generating revisions for functional indexes yet. You'll 156 | need to manually edit the revision. Please follow `this issue 157 | `__ for updates. 158 | --------------------------------------------------------------------------------