├── 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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
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 |
22 |
--------------------------------------------------------------------------------
/docs/theme/static/images/icon-info.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
26 |
--------------------------------------------------------------------------------
/docs/theme/static/images/explanation-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/docs/images/pycharm.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/docs/theme/static/images/icon-hint.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------