├── .appveyor.yml ├── .coveragerc ├── .gitignore ├── .readthedocs.yml ├── .style.yapf ├── .travis.yml ├── CHEATSHEET.rst ├── LICENSE ├── LICENSE.APACHE2 ├── LICENSE.MIT ├── MANIFEST.in ├── README.rst ├── ci ├── rtd-requirements.txt └── travis.sh ├── docs ├── Makefile ├── make.bat └── source │ ├── _static │ └── .gitkeep │ ├── conf.py │ ├── history.rst │ └── index.rst ├── newsfragments ├── .gitkeep └── README.rst ├── pyproject.toml ├── setup.py ├── test-requirements.txt └── triopg ├── __init__.py ├── _tests ├── __init__.py ├── conftest.py └── test_triopg.py ├── _triopg.py ├── _version.py └── exceptions.py /.appveyor.yml: -------------------------------------------------------------------------------- 1 | skip_tags: true 2 | 3 | os: Visual Studio 2015 4 | 5 | services: 6 | - postgresql96 7 | 8 | environment: 9 | global: 10 | PGINSTALLATION: "C:\\Program Files\\PostgreSQL\\9.6\\bin" 11 | matrix: 12 | - PYTHON: "C:\\Python36" 13 | - PYTHON: "C:\\Python36-x64" 14 | 15 | build_script: 16 | - "PATH=C:\\Program Files\\PostgreSQL\\9.6\\bin\\;%PATH%" 17 | - "which pg_config" 18 | - "pg_config --version" 19 | - "git --no-pager log -n2" 20 | - "echo %APPVEYOR_REPO_COMMIT%" 21 | - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;;%PATH%" 22 | - "python --version" 23 | - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" 24 | - "pip install ." 25 | - "pip install -Ur test-requirements.txt" 26 | - "pip install codecov" 27 | 28 | test_script: 29 | - "mkdir empty" 30 | - "cd empty" 31 | # Make sure it's being imported from where we expect 32 | - "python -c \"import os, triopg; print(os.path.dirname(triopg.__file__))\"" 33 | - "python -u -m pytest -ra -v -s --pyargs triopg --cov=triopg --cov-config=../.coveragerc" 34 | - "codecov" 35 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch=True 3 | source=triopg 4 | 5 | [report] 6 | precision = 1 7 | exclude_lines = 8 | pragma: no cover 9 | abc.abstractmethod 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Add any project-specific files here: 2 | 3 | 4 | # Sphinx docs 5 | docs/build/ 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *~ 11 | \#* 12 | .#* 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | /build/ 20 | /develop-eggs/ 21 | /dist/ 22 | /eggs/ 23 | /lib/ 24 | /lib64/ 25 | /parts/ 26 | /sdist/ 27 | /var/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # Installer logs 33 | pip-log.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .coverage.* 40 | .cache 41 | .pytest_cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | 48 | # Mr Developer 49 | .mr.developer.cfg 50 | .project 51 | .pydevproject 52 | 53 | # Rope 54 | .ropeproject 55 | 56 | # Django stuff: 57 | *.log 58 | *.pot 59 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # https://docs.readthedocs.io/en/latest/yaml-config.html 2 | formats: 3 | - htmlzip 4 | - epub 5 | 6 | requirements_file: ci/rtd-requirements.txt 7 | 8 | # Currently RTD's default image only has 3.5 9 | # This gets us 3.6 (and hopefully 3.7 in the future) 10 | # https://docs.readthedocs.io/en/latest/yaml-config.html#build-image 11 | build: 12 | image: latest 13 | 14 | python: 15 | version: 3 16 | pip_install: True 17 | -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | # Align closing bracket with visual indentation. 3 | align_closing_bracket_with_visual_indent=True 4 | 5 | # Allow dictionary keys to exist on multiple lines. For example: 6 | # 7 | # x = { 8 | # ('this is the first element of a tuple', 9 | # 'this is the second element of a tuple'): 10 | # value, 11 | # } 12 | allow_multiline_dictionary_keys=False 13 | 14 | # Allow lambdas to be formatted on more than one line. 15 | allow_multiline_lambdas=False 16 | 17 | # Insert a blank line before a class-level docstring. 18 | blank_line_before_class_docstring=False 19 | 20 | # Insert a blank line before a 'def' or 'class' immediately nested 21 | # within another 'def' or 'class'. For example: 22 | # 23 | # class Foo: 24 | # # <------ this blank line 25 | # def method(): 26 | # ... 27 | blank_line_before_nested_class_or_def=False 28 | 29 | # Do not split consecutive brackets. Only relevant when 30 | # dedent_closing_brackets is set. For example: 31 | # 32 | # call_func_that_takes_a_dict( 33 | # { 34 | # 'key1': 'value1', 35 | # 'key2': 'value2', 36 | # } 37 | # ) 38 | # 39 | # would reformat to: 40 | # 41 | # call_func_that_takes_a_dict({ 42 | # 'key1': 'value1', 43 | # 'key2': 'value2', 44 | # }) 45 | coalesce_brackets=False 46 | 47 | # The column limit. 48 | column_limit=79 49 | 50 | # Indent width used for line continuations. 51 | continuation_indent_width=4 52 | 53 | # Put closing brackets on a separate line, dedented, if the bracketed 54 | # expression can't fit in a single line. Applies to all kinds of brackets, 55 | # including function definitions and calls. For example: 56 | # 57 | # config = { 58 | # 'key1': 'value1', 59 | # 'key2': 'value2', 60 | # } # <--- this bracket is dedented and on a separate line 61 | # 62 | # time_series = self.remote_client.query_entity_counters( 63 | # entity='dev3246.region1', 64 | # key='dns.query_latency_tcp', 65 | # transform=Transformation.AVERAGE(window=timedelta(seconds=60)), 66 | # start_ts=now()-timedelta(days=3), 67 | # end_ts=now(), 68 | # ) # <--- this bracket is dedented and on a separate line 69 | dedent_closing_brackets=True 70 | 71 | # Place each dictionary entry onto its own line. 72 | each_dict_entry_on_separate_line=True 73 | 74 | # The regex for an i18n comment. The presence of this comment stops 75 | # reformatting of that line, because the comments are required to be 76 | # next to the string they translate. 77 | i18n_comment= 78 | 79 | # The i18n function call names. The presence of this function stops 80 | # reformattting on that line, because the string it has cannot be moved 81 | # away from the i18n comment. 82 | i18n_function_call= 83 | 84 | # Indent the dictionary value if it cannot fit on the same line as the 85 | # dictionary key. For example: 86 | # 87 | # config = { 88 | # 'key1': 89 | # 'value1', 90 | # 'key2': value1 + 91 | # value2, 92 | # } 93 | indent_dictionary_value=True 94 | 95 | # The number of columns to use for indentation. 96 | indent_width=4 97 | 98 | # Join short lines into one line. E.g., single line 'if' statements. 99 | join_multiple_lines=False 100 | 101 | # Use spaces around default or named assigns. 102 | spaces_around_default_or_named_assign=False 103 | 104 | # Use spaces around the power operator. 105 | spaces_around_power_operator=False 106 | 107 | # The number of spaces required before a trailing comment. 108 | spaces_before_comment=2 109 | 110 | # Insert a space between the ending comma and closing bracket of a list, 111 | # etc. 112 | space_between_ending_comma_and_closing_bracket=False 113 | 114 | # Split before arguments if the argument list is terminated by a 115 | # comma. 116 | split_arguments_when_comma_terminated=True 117 | 118 | # Set to True to prefer splitting before '&', '|' or '^' rather than 119 | # after. 120 | split_before_bitwise_operator=True 121 | 122 | # Split before a dictionary or set generator (comp_for). For example, note 123 | # the split before the 'for': 124 | # 125 | # foo = { 126 | # variable: 'Hello world, have a nice day!' 127 | # for variable in bar if variable != 42 128 | # } 129 | split_before_dict_set_generator=True 130 | 131 | # If an argument / parameter list is going to be split, then split before 132 | # the first argument. 133 | split_before_first_argument=True 134 | 135 | # Set to True to prefer splitting before 'and' or 'or' rather than 136 | # after. 137 | split_before_logical_operator=True 138 | 139 | # Split named assignments onto individual lines. 140 | split_before_named_assigns=True 141 | 142 | # The penalty for splitting right after the opening bracket. 143 | split_penalty_after_opening_bracket=30 144 | 145 | # The penalty for splitting the line after a unary operator. 146 | split_penalty_after_unary_operator=10000 147 | 148 | # The penalty for splitting right before an if expression. 149 | split_penalty_before_if_expr=0 150 | 151 | # The penalty of splitting the line around the '&', '|', and '^' 152 | # operators. 153 | split_penalty_bitwise_operator=300 154 | 155 | # The penalty for characters over the column limit. 156 | split_penalty_excess_character=4500 157 | 158 | # The penalty incurred by adding a line split to the unwrapped line. The 159 | # more line splits added the higher the penalty. 160 | split_penalty_for_added_line_split=30 161 | 162 | # The penalty of splitting a list of "import as" names. For example: 163 | # 164 | # from a_very_long_or_indented_module_name_yada_yad import (long_argument_1, 165 | # long_argument_2, 166 | # long_argument_3) 167 | # 168 | # would reformat to something like: 169 | # 170 | # from a_very_long_or_indented_module_name_yada_yad import ( 171 | # long_argument_1, long_argument_2, long_argument_3) 172 | split_penalty_import_names=0 173 | 174 | # The penalty of splitting the line around the 'and' and 'or' 175 | # operators. 176 | split_penalty_logical_operator=0 177 | 178 | # Use the Tab character for indentation. 179 | use_tabs=False 180 | 181 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | dist: trusty 4 | 5 | addons: 6 | postgresql: "9.6" 7 | apt: 8 | packages: 9 | - postgresql-server-dev-9.6 10 | 11 | services: 12 | - postgresql 13 | 14 | matrix: 15 | include: 16 | # These are quick and often catch errors, so list them first 17 | - python: 3.6 18 | env: CHECK_DOCS=1 19 | - python: 3.6 20 | env: CHECK_FORMATTING=1 21 | - python: 3.6 22 | # As of 2018-07-05, Travis's 3.7 and 3.8 builds only work if you 23 | # use dist: xenial AND sudo: required 24 | # See: https://github.com/python-trio/trio/pull/556#issuecomment-402879391 25 | - python: 3.7 26 | dist: xenial 27 | sudo: required 28 | # Currently asyncpg uses deprecated `loop` argument, which cause error 29 | # due to pytest run in `-W error` mode 30 | # - python: 3.8-dev 31 | # dist: xenial 32 | # sudo: required 33 | - os: osx 34 | language: generic 35 | env: MACPYTHON=3.6.6 36 | - os: osx 37 | language: generic 38 | env: MACPYTHON=3.7.0 39 | 40 | before_script: 41 | - which pg_config 42 | - pg_config --version 43 | 44 | script: 45 | - ci/travis.sh 46 | -------------------------------------------------------------------------------- /CHEATSHEET.rst: -------------------------------------------------------------------------------- 1 | Tips 2 | ==== 3 | 4 | To run tests 5 | ------------ 6 | 7 | * Install requirements: ``pip install -r test-requirements.txt`` 8 | (possibly in a virtualenv) 9 | 10 | * Actually run the tests: ``pytest triopg`` 11 | 12 | 13 | To run yapf 14 | ----------- 15 | 16 | * Show what changes yapf wants to make: ``yapf -rpd setup.py 17 | triopg`` 18 | 19 | * Apply all changes directly to the source tree: ``yapf -rpi setup.py 20 | triopg`` 21 | 22 | 23 | To make a release 24 | ----------------- 25 | 26 | * Update the version in ``triopg/_version.py`` 27 | 28 | * Run ``towncrier`` to collect your release notes. 29 | 30 | * Review your release notes. 31 | 32 | * Check everything in. 33 | 34 | * Double-check it all works, docs build, etc. 35 | 36 | * Build your sdist and wheel: ``python setup.py sdist bdist_wheel`` 37 | 38 | * Upload to PyPI: ``twine upload dist/*`` 39 | 40 | * Use ``git tag`` to tag your version. 41 | 42 | * Don't forget to ``git push --tags``. 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is made available under the terms of *either* of the 2 | licenses found in LICENSE.APACHE2 or LICENSE.MIT. Contributions to are 3 | made under the terms of *both* these licenses. 4 | -------------------------------------------------------------------------------- /LICENSE.APACHE2: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /LICENSE.MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst CHEATSHEET.rst LICENSE* CODE_OF_CONDUCT* CONTRIBUTING* 2 | include .coveragerc .style.yapf 3 | include test-requirements.txt 4 | recursive-include docs * 5 | prune docs/build 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://travis-ci.org/python-trio/triopg.svg?branch=master 2 | :target: https://travis-ci.org/python-trio/triopg 3 | :alt: Automated test status (Linux and MacOS) 4 | 5 | .. image:: https://ci.appveyor.com/api/projects/status/4t8ydnax9p6ehauj/branch/master?svg=true 6 | :target: https://ci.appveyor.com/project/touilleMan/triopg/history 7 | :alt: Automated test status (Windows) 8 | 9 | .. image:: https://codecov.io/gh/python-trio/triopg/branch/master/graph/badge.svg 10 | :target: https://codecov.io/gh/python-trio/triopg 11 | :alt: Test coverage 12 | 13 | triopg 14 | ====== 15 | 16 | WARNING: this project is not actively maintained 17 | ================================================ 18 | 19 | Welcome to `triopg `__! 20 | 21 | PostgreSQL client for `Trio `__ based on 22 | `asyncpg `__. 23 | 24 | License: Your choice of MIT or Apache License 2.0 25 | 26 | Quick example: 27 | 28 | .. code-block:: python 29 | 30 | import trio_asyncio 31 | import triopg 32 | 33 | 34 | async def main(): 35 | async with triopg.connect() as conn: 36 | 37 | await conn.execute( 38 | """ 39 | DROP TABLE IF EXISTS users; 40 | CREATE TABLE IF NOT EXISTS users ( 41 | _id SERIAL PRIMARY KEY, 42 | user_id VARCHAR(32) UNIQUE 43 | )""" 44 | ) 45 | 46 | async with conn.transaction(): 47 | await conn.execute("INSERT INTO users (user_id) VALUES (1)") 48 | await conn.execute("INSERT INTO users (user_id) VALUES (2)") 49 | await conn.execute("INSERT INTO users (user_id) VALUES (3)") 50 | 51 | print(await conn.fetch("SELECT * FROM users")) 52 | 53 | 54 | trio_asyncio.run(main) 55 | 56 | API basics 57 | ---------- 58 | 59 | ``triopg`` is a thin Trio-compatible wrapper around ``asyncpg``. The API is the same, 60 | with one exception - ``triopg`` does not support manual resource management. 61 | In ``asyncpg`` you can manage pools, connections and transactions manually: 62 | 63 | .. code-block:: python 64 | 65 | conn = await asyncpg.connect() 66 | tr = conn.transaction() 67 | # .. 68 | tr.commit() 69 | conn.close() 70 | 71 | While in ``triopg`` you can *only* use ``async with`` blocks: 72 | 73 | .. code-block:: python 74 | 75 | async with triopg.connect() as conn: 76 | async with conn.transaction(): 77 | # ... 78 | 79 | Otherwise you can follow ``asyncpg`` 80 | `tutorial `__ and 81 | `reference `__. 82 | Everything should work the same way. Please 83 | `file an issue `__ if it doesn't. 84 | 85 | Helpers 86 | ------- 87 | 88 | In addition to ``asyncpg``-compatible API, ``triopg`` provides Trio-style 89 | ``.listen()`` helper for the eponymous 90 | `Postgres statement `__: 91 | 92 | .. code-block:: python 93 | 94 | async with conn.listen('some.channel', max_buffer_size=1) as notifications: 95 | async for notification in notifications: 96 | if notification != triopg.NOTIFY_OVERFLOW: 97 | print('Notification received:', notification) 98 | 99 | ``max_buffer_size`` is the amount of notifications you are willing to queue in memory. 100 | 101 | If you **don't** want to think about buffering, set the buffer size to ``math.inf`` 102 | and everything will just work in regular non-pathological situations. 103 | 104 | Otherwise, you can set a finite buffer. In this case you should handle 105 | ``triopg.NOTIFY_OVERFLOW`` marker and react according to your use case. 106 | For example, you could re-scan the tables, like you would do at startup. 107 | Or could you simply ignore the marker if you are only interested in the 108 | newest notifications. 109 | 110 | For detailed discussion on buffering, see Trio manual, 111 | `"Buffering in channels" `__ 112 | section. 113 | 114 | **Note:** we can't politely ask Postgres to slow down: ``LISTEN`` backpressure is 115 | `not supported by asyncpg `__. 116 | There's also an inherent challenge with Postgres. Postgres (like most 117 | broadcast systems) doesn't really have a good way to communicate backpressure 118 | further upstream to the clients that are calling ``NOTIFY``. 119 | -------------------------------------------------------------------------------- /ci/rtd-requirements.txt: -------------------------------------------------------------------------------- 1 | # RTD is currently installing 1.5.3, which has a bug in :lineno-match: 2 | sphinx >= 1.6.1 3 | sphinx_rtd_theme 4 | sphinxcontrib-trio 5 | -------------------------------------------------------------------------------- /ci/travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | YAPF_VERSION=0.22.0 6 | 7 | if [ "$TRAVIS_OS_NAME" = "osx" ]; then 8 | curl -Lo macpython.pkg https://www.python.org/ftp/python/${MACPYTHON}/python-${MACPYTHON}-macosx10.6.pkg 9 | sudo installer -pkg macpython.pkg -target / 10 | ls /Library/Frameworks/Python.framework/Versions/*/bin/ 11 | PYTHON_EXE=/Library/Frameworks/Python.framework/Versions/*/bin/python3 12 | # The pip in older MacPython releases doesn't support a new enough TLS 13 | curl https://bootstrap.pypa.io/get-pip.py | sudo $PYTHON_EXE 14 | sudo $PYTHON_EXE -m pip install virtualenv 15 | $PYTHON_EXE -m virtualenv testenv 16 | source testenv/bin/activate 17 | fi 18 | 19 | if [ "$USE_PYPY_NIGHTLY" = "1" ]; then 20 | curl -fLo pypy.tar.bz2 http://buildbot.pypy.org/nightly/py3.5/pypy-c-jit-latest-linux64.tar.bz2 21 | if [ ! -s pypy.tar.bz2 ]; then 22 | # We know: 23 | # - curl succeeded (200 response code; -f means "exit with error if 24 | # server returns 4xx or 5xx") 25 | # - nonetheless, pypy.tar.bz2 does not exist, or contains no data 26 | # This isn't going to work, and the failure is not informative of 27 | # anything involving this package. 28 | ls -l 29 | echo "PyPy3 nightly build failed to download – something is wrong on their end." 30 | echo "Skipping testing against the nightly build for right now." 31 | exit 0 32 | fi 33 | tar xaf pypy.tar.bz2 34 | # something like "pypy-c-jit-89963-748aa3022295-linux64" 35 | PYPY_DIR=$(echo pypy-c-jit-*) 36 | PYTHON_EXE=$PYPY_DIR/bin/pypy3 37 | ($PYTHON_EXE -m ensurepip \ 38 | && $PYTHON_EXE -m pip install virtualenv \ 39 | && $PYTHON_EXE -m virtualenv testenv) \ 40 | || (echo "pypy nightly is broken; skipping tests"; exit 0) 41 | source testenv/bin/activate 42 | fi 43 | 44 | if [ "$USE_PYPY_RELEASE_VERSION" != "" ]; then 45 | curl -fLo pypy.tar.bz2 https://bitbucket.org/squeaky/portable-pypy/downloads/pypy3.5-${USE_PYPY_RELEASE_VERSION}-linux_x86_64-portable.tar.bz2 46 | tar xaf pypy.tar.bz2 47 | # something like "pypy3.5-5.7.1-beta-linux_x86_64-portable" 48 | PYPY_DIR=$(echo pypy3.5-*) 49 | PYTHON_EXE=$PYPY_DIR/bin/pypy3 50 | $PYTHON_EXE -m ensurepip 51 | $PYTHON_EXE -m pip install virtualenv 52 | $PYTHON_EXE -m virtualenv testenv 53 | source testenv/bin/activate 54 | fi 55 | 56 | pip install -U pip setuptools wheel 57 | 58 | if [ "$CHECK_FORMATTING" = "1" ]; then 59 | pip install yapf==${YAPF_VERSION} 60 | if ! yapf -rpd setup.py triopg; then 61 | cat <NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/source/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-trio/triopg/6575b2212aca2e86c5c91d9f39cf912b6dc40396/docs/source/_static/.gitkeep -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Jan 21 19:11:14 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | # So autodoc can import our package 23 | sys.path.insert(0, os.path.abspath('../..')) 24 | 25 | # Warn about all references to unknown targets 26 | nitpicky = True 27 | # Except for these ones, which we expect to point to unknown targets: 28 | nitpick_ignore = [ 29 | # Format is ("sphinx reference type", "string"), e.g.: 30 | ("py:obj", "bytes-like"), 31 | ] 32 | 33 | # -- General configuration ------------------------------------------------ 34 | 35 | # If your documentation needs a minimal Sphinx version, state it here. 36 | # 37 | # needs_sphinx = '1.0' 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 41 | # ones. 42 | extensions = [ 43 | 'sphinx.ext.autodoc', 44 | 'sphinx.ext.intersphinx', 45 | 'sphinx.ext.coverage', 46 | 'sphinx.ext.napoleon', 47 | 'sphinxcontrib_trio', 48 | ] 49 | 50 | intersphinx_mapping = { 51 | "python": ('https://docs.python.org/3', None), 52 | "trio": ('https://trio.readthedocs.io/en/stable', None), 53 | } 54 | 55 | autodoc_member_order = "bysource" 56 | 57 | # Add any paths that contain templates here, relative to this directory. 58 | templates_path = [] 59 | 60 | # The suffix(es) of source filenames. 61 | # You can specify multiple suffix as a list of string: 62 | # 63 | # source_suffix = ['.rst', '.md'] 64 | source_suffix = '.rst' 65 | 66 | # The master toctree document. 67 | master_doc = 'index' 68 | 69 | # General information about the project. 70 | project = 'triopg' 71 | copyright = 'The triopg authors' 72 | author = 'The triopg authors' 73 | 74 | # The version info for the project you're documenting, acts as replacement for 75 | # |version| and |release|, also used in various other places throughout the 76 | # built documents. 77 | # 78 | # The short X.Y version. 79 | import triopg 80 | version = triopg.__version__ 81 | # The full version, including alpha/beta/rc tags. 82 | release = version 83 | 84 | # The language for content autogenerated by Sphinx. Refer to documentation 85 | # for a list of supported languages. 86 | # 87 | # This is also used if you do content translation via gettext catalogs. 88 | # Usually you set "language" from the command line for these cases. 89 | language = None 90 | 91 | # List of patterns, relative to source directory, that match files and 92 | # directories to ignore when looking for source files. 93 | # This patterns also effect to html_static_path and html_extra_path 94 | exclude_patterns = [] 95 | 96 | # The name of the Pygments (syntax highlighting) style to use. 97 | pygments_style = 'sphinx' 98 | 99 | # The default language for :: blocks 100 | highlight_language = 'python3' 101 | 102 | # If true, `todo` and `todoList` produce output, else they produce nothing. 103 | todo_include_todos = False 104 | 105 | 106 | # -- Options for HTML output ---------------------------------------------- 107 | 108 | # The theme to use for HTML and HTML Help pages. See the documentation for 109 | # a list of builtin themes. 110 | # 111 | #html_theme = 'alabaster' 112 | 113 | # We have to set this ourselves, not only because it's useful for local 114 | # testing, but also because if we don't then RTD will throw away our 115 | # html_theme_options. 116 | import sphinx_rtd_theme 117 | html_theme = 'sphinx_rtd_theme' 118 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 119 | 120 | # Theme options are theme-specific and customize the look and feel of a theme 121 | # further. For a list of options available for each theme, see the 122 | # documentation. 123 | # 124 | html_theme_options = { 125 | # default is 2 126 | # show deeper nesting in the RTD theme's sidebar TOC 127 | # https://stackoverflow.com/questions/27669376/ 128 | # I'm not 100% sure this actually does anything with our current 129 | # versions/settings... 130 | "navigation_depth": 4, 131 | "logo_only": True, 132 | } 133 | 134 | # Add any paths that contain custom static files (such as style sheets) here, 135 | # relative to this directory. They are copied after the builtin static files, 136 | # so a file named "default.css" will overwrite the builtin "default.css". 137 | html_static_path = ['_static'] 138 | 139 | 140 | # -- Options for HTMLHelp output ------------------------------------------ 141 | 142 | # Output file base name for HTML help builder. 143 | htmlhelp_basename = 'triopgdoc' 144 | 145 | 146 | # -- Options for LaTeX output --------------------------------------------- 147 | 148 | latex_elements = { 149 | # The paper size ('letterpaper' or 'a4paper'). 150 | # 151 | # 'papersize': 'letterpaper', 152 | 153 | # The font size ('10pt', '11pt' or '12pt'). 154 | # 155 | # 'pointsize': '10pt', 156 | 157 | # Additional stuff for the LaTeX preamble. 158 | # 159 | # 'preamble': '', 160 | 161 | # Latex figure (float) alignment 162 | # 163 | # 'figure_align': 'htbp', 164 | } 165 | 166 | # Grouping the document tree into LaTeX files. List of tuples 167 | # (source start file, target name, title, 168 | # author, documentclass [howto, manual, or own class]). 169 | latex_documents = [ 170 | (master_doc, 'triopg.tex', 'Trio Documentation', 171 | author, 'manual'), 172 | ] 173 | 174 | 175 | # -- Options for manual page output --------------------------------------- 176 | 177 | # One entry per manual page. List of tuples 178 | # (source start file, name, description, authors, manual section). 179 | man_pages = [ 180 | (master_doc, 'triopg', 'triopg Documentation', 181 | [author], 1) 182 | ] 183 | 184 | 185 | # -- Options for Texinfo output ------------------------------------------- 186 | 187 | # Grouping the document tree into Texinfo files. List of tuples 188 | # (source start file, target name, title, author, 189 | # dir menu entry, description, category) 190 | texinfo_documents = [ 191 | (master_doc, 'triopg', 'triopg Documentation', 192 | author, 'triopg', 'PostgreSQL client for Trio based on asyncpg', 193 | 'Miscellaneous'), 194 | ] 195 | -------------------------------------------------------------------------------- /docs/source/history.rst: -------------------------------------------------------------------------------- 1 | Release history 2 | =============== 3 | 4 | .. currentmodule:: triopg 5 | 6 | .. towncrier release notes start 7 | 8 | Triopg 0.6.0 (2021-03-03) 9 | ------------------------- 10 | 11 | Features 12 | ~~~~~~~~ 13 | 14 | - Add ``.listen`` helper for Trio-style handling of LISTEN notifications. (`#13 `__) 15 | 16 | 17 | Bugfixes 18 | ~~~~~~~~ 19 | 20 | - Correctly proxy ordinary (non-async) ``asyncpg.Connection`` methods. 21 | For example, ``.is_closed()``, ``.get_server_pid()`` etc. (`#11 `__) 22 | - Correctly proxy ordinary (non-async) ``PreparedStatement`` methods. 23 | For example, ``.get_statusmsg()``, ``.get_query()`` etc. (`#14 `__) 24 | 25 | 26 | Triopg 0.5.0 (2020-08-03) 27 | ------------------------- 28 | 29 | Features 30 | ~~~~~~~~ 31 | 32 | - Add support for prepared statement and cursor (`#9 `__) 33 | 34 | 35 | Misc 36 | ~~~~ 37 | 38 | - `#5 `__ 39 | 40 | 41 | Triopg 0.4.0 (2020-03-09) 42 | ------------------------- 43 | 44 | - Add ``Pool.fetchrow``, ``Pool.fetchval``, ``Pool.execute``, 45 | and ``Pool.executemany`` shortcuts. 46 | 47 | 48 | Triopg 0.3.0 (2018-10-14) 49 | ------------------------- 50 | 51 | - Make ``connect`` and ``create_pool`` async context manager only to avoid 52 | crash due to asyncpg using __del__ to call on the asyncio loop 53 | (see https://github.com/python-trio/trio-asyncio/issues/44) 54 | 55 | 56 | Triopg 0.2.1 (2018-10-03) 57 | ------------------------- 58 | 59 | - Upgrade for compatibility with trio-asyncio>=0.9.0 60 | 61 | 62 | Triopg 0.2.0 (2018-08-16) 63 | ------------------------- 64 | 65 | Features 66 | ~~~~~~~~ 67 | 68 | - Republish asyncpg's exceptions. 69 | - Introduce ``triopg.create_pool_cm`` to handle pool creation as an 70 | async context manager. 71 | 72 | Bugfixes 73 | ~~~~~~~~ 74 | 75 | - Fix ``triopg.create_pool`` being used an async function. 76 | 77 | Deprecations and Removals 78 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 79 | 80 | - ``triopg.create_pool`` can no longer be used as an async context manager, 81 | ``triopg.create_pool_cm`` should be used instead. 82 | 83 | 84 | Triopg 0.1.0 (2018-07-28) 85 | ------------------------- 86 | 87 | No significant changes. 88 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. documentation master file, created by 2 | sphinx-quickstart on Sat Jan 21 19:11:14 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 7 | =================================================== 8 | triopg: PostgreSQL client for Trio based on asyncpg 9 | =================================================== 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | history.rst 15 | 16 | ==================== 17 | Indices and tables 18 | ==================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | * :ref:`glossary` 24 | -------------------------------------------------------------------------------- /newsfragments/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-trio/triopg/6575b2212aca2e86c5c91d9f39cf912b6dc40396/newsfragments/.gitkeep -------------------------------------------------------------------------------- /newsfragments/README.rst: -------------------------------------------------------------------------------- 1 | Adding newsfragments 2 | ==================== 3 | 4 | This directory collects "newsfragments": short files that each contain 5 | a snippet of ReST-formatted text that will be added to the next 6 | release notes. This should be a description of aspects of the change 7 | (if any) that are relevant to users. (This contrasts with your commit 8 | message and PR description, which are a description of the change as 9 | relevant to people working on the code itself.) 10 | 11 | Each file should be named like ``..rst``, where 12 | ```` is an issue numbers, and ```` is one of: 13 | 14 | * ``feature`` 15 | * ``bugfix`` 16 | * ``doc`` 17 | * ``removal`` 18 | * ``misc`` 19 | 20 | So for example: ``123.feature.rst``, ``456.bugfix.rst`` 21 | 22 | If your PR fixes an issue, use that number here. If there is no issue, 23 | then after you submit the PR and get the PR number you can add a 24 | newsfragment using that instead. 25 | 26 | Note that the ``towncrier`` tool will automatically 27 | reflow your text, so don't try to do any fancy formatting. You can 28 | install ``towncrier`` and then run ``towncrier --draft`` if you want 29 | to get a preview of how your change will look in the final release 30 | notes. 31 | 32 | 33 | Making releases 34 | =============== 35 | 36 | ``pip install towncrier``, then run ``towncrier``. (You can use 37 | ``towncrier --draft`` to get a preview of what this will do.) 38 | 39 | You can configure ``towncrier`` (for example: customizing the 40 | different types of changes) by modifying ``pyproject.toml``. 41 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.towncrier] 2 | package = "triopg" 3 | filename = "docs/source/history.rst" 4 | directory = "newsfragments" 5 | underlines = ["-", "~", "^"] 6 | issue_format = "`#{issue} `__" 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | exec(open("triopg/_version.py", encoding="utf-8").read()) 4 | 5 | LONG_DESC = open("README.rst", encoding="utf-8").read() 6 | 7 | setup( 8 | name="triopg", 9 | version=__version__, 10 | description="PostgreSQL client for Trio based on asyncpg", 11 | url="https://github.com/python-trio/triopg", 12 | long_description=LONG_DESC, 13 | author="Emmanuel Leblond", 14 | author_email="emmanuel.leblond@gmail.com", 15 | license="MIT -or- Apache License 2.0", 16 | packages=find_packages(), 17 | install_requires=[ 18 | "trio>=0.7.0", 19 | "trio-asyncio>=0.9.0", 20 | "asyncpg>=0.15.0", 21 | ], 22 | keywords=["async", "trio", "sql", "postgresql", "asyncpg"], 23 | python_requires=">=3.6", 24 | classifiers=[ 25 | "Development Status :: 7 - Inactive", 26 | "License :: OSI Approved :: MIT License", 27 | "License :: OSI Approved :: Apache Software License", 28 | "Framework :: Trio", 29 | "Operating System :: POSIX :: Linux", 30 | "Operating System :: MacOS :: MacOS X", 31 | "Operating System :: Microsoft :: Windows", 32 | "Programming Language :: Python :: 3 :: Only", 33 | "Programming Language :: Python :: Implementation :: CPython", 34 | "Programming Language :: Python :: Implementation :: PyPy", 35 | "Programming Language :: SQL", 36 | ], 37 | ) 38 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov 3 | pytest-trio 4 | -------------------------------------------------------------------------------- /triopg/__init__.py: -------------------------------------------------------------------------------- 1 | """Top-level package for triopg.""" 2 | 3 | from ._version import __version__ 4 | from ._triopg import connect, create_pool, NOTIFY_OVERFLOW 5 | from .exceptions import * # NOQA 6 | 7 | __all__ = ( 8 | '__version__', 9 | 'connect', 10 | 'create_pool', 11 | 'NOTIFY_OVERFLOW', 12 | ) + exceptions.__all__ # NOQA 13 | -------------------------------------------------------------------------------- /triopg/_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-trio/triopg/6575b2212aca2e86c5c91d9f39cf912b6dc40396/triopg/_tests/__init__.py -------------------------------------------------------------------------------- /triopg/_tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import trio_asyncio 3 | import asyncpg 4 | from asyncpg.cluster import Cluster 5 | import tempfile 6 | 7 | import triopg 8 | 9 | 10 | @pytest.fixture() 11 | async def asyncio_loop(): 12 | async with trio_asyncio.open_loop() as loop: 13 | yield loop 14 | 15 | 16 | @pytest.fixture(scope='session') 17 | def cluster(): 18 | cluster_dir = tempfile.mkdtemp() 19 | cluster = Cluster(cluster_dir) 20 | 21 | cluster.init() 22 | try: 23 | cluster.start(port='dynamic') 24 | yield cluster 25 | cluster.stop() 26 | finally: 27 | cluster.destroy() 28 | 29 | 30 | @pytest.fixture 31 | def postgresql_connection_specs(cluster): 32 | return {'database': 'postgres', **cluster.get_connection_spec()} 33 | 34 | 35 | @pytest.fixture() 36 | async def asyncpg_conn(asyncio_loop, postgresql_connection_specs): 37 | conn = await trio_asyncio.aio_as_trio(asyncpg.connect 38 | )(**postgresql_connection_specs) 39 | try: 40 | yield conn 41 | finally: 42 | await trio_asyncio.aio_as_trio(conn.close)() 43 | 44 | 45 | @pytest.fixture 46 | def asyncpg_execute(asyncpg_conn): 47 | @trio_asyncio.aio_as_trio 48 | async def _asyncpg_execute(sql): 49 | return await asyncpg_conn.execute(sql) 50 | 51 | return _asyncpg_execute 52 | 53 | 54 | @pytest.fixture(params=["from_connect", "from_pool"]) 55 | async def triopg_conn(request, asyncio_loop, postgresql_connection_specs): 56 | if request.param == "from_connect": 57 | async with triopg.connect(**postgresql_connection_specs) as conn: 58 | yield conn 59 | 60 | else: 61 | async with triopg.create_pool(**postgresql_connection_specs) as pool: 62 | async with pool.acquire() as conn: 63 | yield conn 64 | 65 | 66 | @pytest.fixture 67 | async def triopg_pool(asyncio_loop, postgresql_connection_specs): 68 | async with triopg.create_pool(**postgresql_connection_specs) as pool: 69 | yield pool 70 | -------------------------------------------------------------------------------- /triopg/_tests/test_triopg.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import trio 3 | import trio_asyncio 4 | import asyncpg 5 | 6 | import triopg 7 | 8 | 9 | def unwrap(record): 10 | assert isinstance(record, asyncpg.Record) 11 | return tuple(record.values()) 12 | 13 | 14 | @pytest.mark.trio 15 | async def test_connection_closed(asyncio_loop, postgresql_connection_specs): 16 | termination_listener_called = False 17 | 18 | def _termination_listener(connection): 19 | nonlocal termination_listener_called 20 | termination_listener_called = True 21 | 22 | async with triopg.connect(**postgresql_connection_specs) as conn: 23 | assert not conn.is_closed() 24 | pid = conn.get_server_pid() 25 | assert isinstance(pid, int) 26 | conn.add_termination_listener(_termination_listener) 27 | 28 | assert conn.is_closed() 29 | assert termination_listener_called 30 | with pytest.raises(triopg.InterfaceError): 31 | await conn.execute("VALUES (1)") 32 | 33 | 34 | @pytest.mark.trio 35 | async def test_cursor(triopg_conn): 36 | async with triopg_conn.transaction(): 37 | cursor_factory = triopg_conn.cursor( 38 | "VALUES ($1, 1), ($2, 2), ($3, 3), ($4, 4), ($5, 5)", "1", "2", 39 | "3", "4", "5" 40 | ) 41 | cursor = await cursor_factory 42 | 43 | row = await cursor.fetchrow() 44 | assert unwrap(row) == ("1", 1) 45 | advanced = await cursor.forward(1) 46 | assert advanced == 1 47 | fetched = await cursor.fetch(2) 48 | assert [unwrap(x) for x in fetched] == [("3", 3), ("4", 4)] 49 | 50 | items = [] 51 | async for row in triopg_conn.cursor("VALUES ($1, 1), ($2, 2), ($3, 3)", 52 | "1", "2", "3"): 53 | items.append(unwrap(row)) 54 | assert items == [("1", 1), ("2", 2), ("3", 3)] 55 | 56 | 57 | @pytest.mark.trio 58 | async def test_transaction(triopg_conn, asyncpg_execute): 59 | # Execute without transaction 60 | assert not triopg_conn.is_in_transaction() 61 | await triopg_conn.execute( 62 | """ 63 | DROP TABLE IF EXISTS users; 64 | CREATE TABLE IF NOT EXISTS users ( 65 | _id SERIAL PRIMARY KEY, 66 | user_id VARCHAR(32) UNIQUE 67 | )""" 68 | ) 69 | assert await asyncpg_execute("SELECT * FROM users") == "SELECT 0" 70 | 71 | # Execute in transaction without exception 72 | async with triopg_conn.transaction(): 73 | assert triopg_conn.is_in_transaction() 74 | await triopg_conn.execute("INSERT INTO users (user_id) VALUES (1)") 75 | assert await asyncpg_execute("SELECT * FROM users") == "SELECT 1" 76 | 77 | # Execute in transaction raising exception, request should not be executed 78 | with pytest.raises(Exception): 79 | async with triopg_conn.transaction(): 80 | await triopg_conn.execute("INSERT INTO users (user_id) VALUES (2)") 81 | raise Exception() 82 | 83 | assert not triopg_conn.is_in_transaction() 84 | assert await asyncpg_execute("SELECT * FROM users") == "SELECT 1" 85 | 86 | 87 | @pytest.mark.trio 88 | async def test_prepared_statement(triopg_conn): 89 | # Execute with prepared statement 90 | stmt = await triopg_conn.prepare("VALUES ($1, 2)") 91 | records = await stmt.fetchval("1") 92 | assert records == "1" 93 | 94 | # Test cursor in prepared statement 95 | async with triopg_conn.transaction(): 96 | stmt = await triopg_conn.prepare( 97 | "VALUES ($1, 1), ($2, 2), ($3, 3), ($4, 4), ($5, 5)" 98 | ) 99 | 100 | cursor = await stmt.cursor("1", "2", "3", "4", "5") 101 | row = await cursor.fetchrow() 102 | assert unwrap(row) == ("1", 1) 103 | advanced = await cursor.forward(1) 104 | assert advanced == 1 105 | fetched = await cursor.fetch(2) 106 | assert [unwrap(x) for x in fetched] == [("3", 3), ("4", 4)] 107 | 108 | items = [] 109 | async for row in stmt.cursor("1", "2", "3", "4", "5"): 110 | items.append(unwrap(row)) 111 | assert items == [("1", 1), ("2", 2), ("3", 3), ("4", 4), ("5", 5)] 112 | 113 | 114 | @pytest.mark.trio 115 | async def test_prepared_statement_statusmsg(triopg_conn): 116 | stmt = await triopg_conn.prepare("VALUES (1), (1), (1)") 117 | await stmt.fetch() 118 | assert stmt.get_statusmsg() == 'SELECT 3' 119 | assert stmt.get_query() == 'VALUES (1), (1), (1)' 120 | 121 | 122 | @pytest.mark.trio 123 | async def test_execute_many(triopg_conn, asyncpg_execute): 124 | await triopg_conn.execute( 125 | """ 126 | DROP TABLE IF EXISTS users; 127 | CREATE TABLE IF NOT EXISTS users ( 128 | _id SERIAL PRIMARY KEY, 129 | user_id VARCHAR(32) UNIQUE 130 | )""" 131 | ) 132 | 133 | user_ids = [(str(i),) for i in range(10)] 134 | await triopg_conn.executemany( 135 | "INSERT INTO users (user_id) VALUES ($1)", user_ids 136 | ) 137 | 138 | assert await asyncpg_execute("SELECT * FROM users") == "SELECT 10" 139 | 140 | 141 | @pytest.mark.trio 142 | async def test_use_pool_without_acquire_connection(triopg_pool): 143 | rep = await triopg_pool.execute( 144 | """ 145 | DROP TABLE IF EXISTS users; 146 | CREATE TABLE IF NOT EXISTS users ( 147 | _id SERIAL PRIMARY KEY, 148 | user_id VARCHAR(32) UNIQUE 149 | )""" 150 | ) 151 | assert rep == "CREATE TABLE" 152 | 153 | user_ids = [(str(i),) for i in range(10)] 154 | await triopg_pool.executemany( 155 | "INSERT INTO users (user_id) VALUES ($1)", user_ids 156 | ) 157 | 158 | rep = await triopg_pool.fetch("SELECT * FROM users") 159 | rep = [dict(x.items()) for x in rep] 160 | assert rep == [{"_id": i + 1, "user_id": str(i)} for i in range(10)] 161 | 162 | rep = await triopg_pool.fetchrow("SELECT * FROM users WHERE _id = $1", 1) 163 | assert dict(rep.items()) == {"_id": 1, "user_id": "0"} 164 | 165 | val = await triopg_pool.fetchval( 166 | "SELECT user_id FROM users WHERE _id = $1", 2 167 | ) 168 | assert val == "1" 169 | 170 | 171 | @pytest.mark.trio 172 | async def test_listener(triopg_conn, asyncpg_execute): 173 | listener_sender, listener_receiver = trio.open_memory_channel(100) 174 | 175 | def _listener(connection, pid, channel, payload): 176 | listener_sender.send_nowait((channel, payload)) 177 | 178 | assert await asyncpg_execute("NOTIFY foo, '1'") # Should be ignored 179 | await triopg_conn.add_listener("foo", _listener) 180 | assert await asyncpg_execute("NOTIFY dummy") # Should be ignored 181 | assert await asyncpg_execute("NOTIFY foo, '2'") 182 | await triopg_conn.remove_listener("foo", _listener) 183 | assert await asyncpg_execute("NOTIFY foo, '3'") # Should be ignored 184 | 185 | with trio.fail_after(5): 186 | channel, payload = await listener_receiver.receive() 187 | assert channel == "foo" 188 | assert payload == "2" 189 | with pytest.raises(trio.WouldBlock): 190 | await listener_receiver.receive_nowait() 191 | 192 | 193 | async def assert_listeners(conn, status): 194 | """Assert expected listeners `status`, on Postgres and on asyncpg""" 195 | 196 | pg = await conn.fetchval( 197 | "select * from pg_listening_channels()" 198 | ) is not None 199 | assert pg == status 200 | assert bool(conn._asyncpg_conn._listeners) == status 201 | 202 | 203 | @pytest.mark.trio 204 | async def test_listener_cancel(triopg_conn, asyncpg_execute): 205 | """Test .remove_listener consistent cancellation""" 206 | 207 | def _listener(*args): 208 | pass # pragma: no cover 209 | 210 | await assert_listeners(triopg_conn, False) 211 | await triopg_conn.add_listener("foo", _listener) 212 | await assert_listeners(triopg_conn, True) 213 | with trio.CancelScope() as cancel_scope: 214 | cancel_scope.cancel() 215 | await triopg_conn.remove_listener("foo", _listener) 216 | 217 | # cancellation completely prevented .remove_listener() call 218 | await assert_listeners(triopg_conn, True) 219 | 220 | # clean up to prevent "active connection left" warning 221 | await triopg_conn.remove_listener("foo", _listener) 222 | await assert_listeners(triopg_conn, False) 223 | 224 | 225 | @pytest.mark.trio 226 | async def test_listen(triopg_conn, asyncpg_execute): 227 | await asyncpg_execute("NOTIFY foo, '1'") # should be ignored 228 | 229 | async with triopg_conn.listen("foo", max_buffer_size=1) as changes: 230 | await asyncpg_execute("NOTIFY foo, '2'") 231 | assert await changes.receive() == "2" 232 | await asyncpg_execute("NOTIFY foo, '3'") 233 | assert await changes.receive() == "3" 234 | 235 | await asyncpg_execute("NOTIFY foo, '4'") # should be ignored 236 | 237 | with pytest.raises(trio.ClosedResourceError): 238 | await changes.receive() 239 | 240 | 241 | @pytest.mark.trio 242 | async def test_listen_overflow(triopg_conn, asyncpg_execute): 243 | async with triopg_conn.listen("foo", max_buffer_size=1) as changes: 244 | await asyncpg_execute("NOTIFY foo, '1'") 245 | assert await changes.receive() == "1" 246 | 247 | async with triopg_conn.listen("foo", max_buffer_size=2) as changes: 248 | await asyncpg_execute("NOTIFY foo, '1'") 249 | await asyncpg_execute("NOTIFY foo, '2'") 250 | assert await changes.receive() == "1" 251 | assert await changes.receive() == "2" 252 | 253 | async with triopg_conn.listen("foo", max_buffer_size=1) as changes: 254 | await asyncpg_execute("NOTIFY foo, '1'") 255 | await asyncpg_execute("NOTIFY foo, '2'") 256 | await asyncpg_execute("NOTIFY foo, '3'") 257 | await asyncpg_execute("NOTIFY foo, '4'") 258 | assert await changes.receive() == "1" 259 | assert await changes.receive() == triopg.NOTIFY_OVERFLOW 260 | # '2', '3', '4' were dropped on the floor 261 | await asyncpg_execute("NOTIFY foo, '5'") 262 | assert await changes.receive() == "5" 263 | 264 | async with triopg_conn.listen("foo", max_buffer_size=2) as changes: 265 | await asyncpg_execute("NOTIFY foo, '1'") 266 | await asyncpg_execute("NOTIFY foo, '2'") 267 | await asyncpg_execute("NOTIFY foo, '3'") 268 | await asyncpg_execute("NOTIFY foo, '4'") 269 | assert await changes.receive() == "1" 270 | assert await changes.receive() == "2" 271 | assert await changes.receive() == triopg.NOTIFY_OVERFLOW 272 | # '3', '4' were dropped on the floor 273 | await asyncpg_execute("NOTIFY foo, '6'") 274 | assert await changes.receive() == "6" 275 | 276 | 277 | @pytest.mark.trio 278 | async def test_listen_cancel(triopg_conn): 279 | with trio.CancelScope() as cancel_scope: 280 | await assert_listeners(triopg_conn, False) 281 | async with triopg_conn.listen("foo", max_buffer_size=1): 282 | await assert_listeners(triopg_conn, True) 283 | cancel_scope.cancel() 284 | await assert_listeners(triopg_conn, False) 285 | -------------------------------------------------------------------------------- /triopg/_triopg.py: -------------------------------------------------------------------------------- 1 | from functools import wraps, partial 2 | from inspect import iscoroutinefunction 3 | import trio 4 | import asyncpg 5 | import trio_asyncio 6 | from async_generator import asynccontextmanager 7 | 8 | 9 | def _shielded(f): 10 | @wraps(f) 11 | async def wrapper(*args, **kwargs): 12 | with trio.CancelScope(shield=True): 13 | return await f(*args, **kwargs) 14 | 15 | return wrapper 16 | 17 | 18 | def connect(*args, **kwargs): 19 | return TrioConnectionProxy(*args, **kwargs) 20 | 21 | 22 | def create_pool(*args, **kwargs): 23 | return TrioPoolProxy(*args, **kwargs) 24 | 25 | 26 | class TrioTransactionProxy: 27 | def __init__(self, asyncpg_transaction): 28 | self._asyncpg_transaction = asyncpg_transaction 29 | 30 | @trio_asyncio.aio_as_trio 31 | async def __aenter__(self, *args): 32 | return await self._asyncpg_transaction.__aenter__(*args) 33 | 34 | @_shielded 35 | @trio_asyncio.aio_as_trio 36 | async def __aexit__(self, *args): 37 | return await self._asyncpg_transaction.__aexit__(*args) 38 | 39 | 40 | class TrioCursorProxy: 41 | def __init__(self, asyncpg_cursor): 42 | self._asyncpg_cursor = asyncpg_cursor 43 | 44 | @trio_asyncio.aio_as_trio 45 | async def fetch(self, *args, **kwargs): 46 | return await self._asyncpg_cursor.fetch(*args, **kwargs) 47 | 48 | @trio_asyncio.aio_as_trio 49 | async def fetchrow(self, *args, **kwargs): 50 | return await self._asyncpg_cursor.fetchrow(*args, **kwargs) 51 | 52 | @trio_asyncio.aio_as_trio 53 | async def forward(self, *args, **kwargs): 54 | return await self._asyncpg_cursor.forward(*args, **kwargs) 55 | 56 | 57 | class TrioCursorFactoryProxy: 58 | def __init__(self, asyncpg_transaction_factory): 59 | self._asyncpg_transaction_factory = asyncpg_transaction_factory 60 | self._asyncpg_cursor_aiter = None 61 | 62 | def __await__(self): 63 | return self._wrapped_asyncpg_await().__await__() 64 | 65 | @trio_asyncio.aio_as_trio 66 | async def _wrapped_asyncpg_await(self): 67 | asyncpg_cursor = await self._asyncpg_transaction_factory 68 | return TrioCursorProxy(asyncpg_cursor) 69 | 70 | def __aiter__(self): 71 | self._asyncpg_cursor_aiter = self._asyncpg_transaction_factory.__aiter__( 72 | ) 73 | return self 74 | 75 | @trio_asyncio.aio_as_trio 76 | async def __anext__(self): 77 | return await self._asyncpg_cursor_aiter.__anext__() 78 | 79 | 80 | class TrioStatementProxy: 81 | def __init__(self, asyncpg_statement): 82 | self._asyncpg_statement = asyncpg_statement 83 | 84 | def cursor(self, *args, **kwargs): 85 | asyncpg_cursor_factory = self._asyncpg_statement.cursor( 86 | *args, **kwargs 87 | ) 88 | return TrioCursorFactoryProxy(asyncpg_cursor_factory) 89 | 90 | def __getattr__(self, attr): 91 | target = getattr(self._asyncpg_statement, attr) 92 | 93 | # iscoroutinefunction(target) is not enough, because PreparedStatement 94 | # methods are wrapped with @connresource.guarded 95 | if iscoroutinefunction(target.__wrapped__ 96 | if hasattr(target, '__wrapped__') else target): 97 | 98 | @wraps(target) 99 | @trio_asyncio.aio_as_trio 100 | async def wrapper(*args, **kwargs): 101 | return await target(*args, **kwargs) 102 | 103 | # Only generate the function wrapper once per instance 104 | setattr(self, attr, wrapper) 105 | 106 | return wrapper 107 | 108 | return target 109 | 110 | 111 | NOTIFY_OVERFLOW = object() 112 | 113 | 114 | class TrioConnectionProxy: 115 | def __init__(self, *args, **kwargs): 116 | self._asyncpg_create_connection = partial( 117 | asyncpg.connect, *args, **kwargs 118 | ) 119 | self._asyncpg_conn = None 120 | 121 | def transaction(self, *args, **kwargs): 122 | asyncpg_transaction = self._asyncpg_conn.transaction(*args, **kwargs) 123 | return TrioTransactionProxy(asyncpg_transaction) 124 | 125 | async def prepare(self, *args, **kwargs): 126 | asyncpg_statement = await trio_asyncio.aio_as_trio( 127 | self._asyncpg_conn.prepare(*args, **kwargs) 128 | ) 129 | return TrioStatementProxy(asyncpg_statement) 130 | 131 | @asynccontextmanager 132 | async def listen(self, channel, max_buffer_size): 133 | """LISTEN on `channel` notifications and return memory channel to iterate over 134 | 135 | max_buffer_size - memory channel max buffer size 136 | 137 | For example: 138 | 139 | async with conn.listen('some.changes', max_buffer_size=1) as notifications: 140 | async for notification in notifications: 141 | if notification != NOTIFY_OVERFLOW: 142 | print('Postgres notification received:', notification) 143 | """ 144 | 145 | assert max_buffer_size >= 1 146 | send_channel, receive_channel = trio.open_memory_channel( 147 | max_buffer_size + 1 148 | ) 149 | 150 | def _listen_callback(c, pid, chan, payload): 151 | stats = send_channel.statistics() 152 | if stats.current_buffer_used == stats.max_buffer_size - 1: 153 | send_channel.send_nowait(NOTIFY_OVERFLOW) 154 | try: 155 | send_channel.send_nowait(payload) 156 | except trio.WouldBlock: 157 | pass # drop payload on the floor 158 | 159 | async with receive_channel, send_channel: 160 | await self.add_listener(channel, _listen_callback) 161 | try: 162 | yield receive_channel 163 | finally: 164 | with trio.CancelScope(shield=True): 165 | await self.remove_listener(channel, _listen_callback) 166 | 167 | def __getattr__(self, attr): 168 | target = getattr(self._asyncpg_conn, attr) 169 | 170 | if iscoroutinefunction(target): 171 | 172 | @wraps(target) 173 | @trio_asyncio.aio_as_trio 174 | async def wrapper(*args, **kwargs): 175 | return await target(*args, **kwargs) 176 | 177 | # Only generate the function wrapper once per connection instance 178 | setattr(self, attr, wrapper) 179 | 180 | return wrapper 181 | 182 | return target 183 | 184 | def cursor(self, *args, **kwargs): 185 | asyncpg_cursor_factory = self._asyncpg_conn.cursor(*args, **kwargs) 186 | return TrioCursorFactoryProxy(asyncpg_cursor_factory) 187 | 188 | @_shielded 189 | @trio_asyncio.aio_as_trio 190 | async def close(self): 191 | return await self._asyncpg_conn.close() 192 | 193 | async def __aenter__(self): 194 | if not self._asyncpg_conn: 195 | self._asyncpg_conn = await trio_asyncio.aio_as_trio( 196 | self._asyncpg_create_connection 197 | )() 198 | return self 199 | 200 | async def __aexit__(self, *exc): 201 | return await self.close() 202 | 203 | 204 | class TrioPoolAcquireContextProxy: 205 | def __init__(self, asyncpg_acquire_context): 206 | self._asyncpg_acquire_context = asyncpg_acquire_context 207 | 208 | @trio_asyncio.aio_as_trio 209 | async def __aenter__(self, *args): 210 | proxy = await self._asyncpg_acquire_context.__aenter__(*args) 211 | conn_proxy = TrioConnectionProxy() 212 | conn_proxy._asyncpg_conn = proxy._con 213 | return conn_proxy 214 | 215 | @_shielded 216 | @trio_asyncio.aio_as_trio 217 | async def __aexit__(self, *args): 218 | return await self._asyncpg_acquire_context.__aexit__(*args) 219 | 220 | 221 | class TrioPoolProxy: 222 | def __init__(self, *args, **kwargs): 223 | self._asyncpg_create_pool = partial( 224 | asyncpg.create_pool, *args, **kwargs 225 | ) 226 | self._asyncpg_pool = None 227 | 228 | def acquire(self): 229 | return TrioPoolAcquireContextProxy(self._asyncpg_pool.acquire()) 230 | 231 | async def execute(self, statement: str, *args, timeout: float = None): 232 | async with self.acquire() as conn: 233 | return await conn.execute(statement, *args, timeout=timeout) 234 | 235 | async def executemany( 236 | self, statement: str, args, *, timeout: float = None 237 | ): 238 | async with self.acquire() as conn: 239 | return await conn.executemany(statement, args, timeout=timeout) 240 | 241 | async def fetch(self, query, *args, timeout: float = None): 242 | async with self.acquire() as conn: 243 | return await conn.fetch(query, *args, timeout=timeout) 244 | 245 | async def fetchval(self, query, *args, timeout: float = None): 246 | async with self.acquire() as conn: 247 | return await conn.fetchval(query, *args, timeout=timeout) 248 | 249 | async def fetchrow(self, query, *args, timeout: float = None): 250 | async with self.acquire() as conn: 251 | return await conn.fetchrow(query, *args, timeout=timeout) 252 | 253 | @_shielded 254 | @trio_asyncio.aio_as_trio 255 | async def close(self): 256 | return await self._asyncpg_pool.close() 257 | 258 | def terminate(self): 259 | return self._asyncpg_pool.terminate() 260 | 261 | async def __aenter__(self): 262 | if not self._asyncpg_pool: 263 | self._asyncpg_pool = await trio_asyncio.aio_as_trio( 264 | self._asyncpg_create_pool 265 | )() 266 | return self 267 | 268 | async def __aexit__(self, *exc): 269 | return await self.close() 270 | -------------------------------------------------------------------------------- /triopg/_version.py: -------------------------------------------------------------------------------- 1 | # This file is imported from __init__.py and exec'd from setup.py 2 | 3 | __version__ = "0.6.0" 4 | -------------------------------------------------------------------------------- /triopg/exceptions.py: -------------------------------------------------------------------------------- 1 | from asyncpg.exceptions import * # NOQA 2 | from asyncpg.exceptions import __all__ # NOQA 3 | --------------------------------------------------------------------------------