├── .gitignore ├── .travis.yml ├── CHANGES.rst ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── asyncpg_utils ├── __init__.py ├── databases.py ├── managers.py └── templates.py ├── examples ├── database.py ├── database_pool.py ├── table_manager.py ├── table_manager_filters.py ├── table_manager_hook.py ├── table_manager_joins.py ├── table_manager_transaction.py ├── table_manager_transaction_pool.py ├── transaction.py ├── transaction_pool.py └── utils.py ├── pytest.ini ├── requirements-dev.txt ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── conftest.py ├── test_databases.py └── test_managers.py /.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 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | env: 3 | - DATABASE_URL=postgresql://postgres:@localhost/travis_ci_test 4 | language: python 5 | python: 6 | - "3.5" 7 | - "3.6" 8 | - "3.7" 9 | services: 10 | - postgresql 11 | before_script: 12 | - psql -c 'create database travis_ci_test;' -U postgres 13 | # command to install dependencies 14 | install: 15 | - pip install -U -r requirements-dev.txt 16 | # command to run tests 17 | script: 18 | pytest 19 | after_success: 20 | codecov 21 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | --------- 3 | 4 | 0.6.0 5 | ~~~~~ 6 | 7 | * Update readme with link for examples . 8 | * Add init_pool coroutine to PoolDatabase. 9 | * Add support for filters operators. 10 | * Add joins support. 11 | 12 | 0.5.0 13 | ~~~~~ 14 | 15 | * Add limit/offset support. 16 | * Add count support. 17 | 18 | 0.4.1 19 | ~~~~~ 20 | 21 | * Fix fields on templates. 22 | 23 | 0.4.0 24 | ~~~~~ 25 | 26 | * Add TableManager order_by support. 27 | 28 | 0.3.0 29 | ~~~~~ 30 | 31 | * Use jinja2 templates for sql queries, drop PyPika from requirements. 32 | 33 | 0.2.1 34 | ~~~~~ 35 | 36 | * Fix requirements. 37 | 38 | 0.2.0 39 | ~~~~~ 40 | 41 | * Add TableManager and AbstractHook. 42 | 43 | 0.1.0 44 | ~~~~~ 45 | 46 | * Initial release. 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Allisson Azevedo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include CHANGES.rst 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-pyc 2 | 3 | default: test 4 | 5 | clean-pyc: 6 | @find . -iname '*.py[co]' -delete 7 | @find . -iname '__pycache__' -delete 8 | @find . -iname '.coverage' -delete 9 | @rm -rf htmlcov/ 10 | 11 | clean-dist: 12 | @rm -rf dist/ 13 | @rm -rf build/ 14 | @rm -rf *.egg-info 15 | 16 | clean: clean-pyc clean-dist 17 | 18 | test: 19 | py.test 20 | 21 | dist: clean 22 | python setup.py sdist 23 | python setup.py bdist_wheel 24 | 25 | release: dist 26 | git tag `python setup.py -q version` 27 | git push origin `python setup.py -q version` 28 | twine upload dist/* 29 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | asyncpg-utils 3 | ============= 4 | 5 | |TravisCI Build Status| |Coverage Status| |Requirements Status| |Version| 6 | 7 | ---- 8 | 9 | Utilities for Asyncpg. 10 | 11 | 12 | How to install 13 | ============== 14 | 15 | .. code:: shell 16 | 17 | pip install asyncpg-utils 18 | 19 | 20 | How to Use 21 | ========== 22 | 23 | * `Database `_ 24 | * `Database Pool `_ 25 | * `Transaction `_ 26 | * `Transaction Pool `_ 27 | * `TableManager `_ 28 | * `TableManager Filters `_ 29 | * `TableManager Joins `_ 30 | * `TableManager Hook `_ 31 | * `TableManager Transaction `_ 32 | * `TableManager Transaction Pool `_ 33 | 34 | Check `https://github.com/allisson/asyncpg-utils/tree/master/examples `_ for more code examples. 35 | 36 | .. |TravisCI Build Status| image:: https://travis-ci.org/allisson/asyncpg-utils.svg?branch=master 37 | :target: https://travis-ci.org/allisson/asyncpg-utils 38 | .. |Coverage Status| image:: https://codecov.io/gh/allisson/asyncpg-utils/branch/master/graph/badge.svg 39 | :target: https://codecov.io/gh/allisson/asyncpg-utils 40 | .. |Requirements Status| image:: https://requires.io/github/allisson/asyncpg-utils/requirements.svg?branch=master 41 | :target: https://requires.io/github/allisson/asyncpg-utils/requirements/?branch=master 42 | .. |Version| image:: https://badge.fury.io/py/asyncpg-utils.svg 43 | :target: https://badge.fury.io/py/asyncpg-utils 44 | -------------------------------------------------------------------------------- /asyncpg_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allisson/asyncpg-utils/616d5f47d7e01108cb9272b16e4d2b511bfdae08/asyncpg_utils/__init__.py -------------------------------------------------------------------------------- /asyncpg_utils/databases.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | import asyncpg 4 | 5 | 6 | class AbstractDatabase: 7 | @abc.abstractmethod 8 | async def get_connection(self): 9 | """A coroutine that returns a connection to database.""" 10 | 11 | @abc.abstractmethod 12 | async def call_connection_coroutine( 13 | self, coroutine_name, sql_query, *args, connection=None, 14 | timeout=None, close_connection=True): 15 | """A coroutine that executes another coroutine inside connection.""" 16 | 17 | async def query( 18 | self, sql_query, *args, connection=None, timeout=None, 19 | close_connection=True): 20 | return await self.call_connection_coroutine( 21 | 'fetch', sql_query, *args, connection=connection, timeout=timeout, 22 | close_connection=close_connection 23 | ) 24 | 25 | async def query_one( 26 | self, sql_query, *args, connection=None, timeout=None, 27 | close_connection=True): 28 | return await self.call_connection_coroutine( 29 | 'fetchrow', sql_query, *args, connection=connection, 30 | timeout=timeout, close_connection=close_connection 31 | ) 32 | 33 | async def insert( 34 | self, table_name, data, connection=None, timeout=None, 35 | close_connection=True): 36 | fields = [field_name for field_name in data.keys()] 37 | values = [field_value for field_name, field_value in data.items()] 38 | variables = tuple('${}'.format(x + 1) for x in range(len(fields))) 39 | sql_query = ( 40 | 'INSERT INTO {table_name} ' 41 | '({fields}) ' 42 | 'VALUES ({values}) ' 43 | 'RETURNING *' 44 | ).format( 45 | table_name=table_name, 46 | fields=', '.join('"{}"'.format(field) for field in fields), 47 | values=', '.join(variables), 48 | ) 49 | return await self.call_connection_coroutine( 50 | 'fetchrow', sql_query, *values, connection=connection, 51 | timeout=timeout, close_connection=close_connection 52 | ) 53 | 54 | 55 | class Database(AbstractDatabase): 56 | def __init__(self, dsn, **kwargs): 57 | self.dsn = dsn 58 | self.kwargs = kwargs 59 | 60 | async def get_connection(self): 61 | return await asyncpg.connect(self.dsn, **self.kwargs) 62 | 63 | async def call_connection_coroutine( 64 | self, coroutine_name, sql_query, *args, connection=None, 65 | timeout=None, close_connection=True): 66 | conn = connection or await self.get_connection() 67 | conn_coroutine = getattr(conn, coroutine_name) 68 | try: 69 | return await conn_coroutine(sql_query, *args, timeout=timeout) 70 | finally: 71 | if close_connection: 72 | await conn.close() 73 | 74 | 75 | class PoolDatabase(AbstractDatabase): 76 | def __init__(self, dsn, pool=None, **kwargs): 77 | self.dsn = dsn 78 | self.pool = pool 79 | self.kwargs = kwargs 80 | 81 | async def init_pool(self): 82 | if self.pool is None: 83 | self.pool = await asyncpg.create_pool(self.dsn, **self.kwargs) 84 | 85 | async def get_connection(self): 86 | return await self.pool.acquire() 87 | 88 | async def call_connection_coroutine( 89 | self, coroutine_name, sql_query, *args, connection=None, 90 | timeout=None, close_connection=True): 91 | conn = connection or await self.get_connection() 92 | conn_coroutine = getattr(conn, coroutine_name) 93 | try: 94 | return await conn_coroutine(sql_query, *args, timeout=timeout) 95 | finally: 96 | if close_connection: 97 | await self.pool.release(conn) 98 | -------------------------------------------------------------------------------- /asyncpg_utils/managers.py: -------------------------------------------------------------------------------- 1 | from .templates import ( 2 | sql_create_template, 3 | sql_delete_template, 4 | sql_detail_template, 5 | sql_list_template, 6 | sql_update_template, 7 | ) 8 | 9 | 10 | class AbstractHook: 11 | def __init__(self, table_manager): 12 | self.table_manager = table_manager 13 | 14 | async def trigger_event(self, event_name, *args, **kwargs): 15 | event_coroutine = getattr(self, event_name, None) 16 | if event_coroutine is None: 17 | return 18 | return await event_coroutine(*args, **kwargs) 19 | 20 | 21 | class TableManager: 22 | def __init__(self, database, table_name, pk_field='id', hooks=None): 23 | self.database = database 24 | self.table_name = table_name 25 | self.pk_field = pk_field 26 | self.hooks = [hook(self) for hook in hooks or []] 27 | 28 | def parse_filters(self, filters): 29 | result = {} 30 | 31 | for field, value in filters.items(): 32 | lookup = 'exact' 33 | if '__' in field: 34 | field, lookup = field.split('__') 35 | result[field] = {'lookup': lookup, 'value': value} 36 | 37 | return result 38 | 39 | async def trigger_hooks(self, event_name, *args, **kwargs): 40 | for hook in self.hooks: 41 | await hook.trigger_event(event_name, *args, **kwargs) 42 | 43 | async def create(self, data, **kwargs): 44 | field_names = [field_name for field_name in data.keys()] 45 | field_values = [field_value for _, field_value in data.items()] 46 | sql_query = sql_create_template.render({ 47 | 'table_name': self.table_name, 48 | 'field_names': field_names 49 | }) 50 | await self.trigger_hooks('pre_create', data) 51 | row = await self.database.query_one(sql_query, *field_values, **kwargs) 52 | await self.trigger_hooks('post_create', row) 53 | return row 54 | 55 | async def list( 56 | self, fields=None, filters=None, filters_operator='AND', 57 | joins=None, order_by=None, order_by_sort='ASC', count=False, 58 | limit=None, offset=None, **kwargs): 59 | filters = filters or {} 60 | filter_values = [filter_value for _, filter_value in filters.items()] 61 | joins = joins or {} 62 | sql_query = sql_list_template.render({ 63 | 'table_name': self.table_name, 64 | 'fields': fields, 65 | 'filters': self.parse_filters(filters), 66 | 'filters_operator': filters_operator, 67 | 'joins': joins, 68 | 'order_by': order_by, 69 | 'order_by_sort': order_by_sort, 70 | 'count': count, 71 | 'limit': limit, 72 | 'offset': offset 73 | }) 74 | await self.trigger_hooks( 75 | 'pre_list', fields, filters, order_by, order_by_sort, count, limit, 76 | offset 77 | ) 78 | rows = await self.database.query(sql_query, *filter_values, **kwargs) 79 | await self.trigger_hooks('post_list', rows) 80 | return rows 81 | 82 | async def detail(self, pk, pk_field=None, fields=None, **kwargs): 83 | pk_field = pk_field or self.pk_field 84 | sql_query = sql_detail_template.render({ 85 | 'table_name': self.table_name, 86 | 'fields': fields, 87 | 'pk_field': pk_field 88 | }) 89 | await self.trigger_hooks('pre_detail', pk, pk_field, fields) 90 | row = await self.database.query_one(sql_query, pk, **kwargs) 91 | await self.trigger_hooks('post_detail', row) 92 | return row 93 | 94 | async def update(self, pk, data, **kwargs): 95 | field_names = [field_name for field_name in data.keys()] 96 | field_values = [field_value for _, field_value in data.items()] 97 | sql_query = sql_update_template.render({ 98 | 'table_name': self.table_name, 99 | 'field_names': field_names, 100 | 'pk_field': self.pk_field 101 | }) 102 | await self.trigger_hooks('pre_update', pk, data) 103 | row = await self.database.query_one(sql_query, *field_values, pk, **kwargs) 104 | await self.trigger_hooks('post_update', row) 105 | return row 106 | 107 | async def delete(self, pk, **kwargs): 108 | sql_query = sql_delete_template.render({ 109 | 'table_name': self.table_name, 110 | 'pk_field': self.pk_field 111 | }) 112 | await self.trigger_hooks('pre_delete', pk) 113 | await self.database.query_one(sql_query, pk, **kwargs) 114 | await self.trigger_hooks('post_delete', pk) 115 | return True 116 | -------------------------------------------------------------------------------- /asyncpg_utils/templates.py: -------------------------------------------------------------------------------- 1 | from jinja2 import Template 2 | 3 | sql_create_template = Template( 4 | """ 5 | INSERT INTO {{ table_name }} 6 | ({% for field_name in field_names %}{{ field_name }}{% if not loop.last %}, {% endif %}{% endfor %}) 7 | VALUES 8 | ({% for field_name in field_names %}${{ loop.index }}{% if not loop.last %}, {% endif %}{% endfor %}) 9 | RETURNING * 10 | """ 11 | ) 12 | 13 | sql_list_template = Template( 14 | """ 15 | SELECT 16 | {% if count %} 17 | COUNT(1) 18 | {% else %} 19 | {% if not fields %}*{% else %}{% for field_name in fields %}{{ field_name }}{% if not loop.last %}, {% endif %}{% endfor %} {% endif %} 20 | {% endif %} 21 | FROM {{ table_name }} 22 | {% if joins %} 23 | {% for join_table, value in joins.items() %} 24 | {{ value['type'] }} {{ join_table }} ON {{ value['source'] }} = {{ value['target'] }} 25 | {% endfor %} 26 | {% endif %} 27 | {% if filters %} 28 | WHERE 29 | {% for field, value in filters.items() %} 30 | {% if value['lookup'] == 'exact' %} 31 | {{ field }} = ${{ loop.index }} 32 | {% elif value['lookup'] == 'like' %} 33 | {{ field }} LIKE ${{ loop.index }} 34 | {% elif value['lookup'] == 'ilike' %} 35 | {{ field }} ILIKE ${{ loop.index }} 36 | {% elif value['lookup'] == 'in' %} 37 | {{ field }} = any(${{ loop.index }}) 38 | {% elif value['lookup'] == 'gt' %} 39 | {{ field }} > ${{ loop.index }} 40 | {% elif value['lookup'] == 'gte' %} 41 | {{ field }} >= ${{ loop.index }} 42 | {% elif value['lookup'] == 'lt' %} 43 | {{ field }} < ${{ loop.index }} 44 | {% elif value['lookup'] == 'lte' %} 45 | {{ field }} <= ${{ loop.index }} 46 | {% endif %} 47 | {% if not loop.last %}{{ filters_operator }}{% endif %} 48 | {% endfor %} 49 | {% endif %} 50 | {% if order_by %}ORDER BY {{ order_by }} {{ order_by_sort }}{% endif %} 51 | {% if limit %}LIMIT {{ limit }}{% endif %} 52 | {% if offset %}OFFSET {{ offset }}{% endif %} 53 | """ 54 | ) 55 | 56 | sql_detail_template = Template( 57 | """ 58 | SELECT 59 | {% if not fields %}*{% else %}{% for field_name in fields %}{{ field_name }}{% if not loop.last %}, {% endif %}{% endfor %} {% endif %} 60 | FROM {{ table_name }} 61 | WHERE {{ pk_field }} = $1 62 | """ 63 | ) 64 | 65 | sql_update_template = Template( 66 | """ 67 | UPDATE {{ table_name }} 68 | SET 69 | {% for field_name in field_names %}{{ field_name }} = ${{ loop.index }}{% if not loop.last %}, {% endif %}{% endfor %} 70 | WHERE {{ pk_field }} = ${{ field_names|length + 1}} 71 | RETURNING * 72 | """ 73 | ) 74 | 75 | sql_delete_template = Template( 76 | """ 77 | DELETE FROM {{ table_name }} 78 | WHERE {{ pk_field }} = $1 79 | """ 80 | ) 81 | -------------------------------------------------------------------------------- /examples/database.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from datetime import date 3 | 4 | from asyncpg_utils.databases import Database 5 | 6 | from utils import create_table, drop_table 7 | 8 | loop = asyncio.get_event_loop() 9 | database = Database('postgresql://postgres:postgres@localhost/asyncpg-utils') 10 | 11 | 12 | async def insert_row(data): 13 | return await database.insert('users', data) 14 | 15 | 16 | async def query_all(): 17 | return await database.query( 18 | """ 19 | SELECT * FROM users 20 | """ 21 | ) 22 | 23 | 24 | async def query_one(): 25 | return await database.query_one( 26 | """ 27 | SELECT * FROM users 28 | WHERE name = $1 29 | """, 30 | 'Jane Doe' 31 | ) 32 | 33 | 34 | async def main(): 35 | await create_table(database) 36 | print('insert row, {!r}'.format(await insert_row({'name': 'John Doe', 'dob': date(2000, 1, 1)}))) 37 | print('insert row, {!r}'.format(await insert_row({'name': 'Jane Doe', 'dob': date(2000, 1, 1)}))) 38 | print('query all results, {!r}'.format(await query_all())) 39 | print('query one result, {!r}'.format(await query_one())) 40 | await drop_table(database) 41 | 42 | loop.run_until_complete(main()) 43 | -------------------------------------------------------------------------------- /examples/database_pool.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from datetime import date 3 | 4 | from asyncpg_utils.databases import PoolDatabase 5 | 6 | from utils import create_table, drop_table 7 | 8 | loop = asyncio.get_event_loop() 9 | database = PoolDatabase('postgresql://postgres:postgres@localhost/asyncpg-utils') 10 | 11 | 12 | async def insert_row(data): 13 | return await database.insert('users', data) 14 | 15 | 16 | async def query_all(): 17 | return await database.query( 18 | """ 19 | SELECT * FROM users 20 | """ 21 | ) 22 | 23 | 24 | async def query_one(): 25 | return await database.query_one( 26 | """ 27 | SELECT * FROM users 28 | WHERE name = $1 29 | """, 30 | 'Jane Doe' 31 | ) 32 | 33 | 34 | async def main(): 35 | await database.init_pool() 36 | await create_table(database) 37 | print('insert row, {!r}'.format(await insert_row({'name': 'John Doe', 'dob': date(2000, 1, 1)}))) 38 | print('insert row, {!r}'.format(await insert_row({'name': 'Jane Doe', 'dob': date(2000, 1, 1)}))) 39 | print('query all results, {!r}'.format(await query_all())) 40 | print('query one result, {!r}'.format(await query_one())) 41 | await drop_table(database) 42 | 43 | loop.run_until_complete(main()) 44 | -------------------------------------------------------------------------------- /examples/table_manager.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from datetime import date 3 | 4 | from asyncpg_utils.databases import Database 5 | from asyncpg_utils.managers import TableManager 6 | 7 | from utils import create_table, drop_table 8 | 9 | loop = asyncio.get_event_loop() 10 | database = Database('postgresql://postgres:postgres@localhost/asyncpg-utils') 11 | table_manager = TableManager(database, 'users', pk_field='id', hooks=None) 12 | user_data = { 13 | 'name': 'Allisson', 14 | 'dob': date(1983, 2, 9) 15 | } 16 | 17 | 18 | async def table_manager_create(): 19 | print('table_manager.create, row={!r}'.format(await table_manager.create(user_data))) 20 | 21 | 22 | async def table_manager_list(): 23 | print('table_manager.list, rows={!r}'.format(await table_manager.list())) 24 | print('table_manager.list, only_name_field, rows={!r}'.format(await table_manager.list(fields=['name']))) 25 | print('table_manager.list, filter_by_id, rows={!r}'.format(await table_manager.list(filters={'id': 999999}))) 26 | print('table_manager.list, order_by=name, order_by_sort=ASC, rows={!r}'.format(await table_manager.list(order_by='name', order_by_sort='ASC'))) 27 | print('table_manager.list, count=True, rows={!r}'.format(await table_manager.list(count=True))) 28 | print('table_manager.list, limit=1, offset=0, rows={!r}'.format(await table_manager.list(limit=1, offset=0))) 29 | 30 | 31 | async def table_manager_detail(): 32 | print('table_manager.detail, row={!r}'.format(await table_manager.detail(1))) 33 | print('table_manager.detail, only_name_field, row={!r}'.format(await table_manager.detail(1, fields=['name']))) 34 | 35 | 36 | async def table_manager_update(): 37 | user_data['name'] = 'John Doe' 38 | print('table_manager.update, row={!r}'.format(await table_manager.update(1, user_data))) 39 | 40 | 41 | async def table_manager_delete(): 42 | print('table_manager.delete, result={!r}'.format(await table_manager.delete(1))) 43 | 44 | 45 | async def main(): 46 | await create_table(database) 47 | await table_manager_create() 48 | await table_manager_list() 49 | await table_manager_detail() 50 | await table_manager_update() 51 | await table_manager_delete() 52 | await drop_table(database) 53 | 54 | loop.run_until_complete(main()) 55 | -------------------------------------------------------------------------------- /examples/table_manager_filters.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from datetime import date 3 | 4 | from asyncpg_utils.databases import Database 5 | from asyncpg_utils.managers import TableManager 6 | 7 | from utils import create_table, drop_table 8 | 9 | loop = asyncio.get_event_loop() 10 | database = Database('postgresql://postgres:postgres@localhost/asyncpg-utils') 11 | table_manager = TableManager(database, 'users', pk_field='id', hooks=None) 12 | user1_data = { 13 | 'name': 'Allisson', 14 | 'dob': date(1983, 2, 9) 15 | } 16 | user2_data = { 17 | 'name': 'John Doe', 18 | 'dob': date(2000, 1, 1) 19 | } 20 | 21 | 22 | async def main(): 23 | await create_table(database) 24 | user1_row = await table_manager.create(user1_data) 25 | user2_row = await table_manager.create(user2_data) 26 | print('table_manager.list, filter_by_name, rows={!r}'.format(await table_manager.list(filters={'name': user1_row['name']}))) 27 | print('table_manager.list, filter_by_name__exact, rows={!r}'.format(await table_manager.list(filters={'name__exact': user1_row['name']}))) 28 | print('table_manager.list, filter_by_name__like, rows={!r}'.format(await table_manager.list(filters={'name__like': '%Doe%'}))) 29 | print('table_manager.list, filter_by_name__ilike, rows={!r}'.format(await table_manager.list(filters={'name__ilike': '%doe%'}))) 30 | print('table_manager.list, filter_by_id__in, rows={!r}'.format(await table_manager.list(filters={'id__in': [user1_row['id'], user2_row['id']]}))) 31 | print('table_manager.list, filter_by_dob__gt, rows={!r}'.format(await table_manager.list(filters={'dob__gt': user1_row['dob']}))) 32 | print('table_manager.list, filter_by_dob__gte, rows={!r}'.format(await table_manager.list(filters={'dob__gte': user1_row['dob']}))) 33 | print('table_manager.list, filter_by_dob__lt, rows={!r}'.format(await table_manager.list(filters={'dob__lt': user2_row['dob']}))) 34 | print('table_manager.list, filter_by_dob__lte, rows={!r}'.format(await table_manager.list(filters={'dob__lte': user2_row['dob']}))) 35 | print('table_manager.list, filter_by_name_and_dob, rows={!r}'.format(await table_manager.list(filters={'name': user1_row['name'], 'dob': user1_row['dob']}, filters_operator='AND'))) 36 | print('table_manager.list, filter_by_name_or_dob, rows={!r}'.format(await table_manager.list(filters={'name': user1_row['name'], 'dob': user2_row['dob']}, filters_operator='OR'))) 37 | await drop_table(database) 38 | 39 | loop.run_until_complete(main()) 40 | -------------------------------------------------------------------------------- /examples/table_manager_hook.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from datetime import date 3 | 4 | from asyncpg_utils.databases import Database 5 | from asyncpg_utils.managers import AbstractHook, TableManager 6 | 7 | from utils import create_table, drop_table 8 | 9 | loop = asyncio.get_event_loop() 10 | database = Database('postgresql://postgres:postgres@localhost/asyncpg-utils') 11 | user_data = { 12 | 'name': 'Allisson', 13 | 'dob': date(1983, 2, 9) 14 | } 15 | 16 | 17 | class TestHook(AbstractHook): 18 | async def pre_create(self, *args, **kwargs): 19 | print('pre_create, args={!r}, kwargs={!r}'.format(args, kwargs)) 20 | 21 | async def post_create(self, *args, **kwargs): 22 | print('post_create, args={!r}, kwargs={!r}'.format(args, kwargs)) 23 | 24 | async def pre_list(self, *args, **kwargs): 25 | print('pre_list, args={!r}, kwargs={!r}'.format(args, kwargs)) 26 | 27 | async def post_list(self, *args, **kwargs): 28 | print('post_list, args={!r}, kwargs={!r}'.format(args, kwargs)) 29 | 30 | async def pre_detail(self, *args, **kwargs): 31 | print('pre_detail, args={!r}, kwargs={!r}'.format(args, kwargs)) 32 | 33 | async def post_detail(self, *args, **kwargs): 34 | print('post_detail, args={!r}, kwargs={!r}'.format(args, kwargs)) 35 | 36 | async def pre_update(self, *args, **kwargs): 37 | print('pre_update, args={!r}, kwargs={!r}'.format(args, kwargs)) 38 | 39 | async def post_update(self, *args, **kwargs): 40 | print('post_update, args={!r}, kwargs={!r}'.format(args, kwargs)) 41 | 42 | async def pre_delete(self, *args, **kwargs): 43 | print('pre_delete, args={!r}, kwargs={!r}'.format(args, kwargs)) 44 | 45 | async def post_delete(self, *args, **kwargs): 46 | print('post_delete, args={!r}, kwargs={!r}'.format(args, kwargs)) 47 | 48 | 49 | table_manager = TableManager(database, 'users', pk_field='id', hooks=(TestHook,)) 50 | 51 | 52 | async def main(): 53 | await create_table(database) 54 | await table_manager.create(user_data) 55 | await table_manager.list() 56 | await table_manager.detail(1) 57 | user_data['name'] = 'John Doe' 58 | await table_manager.update(1, user_data) 59 | await table_manager.delete(1) 60 | await drop_table(database) 61 | 62 | 63 | loop.run_until_complete(main()) 64 | -------------------------------------------------------------------------------- /examples/table_manager_joins.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from datetime import datetime 3 | 4 | from asyncpg_utils.databases import Database 5 | from asyncpg_utils.managers import TableManager 6 | 7 | loop = asyncio.get_event_loop() 8 | database = Database('postgresql://postgres:postgres@localhost/asyncpg-utils') 9 | post_table_manager = TableManager(database, 'posts') 10 | comment_table_manager = TableManager(database, 'comments') 11 | post_data = { 12 | 'title': 'Post Title', 13 | 'body': 'Post Body', 14 | 'pub_date': datetime.utcnow() 15 | } 16 | comment_data = { 17 | 'body': 'Comment Body', 18 | 'pub_date': datetime.utcnow() 19 | } 20 | 21 | 22 | async def create_table(database): 23 | conn = await database.get_connection() 24 | await conn.execute( 25 | """ 26 | CREATE TABLE IF NOT EXISTS posts( 27 | id serial PRIMARY KEY, 28 | title varchar(128), 29 | body text, 30 | pub_date timestamp 31 | ); 32 | CREATE TABLE IF NOT EXISTS comments( 33 | id serial PRIMARY KEY, 34 | post_id integer references posts(id), 35 | body text, 36 | pub_date timestamp 37 | ); 38 | """ 39 | ) 40 | await conn.close() 41 | return True 42 | 43 | 44 | async def drop_table(database): 45 | conn = await database.get_connection() 46 | await conn.execute( 47 | """ 48 | DROP TABLE comments; 49 | DROP TABLE posts; 50 | """ 51 | ) 52 | await conn.close() 53 | return True 54 | 55 | 56 | async def main(): 57 | await create_table(database) 58 | post_row = await post_table_manager.create(post_data) 59 | comment_data['post_id'] = post_row['id'] 60 | await comment_table_manager.create(comment_data) 61 | fields = ( 62 | 'comments.id', 63 | 'comments.body', 64 | 'comments.pub_date', 65 | 'posts.title as post_title', 66 | 'posts.body as post_body', 67 | 'posts.pub_date as post_pub_date' 68 | ) 69 | joins = {'posts': {'type': 'INNER JOIN', 'source': 'comments.post_id', 'target': 'posts.id'}} 70 | rows = await comment_table_manager.list(fields=fields, joins=joins) 71 | print('comment_table_manager.list, inner_join_posts, rows={!r}'.format(rows)) 72 | await drop_table(database) 73 | 74 | loop.run_until_complete(main()) 75 | -------------------------------------------------------------------------------- /examples/table_manager_transaction.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from contextlib import suppress 3 | from datetime import date 4 | 5 | from asyncpg_utils.databases import Database 6 | from asyncpg_utils.managers import TableManager 7 | 8 | from utils import create_table, drop_table 9 | 10 | loop = asyncio.get_event_loop() 11 | database = Database('postgresql://postgres:postgres@localhost/asyncpg-utils') 12 | table_manager = TableManager(database, 'users', pk_field='id', hooks=None) 13 | user_data = { 14 | 'name': 'Allisson', 15 | 'dob': date(1983, 2, 9) 16 | } 17 | 18 | 19 | async def main(): 20 | await create_table(database) 21 | 22 | conn = await table_manager.database.get_connection() 23 | with suppress(Exception): 24 | async with conn.transaction(): 25 | await table_manager.create(user_data, connection=conn, close_connection=False) 26 | raise Exception('BOOM') 27 | await conn.close() 28 | result = await table_manager.list(count=True) 29 | print('table users count, {}'.format(result[0]['count'])) 30 | 31 | await drop_table(database) 32 | 33 | loop.run_until_complete(main()) 34 | -------------------------------------------------------------------------------- /examples/table_manager_transaction_pool.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from contextlib import suppress 3 | from datetime import date 4 | 5 | from asyncpg_utils.databases import PoolDatabase 6 | from asyncpg_utils.managers import TableManager 7 | 8 | from utils import create_table, drop_table 9 | 10 | loop = asyncio.get_event_loop() 11 | database = PoolDatabase('postgresql://postgres:postgres@localhost/asyncpg-utils') 12 | table_manager = TableManager(database, 'users', pk_field='id', hooks=None) 13 | user_data = { 14 | 'name': 'Allisson', 15 | 'dob': date(1983, 2, 9) 16 | } 17 | 18 | 19 | async def main(): 20 | await database.init_pool() 21 | await create_table(database) 22 | 23 | conn = await table_manager.database.get_connection() 24 | with suppress(Exception): 25 | async with conn.transaction(): 26 | await table_manager.create(user_data, connection=conn, close_connection=False) 27 | raise Exception('BOOM') 28 | await table_manager.database.pool.release(conn) 29 | result = await table_manager.list(count=True) 30 | print('table users count, {}'.format(result[0]['count'])) 31 | 32 | await drop_table(database) 33 | 34 | loop.run_until_complete(main()) 35 | -------------------------------------------------------------------------------- /examples/transaction.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from contextlib import suppress 3 | from datetime import date 4 | 5 | from asyncpg_utils.databases import Database 6 | 7 | from utils import create_table, drop_table 8 | 9 | loop = asyncio.get_event_loop() 10 | database = Database('postgresql://postgres:postgres@localhost/asyncpg-utils') 11 | 12 | 13 | async def transaction_coroutine(conn): 14 | async with conn.transaction(): 15 | await database.insert('users', {'name': 'John Doe', 'dob': date(2000, 1, 1)}, connection=conn, close_connection=False) 16 | raise Exception('BOOM!') 17 | 18 | 19 | async def main(): 20 | await create_table(database) 21 | conn = await database.get_connection() 22 | 23 | with suppress(Exception): 24 | await transaction_coroutine(conn) 25 | 26 | await conn.close() 27 | 28 | result = await database.query_one( 29 | """ 30 | SELECT COUNT(1) FROM users 31 | """ 32 | ) 33 | print('table users count, {}'.format(result['count'])) 34 | await drop_table(database) 35 | 36 | loop.run_until_complete(main()) 37 | -------------------------------------------------------------------------------- /examples/transaction_pool.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from contextlib import suppress 3 | from datetime import date 4 | 5 | from asyncpg_utils.databases import PoolDatabase 6 | 7 | from utils import create_table, drop_table 8 | 9 | loop = asyncio.get_event_loop() 10 | database = PoolDatabase('postgresql://postgres:postgres@localhost/asyncpg-utils') 11 | 12 | 13 | async def transaction_coroutine(conn): 14 | async with conn.transaction(): 15 | await database.insert('users', {'name': 'John Doe', 'dob': date(2000, 1, 1)}, connection=conn, close_connection=False) 16 | raise Exception('BOOM!') 17 | 18 | 19 | async def main(): 20 | await database.init_pool() 21 | await create_table(database) 22 | conn = await database.get_connection() 23 | 24 | with suppress(Exception): 25 | await transaction_coroutine(conn) 26 | 27 | await conn.close() 28 | 29 | result = await database.query_one( 30 | """ 31 | SELECT COUNT(1) FROM users 32 | """ 33 | ) 34 | print('table users count, {}'.format(result['count'])) 35 | await drop_table(database) 36 | 37 | loop.run_until_complete(main()) 38 | -------------------------------------------------------------------------------- /examples/utils.py: -------------------------------------------------------------------------------- 1 | async def create_table(database): 2 | conn = await database.get_connection() 3 | await conn.execute( 4 | """ 5 | CREATE TABLE IF NOT EXISTS users( 6 | id serial PRIMARY KEY, 7 | name text, 8 | dob date 9 | ) 10 | """ 11 | ) 12 | await conn.close() 13 | return True 14 | 15 | 16 | async def drop_table(database): 17 | conn = await database.get_connection() 18 | await conn.execute( 19 | """ 20 | DROP TABLE users 21 | """ 22 | ) 23 | await conn.close() 24 | return True 25 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -vvv --cov=asyncpg_utils --cov-report=term-missing -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | asynctest 3 | codecov 4 | flake8 5 | pytest 6 | pytest-asyncio 7 | pytest-cov 8 | pytest-lazy-fixture 9 | twine 10 | wheel 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asyncpg>=0.14.0 2 | Jinja2>=2.10 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os 3 | import re 4 | 5 | from setuptools import setup, find_packages, Command 6 | 7 | here = os.path.abspath(os.path.dirname(__file__)) 8 | 9 | version = '0.0.0' 10 | changes = os.path.join(here, 'CHANGES.rst') 11 | match = r'^#*\s*(?P[0-9]+\.[0-9]+(\.[0-9]+)?)$' 12 | with codecs.open(changes, encoding='utf-8') as changes: 13 | for line in changes: 14 | res = re.match(match, line) 15 | if res: 16 | version = res.group('version') 17 | break 18 | 19 | # Get the long description 20 | with codecs.open(os.path.join(here, 'README.rst'), encoding='utf-8') as f: 21 | long_description = f.read() 22 | 23 | # Get version 24 | with codecs.open(os.path.join(here, 'CHANGES.rst'), encoding='utf-8') as f: 25 | changelog = f.read() 26 | 27 | # Get requirements 28 | with codecs.open(os.path.join(here, 'requirements.txt')) as f: 29 | install_requirements = [line.strip() for line in f.readlines()] 30 | 31 | tests_requirements = [ 32 | 'pytest', 33 | 'pytest-cov', 34 | ] 35 | 36 | 37 | class VersionCommand(Command): 38 | description = 'print library version' 39 | user_options = [] 40 | 41 | def initialize_options(self): 42 | pass 43 | 44 | def finalize_options(self): 45 | pass 46 | 47 | def run(self): 48 | print(version) 49 | 50 | 51 | setup( 52 | name='asyncpg-utils', 53 | version=version, 54 | description='Utilities for Asyncpg', 55 | long_description=long_description, 56 | url='https://github.com/allisson/asyncpg-utils', 57 | author='Allisson Azevedo', 58 | author_email='allisson@gmail.com', 59 | classifiers=[ 60 | 'Development Status :: 3 - Alpha', 61 | 'Intended Audience :: Developers', 62 | 'Programming Language :: Python :: 3.5', 63 | 'Programming Language :: Python :: 3.6', 64 | 'Topic :: Software Development :: Libraries', 65 | ], 66 | packages=find_packages(exclude=['docs', 'tests*']), 67 | setup_requires=['pytest-runner'], 68 | install_requires=install_requirements, 69 | tests_require=tests_requirements, 70 | cmdclass={ 71 | 'version': VersionCommand, 72 | }, 73 | ) 74 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allisson/asyncpg-utils/616d5f47d7e01108cb9272b16e4d2b511bfdae08/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | from datetime import datetime 4 | 5 | import asyncpg 6 | import asynctest 7 | import pytest 8 | 9 | from asyncpg_utils.databases import Database, PoolDatabase 10 | from asyncpg_utils.managers import AbstractHook, TableManager 11 | 12 | dsn = os.environ.get('DATABASE_URL') 13 | loop = asyncio.get_event_loop() 14 | 15 | 16 | async def create_table(dsn): 17 | conn = await asyncpg.connect(dsn) 18 | await conn.execute( 19 | """ 20 | CREATE TABLE IF NOT EXISTS posts( 21 | id serial PRIMARY KEY, 22 | title varchar(128), 23 | body text, 24 | pub_date timestamp 25 | ); 26 | CREATE TABLE IF NOT EXISTS comments( 27 | id serial PRIMARY KEY, 28 | post_id integer references posts(id), 29 | body text, 30 | pub_date timestamp 31 | ); 32 | """ 33 | ) 34 | await conn.close() 35 | 36 | 37 | async def drop_table(dsn): 38 | conn = await asyncpg.connect(dsn) 39 | await conn.execute( 40 | """ 41 | DROP TABLE posts CASCADE; 42 | DROP TABLE comments; 43 | """ 44 | ) 45 | await conn.close() 46 | 47 | 48 | async def clear_table(dsn): 49 | conn = await asyncpg.connect(dsn) 50 | await conn.execute( 51 | """ 52 | DELETE FROM comments; 53 | DELETE FROM posts; 54 | """ 55 | ) 56 | await conn.close() 57 | 58 | 59 | @pytest.fixture(scope='session', autouse=True) 60 | def table_setup(request): 61 | def teardown(): 62 | loop.run_until_complete(drop_table(dsn)) 63 | 64 | loop.run_until_complete(create_table(dsn)) 65 | request.addfinalizer(teardown) 66 | 67 | 68 | @pytest.fixture(scope='function', autouse=True) 69 | def table_clear(request): 70 | def teardown(): 71 | loop.run_until_complete(clear_table(dsn)) 72 | 73 | request.addfinalizer(teardown) 74 | 75 | 76 | @pytest.fixture 77 | def database(): 78 | return Database(dsn) 79 | 80 | 81 | @pytest.fixture 82 | def pool_database(): 83 | return PoolDatabase(dsn) 84 | 85 | 86 | @pytest.fixture 87 | def post_data(): 88 | return { 89 | 'title': 'Post Title', 90 | 'body': 'Post Body', 91 | 'pub_date': datetime.utcnow() 92 | } 93 | 94 | 95 | @pytest.fixture 96 | def comment_data(): 97 | return { 98 | 'body': 'Comment Body', 99 | 'pub_date': datetime.utcnow() 100 | } 101 | 102 | 103 | class TestHook(AbstractHook): 104 | def __init__(self, table_manager): 105 | super().__init__(table_manager) 106 | self.table_manager.hook_event_mock = asynctest.CoroutineMock() 107 | 108 | async def pre_create(self, *args, **kwargs): 109 | self.table_manager.hook_event_mock.pre_create(*args, **kwargs) 110 | 111 | async def post_create(self, *args, **kwargs): 112 | self.table_manager.hook_event_mock.post_create(*args, **kwargs) 113 | 114 | async def pre_list(self, *args, **kwargs): 115 | self.table_manager.hook_event_mock.pre_list(*args, **kwargs) 116 | 117 | async def post_list(self, *args, **kwargs): 118 | self.table_manager.hook_event_mock.post_list(*args, **kwargs) 119 | 120 | async def pre_detail(self, *args, **kwargs): 121 | self.table_manager.hook_event_mock.pre_detail(*args, **kwargs) 122 | 123 | async def post_detail(self, *args, **kwargs): 124 | self.table_manager.hook_event_mock.post_detail(*args, **kwargs) 125 | 126 | async def pre_update(self, *args, **kwargs): 127 | self.table_manager.hook_event_mock.pre_update(*args, **kwargs) 128 | 129 | async def post_update(self, *args, **kwargs): 130 | self.table_manager.hook_event_mock.post_update(*args, **kwargs) 131 | 132 | async def pre_delete(self, *args, **kwargs): 133 | self.table_manager.hook_event_mock.pre_delete(*args, **kwargs) 134 | 135 | async def post_delete(self, *args, **kwargs): 136 | self.table_manager.hook_event_mock.post_delete(*args, **kwargs) 137 | 138 | 139 | @pytest.fixture 140 | def post_table(database): 141 | return TableManager(database, 'posts', hooks=(TestHook,)) 142 | 143 | 144 | @pytest.fixture 145 | def pool_post_table(pool_database): 146 | return TableManager(pool_database, 'posts', hooks=(TestHook,)) 147 | 148 | 149 | @pytest.fixture 150 | def comment_table(database): 151 | return TableManager(database, 'comments', hooks=(TestHook,)) 152 | -------------------------------------------------------------------------------- /tests/test_databases.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from asyncpg_utils.databases import PoolDatabase 4 | 5 | pytestmark = pytest.mark.asyncio 6 | 7 | 8 | @pytest.mark.parametrize( 9 | 'selected_database', ( 10 | pytest.lazy_fixture('database'), 11 | pytest.lazy_fixture('pool_database') 12 | ) 13 | ) 14 | async def test_database_insert(selected_database, post_data): 15 | if isinstance(selected_database, PoolDatabase): 16 | await selected_database.init_pool() 17 | 18 | row = await selected_database.insert('posts', post_data) 19 | 20 | assert row['id'] 21 | assert row['title'] == post_data['title'] 22 | assert row['body'] == post_data['body'] 23 | assert row['pub_date'] == post_data['pub_date'] 24 | 25 | 26 | @pytest.mark.parametrize( 27 | 'selected_database', ( 28 | pytest.lazy_fixture('database'), 29 | pytest.lazy_fixture('pool_database') 30 | ) 31 | ) 32 | async def test_database_query(selected_database, post_data): 33 | if isinstance(selected_database, PoolDatabase): 34 | await selected_database.init_pool() 35 | 36 | await selected_database.insert('posts', post_data) 37 | rows = await selected_database.query( 38 | """ 39 | SELECT * FROM posts 40 | """ 41 | ) 42 | 43 | assert len(rows) == 1 44 | row = rows[0] 45 | assert row['id'] 46 | assert row['title'] == post_data['title'] 47 | assert row['body'] == post_data['body'] 48 | assert row['pub_date'] == post_data['pub_date'] 49 | 50 | 51 | @pytest.mark.parametrize( 52 | 'selected_database', ( 53 | pytest.lazy_fixture('database'), 54 | pytest.lazy_fixture('pool_database') 55 | ) 56 | ) 57 | async def test_database_query_one(selected_database, post_data): 58 | if isinstance(selected_database, PoolDatabase): 59 | await selected_database.init_pool() 60 | 61 | await selected_database.insert('posts', post_data) 62 | row = await selected_database.query_one( 63 | """ 64 | SELECT * FROM posts 65 | WHERE title = $1 66 | """, 67 | 'Post Title' 68 | ) 69 | 70 | assert row['id'] 71 | assert row['title'] == post_data['title'] 72 | assert row['body'] == post_data['body'] 73 | assert row['pub_date'] == post_data['pub_date'] 74 | 75 | 76 | async def transaction_coroutine(conn, selected_database, post_data): 77 | async with conn.transaction(): 78 | await selected_database.insert('posts', post_data, connection=conn, close_connection=False) 79 | await selected_database.insert('posts', post_data, connection=conn, close_connection=False) 80 | raise Exception('BOOM!') 81 | 82 | 83 | @pytest.mark.parametrize( 84 | 'selected_database', ( 85 | pytest.lazy_fixture('database'), 86 | pytest.lazy_fixture('pool_database') 87 | ) 88 | ) 89 | async def test_database_transaction(selected_database, post_data): 90 | if isinstance(selected_database, PoolDatabase): 91 | await selected_database.init_pool() 92 | 93 | conn = await selected_database.get_connection() 94 | 95 | with pytest.raises(Exception): 96 | await transaction_coroutine(conn, selected_database, post_data) 97 | 98 | await conn.close() 99 | 100 | rows = await selected_database.query( 101 | """ 102 | SELECT * FROM posts 103 | """ 104 | ) 105 | assert len(rows) == 0 106 | -------------------------------------------------------------------------------- /tests/test_managers.py: -------------------------------------------------------------------------------- 1 | from contextlib import suppress 2 | from datetime import datetime 3 | 4 | import pytest 5 | 6 | pytestmark = pytest.mark.asyncio 7 | 8 | 9 | async def test_post_table_create(post_table, post_data): 10 | row = await post_table.create(post_data) 11 | assert row['id'] 12 | assert row['title'] == post_data['title'] 13 | assert row['body'] == post_data['body'] 14 | assert row['pub_date'] == post_data['pub_date'] 15 | assert post_table.hook_event_mock.pre_create.called is True 16 | assert post_table.hook_event_mock.post_create.called is True 17 | 18 | 19 | async def test_post_table_list(post_table, post_data): 20 | await post_table.create(post_data) 21 | rows = await post_table.list() 22 | assert len(rows) == 1 23 | row = rows[0] 24 | assert row['id'] 25 | assert row['title'] == post_data['title'] 26 | assert row['body'] == post_data['body'] 27 | assert row['pub_date'] == post_data['pub_date'] 28 | assert post_table.hook_event_mock.pre_list.called is True 29 | assert post_table.hook_event_mock.post_list.called is True 30 | 31 | 32 | async def test_post_table_list_with_fields(post_table, post_data): 33 | await post_table.create(post_data) 34 | rows = await post_table.list(fields=['title']) 35 | assert len(rows) == 1 36 | row = rows[0] 37 | assert 'id' not in row 38 | assert row['title'] == post_data['title'] 39 | assert 'body' not in row 40 | assert 'pub_date' not in row 41 | 42 | 43 | async def test_post_table_list_with_count(post_table, post_data): 44 | await post_table.create(post_data) 45 | rows = await post_table.list(count=True) 46 | assert len(rows) == 1 47 | row = rows[0] 48 | assert row['count'] == 1 49 | 50 | 51 | async def test_post_table_list_with_exact_filters(post_table, post_data): 52 | post1_row = await post_table.create(post_data) 53 | post2_row = await post_table.create(post_data) 54 | 55 | rows = await post_table.list(filters={'id': post1_row['id']}) 56 | assert len(rows) == 1 57 | assert post1_row in rows 58 | assert post2_row not in rows 59 | 60 | rows = await post_table.list(filters={'id__exact': post1_row['id']}) 61 | assert len(rows) == 1 62 | assert post1_row in rows 63 | assert post2_row not in rows 64 | 65 | 66 | async def test_post_table_list_with_like_filters(post_table, post_data): 67 | post1_data = post_data.copy() 68 | post2_data = post_data.copy() 69 | post1_data['title'] = 'Post about Allisson' 70 | post2_data['title'] = 'Post about John Doe' 71 | post1_row = await post_table.create(post1_data) 72 | post2_row = await post_table.create(post2_data) 73 | rows = await post_table.list(filters={'title__like': '%Doe%'}) 74 | assert len(rows) == 1 75 | assert post1_row not in rows 76 | assert post2_row in rows 77 | 78 | 79 | async def test_post_table_list_with_ilike_filters(post_table, post_data): 80 | post1_data = post_data.copy() 81 | post2_data = post_data.copy() 82 | post1_data['title'] = 'Post about Allisson' 83 | post2_data['title'] = 'Post about John Doe' 84 | post1_row = await post_table.create(post1_data) 85 | post2_row = await post_table.create(post2_data) 86 | rows = await post_table.list(filters={'title__ilike': '%doe%'}) 87 | assert len(rows) == 1 88 | assert post1_row not in rows 89 | assert post2_row in rows 90 | 91 | 92 | async def test_post_table_list_with_in_filters(post_table, post_data): 93 | post1_row = await post_table.create(post_data) 94 | post2_row = await post_table.create(post_data) 95 | rows = await post_table.list(filters={'id__in': [post1_row['id'], post2_row['id']]}) 96 | assert len(rows) == 2 97 | assert post1_row in rows 98 | assert post2_row in rows 99 | rows = await post_table.list(filters={'id__in': [post1_row['id']]}) 100 | assert len(rows) == 1 101 | assert post1_row in rows 102 | assert post2_row not in rows 103 | 104 | 105 | async def test_post_table_list_with_gt_filters(post_table, post_data): 106 | post1_data = post_data.copy() 107 | post2_data = post_data.copy() 108 | post1_data['pub_date'] = datetime(2018, 1, 1, 0, 0, 0) 109 | post2_data['pub_date'] = datetime(2018, 1, 2, 0, 0, 0) 110 | post1_row = await post_table.create(post1_data) 111 | post2_row = await post_table.create(post2_data) 112 | rows = await post_table.list(filters={'pub_date__gt': post1_data['pub_date']}) 113 | assert len(rows) == 1 114 | assert post1_row not in rows 115 | assert post2_row in rows 116 | 117 | 118 | async def test_post_table_list_with_gte_filters(post_table, post_data): 119 | post1_data = post_data.copy() 120 | post2_data = post_data.copy() 121 | post1_data['pub_date'] = datetime(2018, 1, 1, 0, 0, 0) 122 | post2_data['pub_date'] = datetime(2018, 1, 2, 0, 0, 0) 123 | post1_row = await post_table.create(post1_data) 124 | post2_row = await post_table.create(post2_data) 125 | rows = await post_table.list(filters={'pub_date__gte': post1_data['pub_date']}) 126 | assert len(rows) == 2 127 | assert post1_row in rows 128 | assert post2_row in rows 129 | 130 | 131 | async def test_post_table_list_with_lt_filters(post_table, post_data): 132 | post1_data = post_data.copy() 133 | post2_data = post_data.copy() 134 | post1_data['pub_date'] = datetime(2018, 1, 1, 0, 0, 0) 135 | post2_data['pub_date'] = datetime(2018, 1, 2, 0, 0, 0) 136 | post1_row = await post_table.create(post1_data) 137 | post2_row = await post_table.create(post2_data) 138 | rows = await post_table.list(filters={'pub_date__lt': post2_data['pub_date']}) 139 | assert len(rows) == 1 140 | assert post1_row in rows 141 | assert post2_row not in rows 142 | 143 | 144 | async def test_post_table_list_with_lte_filters(post_table, post_data): 145 | post1_data = post_data.copy() 146 | post2_data = post_data.copy() 147 | post1_data['pub_date'] = datetime(2018, 1, 1, 0, 0, 0) 148 | post2_data['pub_date'] = datetime(2018, 1, 2, 0, 0, 0) 149 | post1_row = await post_table.create(post1_data) 150 | post2_row = await post_table.create(post2_data) 151 | rows = await post_table.list(filters={'pub_date__lte': post2_data['pub_date']}) 152 | assert len(rows) == 2 153 | assert post1_row in rows 154 | assert post2_row in rows 155 | 156 | 157 | async def test_post_table_list_with_multiple_filters(post_table, post_data): 158 | post1_data = post_data.copy() 159 | post2_data = post_data.copy() 160 | post1_data['title'] = 'Title 1' 161 | post2_data['title'] = 'Title 2' 162 | post1_row = await post_table.create(post1_data) 163 | post2_row = await post_table.create(post2_data) 164 | 165 | rows = await post_table.list( 166 | filters={'title': post1_data['title'], 'pub_date': post1_row['pub_date']}, 167 | filters_operator='AND' 168 | ) 169 | assert len(rows) == 1 170 | assert post1_row in rows 171 | assert post2_row not in rows 172 | 173 | rows = await post_table.list( 174 | filters={'title': post1_data['title'], 'pub_date': post2_row['pub_date']}, 175 | filters_operator='OR' 176 | ) 177 | assert len(rows) == 2 178 | assert post1_row in rows 179 | assert post2_row in rows 180 | 181 | 182 | async def test_post_table_list_with_order_by(post_table, post_data): 183 | post1_data = post_data.copy() 184 | post2_data = post_data.copy() 185 | post1_data['title'] = 'Title 1' 186 | post2_data['title'] = 'Title 2' 187 | post1_row = await post_table.create(post1_data) 188 | post2_row = await post_table.create(post2_data) 189 | 190 | rows = await post_table.list(order_by='title', order_by_sort='ASC') 191 | assert rows[0]['title'] == post1_row['title'] 192 | assert rows[1]['title'] == post2_row['title'] 193 | 194 | rows = await post_table.list(order_by='title', order_by_sort='DESC') 195 | assert rows[0]['title'] == post2_row['title'] 196 | assert rows[1]['title'] == post1_row['title'] 197 | 198 | 199 | async def test_post_table_list_with_limit_offset(post_table, post_data): 200 | post1_data = post_data.copy() 201 | post2_data = post_data.copy() 202 | post1_data['title'] = 'Title 1' 203 | post2_data['title'] = 'Title 2' 204 | post1_row = await post_table.create(post1_data) 205 | post2_row = await post_table.create(post2_data) 206 | 207 | rows = await post_table.list(limit=1, offset=0) 208 | assert len(rows) == 1 209 | assert rows[0]['title'] == post1_row['title'] 210 | 211 | rows = await post_table.list(limit=1, offset=1) 212 | assert len(rows) == 1 213 | assert rows[0]['title'] == post2_row['title'] 214 | 215 | 216 | async def test_post_table_list_with_joins(post_table, comment_table, post_data, comment_data): 217 | fields = ( 218 | 'comments.id', 219 | 'comments.body', 220 | 'comments.pub_date', 221 | 'posts.title as post_title', 222 | 'posts.body as post_body', 223 | 'posts.pub_date as post_pub_date' 224 | ) 225 | joins = {'posts': {'type': 'INNER JOIN', 'source': 'comments.post_id', 'target': 'posts.id'}} 226 | post_row = await post_table.create(post_data) 227 | comment_data['post_id'] = post_row['id'] 228 | comment_row = await comment_table.create(comment_data) 229 | 230 | rows = await comment_table.list(fields=fields, joins=joins, limit=1, offset=0) 231 | assert len(rows) == 1 232 | row = rows[0] 233 | assert row['id'] == comment_row['id'] 234 | assert row['body'] == comment_row['body'] 235 | assert row['pub_date'] == comment_row['pub_date'] 236 | assert row['post_title'] == post_row['title'] 237 | assert row['post_body'] == post_row['body'] 238 | assert row['post_pub_date'] == post_row['pub_date'] 239 | 240 | 241 | async def test_post_table_detail(post_table, post_data): 242 | created_row = await post_table.create(post_data) 243 | selected_row = await post_table.detail(created_row['id']) 244 | assert created_row['id'] == selected_row['id'] 245 | assert selected_row['id'] 246 | assert selected_row['title'] == post_data['title'] 247 | assert selected_row['body'] == post_data['body'] 248 | assert selected_row['pub_date'] == post_data['pub_date'] 249 | assert post_table.hook_event_mock.pre_detail.called is True 250 | assert post_table.hook_event_mock.post_detail.called is True 251 | 252 | 253 | async def test_post_table_detail_with_fields(post_table, post_data): 254 | created_row = await post_table.create(post_data) 255 | selected_row = await post_table.detail(created_row['id'], fields=['title']) 256 | assert created_row['title'] == selected_row['title'] 257 | assert 'id' not in selected_row 258 | assert selected_row['title'] == post_data['title'] 259 | assert 'body' not in selected_row 260 | assert 'pub_date' not in selected_row 261 | 262 | 263 | async def test_post_table_detail_with_other_pk_field(post_table, post_data): 264 | created_row = await post_table.create(post_data) 265 | selected_row = await post_table.detail(created_row['title'], pk_field='title') 266 | assert created_row['id'] == selected_row['id'] 267 | assert selected_row['id'] 268 | assert selected_row['title'] == post_data['title'] 269 | assert selected_row['body'] == post_data['body'] 270 | assert selected_row['pub_date'] == post_data['pub_date'] 271 | 272 | 273 | async def test_post_table_update(post_table, post_data): 274 | row = await post_table.create(post_data) 275 | id = row['id'] 276 | assert row['title'] == post_data['title'] 277 | assert row['body'] == post_data['body'] 278 | assert row['pub_date'] == post_data['pub_date'] 279 | update_post_data = { 280 | 'title': 'Post about John Doe', 281 | 'pub_date': datetime(2000, 1, 1, 0, 0, 0) 282 | } 283 | row = await post_table.update(id, update_post_data) 284 | assert row['title'] == update_post_data['title'] 285 | assert row['pub_date'] == update_post_data['pub_date'] 286 | assert post_table.hook_event_mock.pre_update.called is True 287 | assert post_table.hook_event_mock.post_update.called is True 288 | 289 | 290 | async def test_post_table_delete(post_table, post_data): 291 | row = await post_table.create(post_data) 292 | id = row['id'] 293 | assert await post_table.delete(id) is True 294 | rows = await post_table.list() 295 | assert len(rows) == 0 296 | assert post_table.hook_event_mock.pre_delete.called is True 297 | assert post_table.hook_event_mock.post_delete.called is True 298 | 299 | 300 | async def test_post_table_transaction(post_table, post_data): 301 | conn = await post_table.database.get_connection() 302 | with suppress(Exception): 303 | async with conn.transaction(): 304 | await post_table.create(post_data, connection=conn, close_connection=False) 305 | raise Exception('BOOM') 306 | await conn.close() 307 | rows = await post_table.list() 308 | assert len(rows) == 0 309 | 310 | 311 | async def test_pool_post_table_transaction(pool_post_table, post_data): 312 | await pool_post_table.database.init_pool() 313 | conn = await pool_post_table.database.get_connection() 314 | with suppress(Exception): 315 | async with conn.transaction(): 316 | await pool_post_table.create(post_data, connection=conn, close_connection=False) 317 | raise Exception('BOOM') 318 | await pool_post_table.database.pool.release(conn) 319 | rows = await pool_post_table.list() 320 | assert len(rows) == 0 321 | 322 | 323 | async def test_hook_trigger_invalid_event(post_table, post_data): 324 | hook = post_table.hooks[0] 325 | assert await hook.trigger_event('invalid_event') is None 326 | 327 | 328 | @pytest.mark.parametrize('filters,expected_result', [ 329 | ({'field': 'value'}, {'field': {'lookup': 'exact', 'value': 'value'}}), 330 | ({'field__exact': 'value'}, {'field': {'lookup': 'exact', 'value': 'value'}}), 331 | ({'field__like': 'value'}, {'field': {'lookup': 'like', 'value': 'value'}}), 332 | ({'field__ilike': 'value'}, {'field': {'lookup': 'ilike', 'value': 'value'}}), 333 | ({'field__in': 'value'}, {'field': {'lookup': 'in', 'value': 'value'}}), 334 | ({'field__gt': 'value'}, {'field': {'lookup': 'gt', 'value': 'value'}}), 335 | ({'field__gte': 'value'}, {'field': {'lookup': 'gte', 'value': 'value'}}), 336 | ({'field__lt': 'value'}, {'field': {'lookup': 'lt', 'value': 'value'}}), 337 | ({'field__lte': 'value'}, {'field': {'lookup': 'lte', 'value': 'value'}}), 338 | ]) 339 | async def test_post_table_parse_filters(filters, expected_result, post_table): 340 | result = post_table.parse_filters(filters) 341 | assert result == expected_result 342 | --------------------------------------------------------------------------------