├── .flake8 ├── .github ├── dependabot.yml ├── issue_template.md ├── pull_request_template.md └── workflows │ └── ci.yml ├── .gitignore ├── .mailmap ├── .readthedocs.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.rst ├── aiosqlite ├── __init__.py ├── __version__.py ├── context.py ├── core.py ├── cursor.py ├── py.typed └── tests │ ├── __init__.py │ ├── __main__.py │ ├── helpers.py │ ├── perf.py │ └── smoke.py ├── docs ├── _static │ └── custom.css ├── _templates │ ├── badges.html │ └── omnilib.html ├── api.rst ├── changelog.rst ├── conf.py ├── contributing.rst └── index.rst ├── makefile └── pyproject.toml /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | select = C,E,F,W,B,B9 3 | ignore = 4 | # mccabe complexity 5 | C901 6 | 7 | # covered by black/usort 8 | E1 9 | E2 10 | E3 11 | E4 12 | E501 13 | max-line-length = 88 14 | per-file-ignores = 15 | __init__.py: F401 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | - package-ecosystem: "pip" 8 | directory: "/" 9 | schedule: 10 | interval: "monthly" 11 | day: "saturday" 12 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 4 | 5 | ### Details 6 | 7 | * OS: 8 | * Python version: 9 | * aiosqlite version: 10 | * Can you repro on 'main' branch? 11 | * Can you repro in a clean virtualenv? 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 4 | 5 | Fixes: # 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: 7 | - v* 8 | pull_request: 9 | 10 | permissions: 11 | contents: read 12 | 13 | env: 14 | UV_SYSTEM_PYTHON: 1 15 | 16 | jobs: 17 | test: 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 23 | os: [macOS-latest, ubuntu-latest, windows-latest] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | - name: Set Up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v5 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | allow-prereleases: true 33 | - uses: astral-sh/setup-uv@v6 34 | with: 35 | enable-cache: true 36 | cache-dependency-glob: pyproject.toml 37 | cache-suffix: ${{ matrix.python-version }} 38 | - name: Install 39 | run: make EXTRAS=dev install 40 | - name: Test 41 | run: make test 42 | - name: Lint 43 | run: make lint 44 | 45 | build: 46 | needs: test 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v4 50 | - uses: actions/setup-python@v5 51 | with: 52 | python-version: '3.12' 53 | - uses: astral-sh/setup-uv@v6 54 | with: 55 | enable-cache: true 56 | cache-dependency-glob: pyproject.toml 57 | - name: Install 58 | run: make EXTRAS=dev install 59 | - name: Build 60 | run: python -m build 61 | - name: Upload 62 | uses: actions/upload-artifact@v4 63 | with: 64 | name: sdist 65 | path: dist 66 | 67 | publish: 68 | needs: build 69 | runs-on: ubuntu-latest 70 | if: startsWith(github.ref, 'refs/tags/v') 71 | permissions: 72 | id-token: write 73 | steps: 74 | - uses: actions/download-artifact@v4 75 | with: 76 | name: sdist 77 | path: dist 78 | - uses: pypa/gh-action-pypi-publish@release/v1 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | README 2 | test.db* 3 | html/ 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # dotenv 87 | .env 88 | 89 | # virtualenv 90 | .venv 91 | venv/ 92 | ENV/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | 107 | # Editors 108 | .vscode/ 109 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | Amethyst Reese 2 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | sphinx: 3 | configuration: docs/conf.py 4 | python: 5 | install: 6 | - method: pip 7 | path: . 8 | extra_requirements: 9 | - docs 10 | build: 11 | os: "ubuntu-22.04" 12 | tools: 13 | python: "3.10" 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | aiosqlite 2 | ========= 3 | 4 | [![Generated by attribution][attribution-badge]][attribution-url] 5 | 6 | 7 | v0.21.0 8 | ------- 9 | 10 | Maintenance release 11 | 12 | - Fix: close connection correctly when BaseException raised in connection (#317) 13 | - Metadata improvements 14 | - Tested and supported on Python 3.13 15 | - Drop support for Python 3.8 16 | - Drop testing on PyPy 17 | 18 | ```text 19 | $ git shortlog -s v0.20.0...v0.21.0 20 | 6 Amethyst Reese 21 | 1 Gabriel 22 | 1 Stanley Kudrow 23 | 11 dependabot[bot] 24 | ``` 25 | 26 | 27 | v0.20.0 28 | ------- 29 | 30 | Feature release 31 | 32 | - Connection `.close()` method is now idempotent (#238) 33 | - Performance improvements in connection thread and event loop (#213, #271) 34 | - Updated contributor guide (#255) 35 | - Tested on Python 3.12 36 | - Dropped support for Python 3.7 (#230) 37 | 38 | ```text 39 | $ git shortlog -s v0.19.0...v0.20.0 40 | 6 Amethyst Reese 41 | 4 J. Nick Koston 42 | 1 Waket Zheng 43 | 18 dependabot[bot] 44 | 1 mike bayer 45 | 1 vdergachyov 46 | ``` 47 | 48 | 49 | v0.19.0 50 | ------- 51 | 52 | Feature release 53 | 54 | - Add support for setting cursor `row_factory` (#229) 55 | - Dropped unused compatibility shims for 3.5 and 3.6 56 | - Deprecated: Python 3.7 support will be dropped in v0.20.0 57 | 58 | ```text 59 | $ git shortlog -s v0.18.0...v0.19.0 60 | 13 Amethyst Reese 61 | 1 Daniel Baulig 62 | 4 dependabot[bot] 63 | ``` 64 | 65 | 66 | v0.18.0 67 | ------- 68 | 69 | Feature release 70 | 71 | - Added support for `paramstyle` (#197) 72 | - Better type hints for `isolation_level` (#172) and `text_factory` (#179) 73 | - Use stdlib typing module when possible (#114) 74 | - Replace aiounittest with stdlib on 3.8+ 75 | - Docmentation improvements (#108) 76 | - Dropped support for Python 3.7, added support for Python 3.10 and 3.11 (#208) 77 | 78 | ```text 79 | $ git shortlog -s v0.17.0...v0.18.0 80 | 31 Amethyst Reese 81 | 3 Nico0 Smart 82 | 3 Nicolas Martinez 83 | 37 dependabot[bot] 84 | 2 pandaninjas 85 | 6 pyup.io bot 86 | 1 vexelnet 87 | ``` 88 | 89 | 90 | v0.17.0 91 | ------- 92 | 93 | Feature release 94 | 95 | * Connection objects now raise ValueError when closed and a command is executed (#79) 96 | * Fix documented examples in readme (#104) 97 | 98 | ```text 99 | $ git shortlog -s v0.16.1...v0.17.0 100 | 3 Amethyst Reese 101 | 5 Mariano Sorgente 102 | 1 Nuno André 103 | 1 pyup.io bot 104 | ``` 105 | 106 | 107 | v0.16.1 108 | ------- 109 | 110 | Bug fix release 111 | 112 | - Reduce logging severity for exceptions (#93) 113 | - Stop logging result objects; they can be big (#102) 114 | 115 | ```text 116 | $ git shortlog -s v0.16.0...v0.16.1 117 | 1 Alexei Chetroi 118 | 3 Amethyst Reese 119 | 3 pyup.io bot 120 | ``` 121 | 122 | 123 | v0.16.0 124 | ------- 125 | 126 | Feature release 127 | 128 | * Improved performance for async iteration on cursors (#34, #86) 129 | * Support for deterministic user functions in Python 3.8+ (#81, #83, #84) 130 | * Reduced logging severity for exceptions returned from children (#75, #76) 131 | * Fix InvalidStateError when setting future results (#80, #89) 132 | * Allow user to catch exceptions from `close()` (#68, #90) 133 | * Tested under Python 3.9 (#91) 134 | 135 | ```text 136 | $ git shortlog -s v0.15.0...v0.16.0 137 | 14 Amethyst Reese 138 | 3 Caleb Hattingh 139 | 1 Groosha 140 | 1 Lonami 141 | 4 Lonami Exo 142 | 4 ZsoltM 143 | 1 pyup.io bot 144 | ``` 145 | 146 | 147 | v0.15.0 148 | ------- 149 | 150 | Feature release 151 | 152 | - Support for accessing connections from multiple event loops 153 | - Fixed type annotations for connection methods returning cursors 154 | - Move cursors into separate module from connections 155 | - Deprecated `loop` parameter to `connect()` and `Connection` 156 | 157 | ```text 158 | $ git shortlog -s v0.14.1...v0.15.0 159 | 7 Amethyst Reese 160 | ``` 161 | 162 | 163 | v0.14.1 164 | ------- 165 | 166 | Bugfix release 167 | 168 | - Remove debugging print() calls. Oops! (#72) 169 | 170 | ```text 171 | $ git shortlog -s v0.14.0...v0.14.1 172 | 2 Amethyst Reese 173 | 1 Spyros Roum 174 | ``` 175 | 176 | 177 | v0.14.0 178 | ------- 179 | 180 | Feature release 181 | 182 | - `Connection.backup()` now supported (#71) 183 | - PEP 561 support added to mark the package as type annotated (#69) 184 | - Better/fixed type annotations for context managers (#70) 185 | 186 | ```text 187 | $ git shortlog -s v0.13.0...v0.14.0 188 | 5 Amethyst Reese 189 | 3 montag451 190 | ``` 191 | 192 | 193 | v0.13.0 194 | ------- 195 | 196 | Feature release 197 | 198 | - `cursor.execute*()` now returns the cursor to match sqlite3 API (#62) 199 | - `Connection.set_trace_callback()` now supported (#62) 200 | - `Connection.iterdump()` is now supported (#66) 201 | - Fixed possible hung thread if connection failed (#55) 202 | - Dropped support for Python 3.5 203 | 204 | ```text 205 | $ git shortlog -s v0.12.0...v0.13.0 206 | 32 Amethyst Reese 207 | 1 pyup.io bot 208 | 5 shipmints 209 | ``` 210 | 211 | 212 | v0.12.0 213 | ------- 214 | 215 | Feature Release 216 | 217 | - Add support for custom functions (#58) 218 | - Official support for Python 3.8 219 | 220 | ```text 221 | $ git shortlog -s v0.11.0...v0.12.0 222 | 4 Amethyst Reese 223 | 1 dmitrypolo 224 | 3 pyup.io bot 225 | ``` 226 | 227 | 228 | v0.11.0 229 | ------- 230 | 231 | Feature release v0.11.0 232 | 233 | - Added support for `set_progress_handler` (#49) 234 | - Improved and updated documentation 235 | 236 | ```text 237 | $ git shortlog -s v0.10.0...v0.11.0 238 | 11 Amethyst Reese 239 | 4 Stanislas 240 | 2 Vladislav Yarmak 241 | 1 pyup-bot 242 | 5 tat2grl85 243 | ``` 244 | 245 | 246 | v0.10.0 247 | ------- 248 | 249 | Feature release v0.10.0: 250 | 251 | - Support using connections without context managers (#29) 252 | - Include test suite in aiosqlite package 253 | 254 | ```text 255 | $ git shortlog -s v0.9.0...v0.10.0 256 | 16 Amethyst Reese 257 | 1 Simon Willison 258 | 1 dark0ghost 259 | ``` 260 | 261 | 262 | v0.9.0 263 | ------ 264 | 265 | Feature release v0.9.0: 266 | 267 | - Support for sqlite extensions 268 | - Fixed support for type annotations on early Python 3.5 269 | 270 | ```text 271 | $ git shortlog -s v0.8.1...v0.9.0 272 | 2 Alexander Lyon 273 | 3 Amethyst Reese 274 | ``` 275 | 276 | 277 | v0.8.1 278 | ------ 279 | 280 | Bug fix release v0.8.1: 281 | 282 | - Fix connections to byte string db locations (#20) 283 | 284 | ```text 285 | $ git shortlog -s v0.8.0...v0.8.1 286 | 6 Amethyst Reese 287 | 1 DevilXD 288 | ``` 289 | 290 | 291 | v0.8.0 292 | ------ 293 | 294 | Major release v0.8.0: 295 | 296 | - Use futures instead of polling for connections/cursors. 297 | This will significantly reduce time spent blocking the 298 | primary event loop, resulting in better performance of 299 | asyncio applications using aiosqlite. 300 | 301 | ```text 302 | $ git shortlog -s v0.7.0...v0.8.0 303 | 3 Amethyst Reese 304 | 2 Matthew Schubert 305 | ``` 306 | 307 | 308 | v0.7.0 309 | ------ 310 | 311 | Feature release v0.7.0: 312 | 313 | - Added macros for combined insert/id and select/fetch 314 | - Better perf testing output 315 | 316 | ```text 317 | $ git shortlog -s v0.6.0...v0.7.0 318 | 4 Amethyst Reese 319 | 1 Grigi 320 | ``` 321 | 322 | 323 | v0.6.0 324 | ------ 325 | 326 | Feature release v0.6.0: 327 | 328 | - Performance improvements for atomic or fast queries 329 | - Support passing Path-like objects to aiosqlite.connect 330 | - Unit tests now use aiounittest instead of a custom test harness 331 | - Limited set of performance tests now available 332 | 333 | ```text 334 | $ git shortlog -s v0.5.0...v0.6.0 335 | 8 Amethyst Reese 336 | 1 Grigi 337 | ``` 338 | 339 | 340 | v0.5.0 341 | ------ 342 | 343 | Feature release v0.5.0: 344 | 345 | - More aliases from sqlite3, including Row, errors, and register_* 346 | - Additional connection properties for row/text factory, total changes 347 | - Better readme 348 | 349 | ```text 350 | $ git shortlog -s v0.4.0...v0.5.0 351 | 6 Amethyst Reese 352 | ``` 353 | 354 | 355 | v0.4.0 356 | ------ 357 | 358 | Feature release v0.4.0: 359 | 360 | - Enable using a custom asyncio event loop 361 | - Increase performance by decreasing sleep time 362 | 363 | ```text 364 | $ git shortlog -s v0.3.0...v0.4.0 365 | 15 Amethyst Reese 366 | 1 Justin Kula 367 | 1 Richard Schwab 368 | ``` 369 | 370 | 371 | v0.3.0 372 | ------ 373 | 374 | Feature release v0.3.0: 375 | 376 | - Cursors can be used as context managers 377 | 378 | ```text 379 | $ git shortlog -s v0.2.2...v0.3.0 380 | 6 Amethyst Reese 381 | 5 Linus Lewandowski 382 | ``` 383 | 384 | 385 | v0.2.2 386 | ------ 387 | 388 | Minor release: 389 | 390 | - Correct aiosqlite.__version__ 391 | - Markdown readme, release via twine 392 | 393 | ```text 394 | $ git shortlog -s v0.2.1...v0.2.2 395 | 5 Amethyst Reese 396 | ``` 397 | 398 | 399 | v0.2.1 400 | ------ 401 | 402 | Minor release v0.2.1: 403 | 404 | - Increase polling speed on event loop 405 | - Using black and pylint 406 | 407 | ```text 408 | $ git shortlog -s v0.2.0...v0.2.1 409 | 8 Amethyst Reese 410 | 2 Pavol Vargovcik 411 | ``` 412 | 413 | 414 | v0.2.0 415 | ------ 416 | 417 | Beta version 0.2.0 418 | 419 | ```text 420 | $ git shortlog -s v0.2.0 421 | 20 Amethyst Reese 422 | ``` 423 | 424 | [attribution-badge]: 425 | https://img.shields.io/badge/generated%20by-attribution-informational 426 | [attribution-url]: https://attribution.omnilib.dev 427 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@omnilib.dev. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to aiosqlite 2 | 3 | ## Preparation 4 | 5 | You'll need to have at least Python 3.9 available for testing. 6 | 7 | You can do this with [pyenv][]: 8 | 9 | $ pyenv install 10 | $ pyenv local 11 | 12 | 13 | ## Setup 14 | 15 | Once cloned, create a clean virtual environment and 16 | install the appropriate tools and dependencies: 17 | 18 | $ cd 19 | $ make venv 20 | $ source .venv/bin/activate 21 | 22 | 23 | ## Formatting 24 | 25 | aiosqlite uses *[ufmt][]* for formatting code and imports. 26 | If your editor does not already support this workflow, 27 | you can manually format files: 28 | 29 | $ make format 30 | 31 | 32 | ## Testing 33 | 34 | Once you've made changes, you should run unit tests, 35 | validate your type annotations, and ensure your code 36 | meets the appropriate style and linting rules: 37 | 38 | $ make test lint 39 | 40 | 41 | ## Submitting 42 | 43 | Before submitting a pull request, please ensure 44 | that you have done the following: 45 | 46 | * Documented changes or features in README.md 47 | * Added appropriate license headers to new files 48 | * Written or modified tests for new functionality 49 | * Formatted code following project standards 50 | * Validated code and formatting with `make test lint` 51 | 52 | [pyenv]: https://github.com/pyenv/pyenv 53 | [µfmt]: https://ufmt.omnilib.dev 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Amethyst Reese 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 README.md LICENSE 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | aiosqlite\: Sqlite for AsyncIO 2 | ============================== 3 | 4 | .. image:: https://readthedocs.org/projects/aiosqlite/badge/?version=latest 5 | :target: https://aiosqlite.omnilib.dev/en/latest/?badge=latest 6 | :alt: Documentation Status 7 | .. image:: https://img.shields.io/pypi/v/aiosqlite.svg 8 | :target: https://pypi.org/project/aiosqlite 9 | :alt: PyPI Release 10 | .. image:: https://img.shields.io/badge/change-log-blue 11 | :target: https://github.com/omnilib/aiosqlite/blob/master/CHANGELOG.md 12 | :alt: Changelog 13 | .. image:: https://img.shields.io/pypi/l/aiosqlite.svg 14 | :target: https://github.com/omnilib/aiosqlite/blob/master/LICENSE 15 | :alt: MIT Licensed 16 | 17 | aiosqlite provides a friendly, async interface to sqlite databases. 18 | 19 | It replicates the standard ``sqlite3`` module, but with async versions 20 | of all the standard connection and cursor methods, plus context managers for 21 | automatically closing connections and cursors: 22 | 23 | .. code-block:: python 24 | 25 | async with aiosqlite.connect(...) as db: 26 | await db.execute("INSERT INTO some_table ...") 27 | await db.commit() 28 | 29 | async with db.execute("SELECT * FROM some_table") as cursor: 30 | async for row in cursor: 31 | ... 32 | 33 | It can also be used in the traditional, procedural manner: 34 | 35 | .. code-block:: python 36 | 37 | db = await aiosqlite.connect(...) 38 | cursor = await db.execute('SELECT * FROM some_table') 39 | row = await cursor.fetchone() 40 | rows = await cursor.fetchall() 41 | await cursor.close() 42 | await db.close() 43 | 44 | aiosqlite also replicates most of the advanced features of ``sqlite3``: 45 | 46 | .. code-block:: python 47 | 48 | async with aiosqlite.connect(...) as db: 49 | db.row_factory = aiosqlite.Row 50 | async with db.execute('SELECT * FROM some_table') as cursor: 51 | async for row in cursor: 52 | value = row['column'] 53 | 54 | await db.execute('INSERT INTO foo some_table') 55 | assert db.total_changes > 0 56 | 57 | 58 | Install 59 | ------- 60 | 61 | aiosqlite is compatible with Python 3.8 and newer. 62 | You can install it from PyPI: 63 | 64 | .. code-block:: console 65 | 66 | $ pip install aiosqlite 67 | 68 | 69 | Details 70 | ------- 71 | 72 | aiosqlite allows interaction with SQLite databases on the main AsyncIO event 73 | loop without blocking execution of other coroutines while waiting for queries 74 | or data fetches. It does this by using a single, shared thread per connection. 75 | This thread executes all actions within a shared request queue to prevent 76 | overlapping actions. 77 | 78 | Connection objects are proxies to the real connections, contain the shared 79 | execution thread, and provide context managers to handle automatically closing 80 | connections. Cursors are similarly proxies to the real cursors, and provide 81 | async iterators to query results. 82 | 83 | 84 | License 85 | ------- 86 | 87 | aiosqlite is copyright `Amethyst Reese `_, and licensed under the 88 | MIT license. I am providing code in this repository to you under an open source 89 | license. This is my personal repository; the license you receive to my code 90 | is from me and not from my employer. See the `LICENSE`_ file for details. 91 | 92 | .. _LICENSE: https://github.com/omnilib/aiosqlite/blob/master/LICENSE 93 | -------------------------------------------------------------------------------- /aiosqlite/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright Amethyst Reese 2 | # Licensed under the MIT license 3 | 4 | """asyncio bridge to the standard sqlite3 module""" 5 | 6 | from sqlite3 import ( # pylint: disable=redefined-builtin 7 | DatabaseError, 8 | Error, 9 | IntegrityError, 10 | NotSupportedError, 11 | OperationalError, 12 | paramstyle, 13 | ProgrammingError, 14 | register_adapter, 15 | register_converter, 16 | Row, 17 | sqlite_version, 18 | sqlite_version_info, 19 | Warning, 20 | ) 21 | 22 | __author__ = "Amethyst Reese" 23 | from .__version__ import __version__ 24 | from .core import connect, Connection, Cursor 25 | 26 | __all__ = [ 27 | "__version__", 28 | "paramstyle", 29 | "register_adapter", 30 | "register_converter", 31 | "sqlite_version", 32 | "sqlite_version_info", 33 | "connect", 34 | "Connection", 35 | "Cursor", 36 | "Row", 37 | "Warning", 38 | "Error", 39 | "DatabaseError", 40 | "IntegrityError", 41 | "ProgrammingError", 42 | "OperationalError", 43 | "NotSupportedError", 44 | ] 45 | -------------------------------------------------------------------------------- /aiosqlite/__version__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is automatically generated by attribution. 3 | 4 | Do not edit manually. Get more info at https://attribution.omnilib.dev 5 | """ 6 | 7 | __version__ = "0.21.0" 8 | -------------------------------------------------------------------------------- /aiosqlite/context.py: -------------------------------------------------------------------------------- 1 | # Copyright Amethyst Reese 2 | # Licensed under the MIT license 3 | 4 | 5 | from collections.abc import Coroutine, Generator 6 | from contextlib import AbstractAsyncContextManager 7 | from functools import wraps 8 | from typing import Any, Callable, TypeVar 9 | 10 | from .cursor import Cursor 11 | 12 | _T = TypeVar("_T") 13 | 14 | 15 | class Result(AbstractAsyncContextManager[_T], Coroutine[Any, Any, _T]): 16 | __slots__ = ("_coro", "_obj") 17 | 18 | def __init__(self, coro: Coroutine[Any, Any, _T]): 19 | self._coro = coro 20 | self._obj: _T 21 | 22 | def send(self, value) -> None: 23 | return self._coro.send(value) 24 | 25 | def throw(self, typ, val=None, tb=None) -> None: 26 | if val is None: 27 | return self._coro.throw(typ) 28 | 29 | if tb is None: 30 | return self._coro.throw(typ, val) 31 | 32 | return self._coro.throw(typ, val, tb) 33 | 34 | def close(self) -> None: 35 | return self._coro.close() 36 | 37 | def __await__(self) -> Generator[Any, None, _T]: 38 | return self._coro.__await__() 39 | 40 | async def __aenter__(self) -> _T: 41 | self._obj = await self._coro 42 | return self._obj 43 | 44 | async def __aexit__(self, exc_type, exc, tb) -> None: 45 | if isinstance(self._obj, Cursor): 46 | await self._obj.close() 47 | 48 | 49 | def contextmanager( 50 | method: Callable[..., Coroutine[Any, Any, _T]], 51 | ) -> Callable[..., Result[_T]]: 52 | @wraps(method) 53 | def wrapper(self, *args, **kwargs) -> Result[_T]: 54 | return Result(method(self, *args, **kwargs)) 55 | 56 | return wrapper 57 | -------------------------------------------------------------------------------- /aiosqlite/core.py: -------------------------------------------------------------------------------- 1 | # Copyright Amethyst Reese 2 | # Licensed under the MIT license 3 | 4 | """ 5 | Core implementation of aiosqlite proxies 6 | """ 7 | 8 | import asyncio 9 | import logging 10 | import sqlite3 11 | from collections.abc import AsyncIterator, Generator, Iterable 12 | from functools import partial 13 | from pathlib import Path 14 | from queue import Empty, Queue, SimpleQueue 15 | from threading import Thread 16 | from typing import Any, Callable, Literal, Optional, Union 17 | from warnings import warn 18 | 19 | from .context import contextmanager 20 | from .cursor import Cursor 21 | 22 | __all__ = ["connect", "Connection", "Cursor"] 23 | 24 | LOG = logging.getLogger("aiosqlite") 25 | 26 | 27 | IsolationLevel = Optional[Literal["DEFERRED", "IMMEDIATE", "EXCLUSIVE"]] 28 | 29 | 30 | def set_result(fut: asyncio.Future, result: Any) -> None: 31 | """Set the result of a future if it hasn't been set already.""" 32 | if not fut.done(): 33 | fut.set_result(result) 34 | 35 | 36 | def set_exception(fut: asyncio.Future, e: BaseException) -> None: 37 | """Set the exception of a future if it hasn't been set already.""" 38 | if not fut.done(): 39 | fut.set_exception(e) 40 | 41 | 42 | _STOP_RUNNING_SENTINEL = object() 43 | 44 | 45 | class Connection(Thread): 46 | def __init__( 47 | self, 48 | connector: Callable[[], sqlite3.Connection], 49 | iter_chunk_size: int, 50 | loop: Optional[asyncio.AbstractEventLoop] = None, 51 | ) -> None: 52 | super().__init__() 53 | self._running = True 54 | self._connection: Optional[sqlite3.Connection] = None 55 | self._connector = connector 56 | self._tx: SimpleQueue[tuple[asyncio.Future, Callable[[], Any]]] = SimpleQueue() 57 | self._iter_chunk_size = iter_chunk_size 58 | 59 | if loop is not None: 60 | warn( 61 | "aiosqlite.Connection no longer uses the `loop` parameter", 62 | DeprecationWarning, 63 | ) 64 | 65 | def _stop_running(self): 66 | self._running = False 67 | # PEP 661 is not accepted yet, so we cannot type a sentinel 68 | self._tx.put_nowait(_STOP_RUNNING_SENTINEL) # type: ignore[arg-type] 69 | 70 | @property 71 | def _conn(self) -> sqlite3.Connection: 72 | if self._connection is None: 73 | raise ValueError("no active connection") 74 | 75 | return self._connection 76 | 77 | def _execute_insert(self, sql: str, parameters: Any) -> Optional[sqlite3.Row]: 78 | cursor = self._conn.execute(sql, parameters) 79 | cursor.execute("SELECT last_insert_rowid()") 80 | return cursor.fetchone() 81 | 82 | def _execute_fetchall(self, sql: str, parameters: Any) -> Iterable[sqlite3.Row]: 83 | cursor = self._conn.execute(sql, parameters) 84 | return cursor.fetchall() 85 | 86 | def run(self) -> None: 87 | """ 88 | Execute function calls on a separate thread. 89 | 90 | :meta private: 91 | """ 92 | while True: 93 | # Continues running until all queue items are processed, 94 | # even after connection is closed (so we can finalize all 95 | # futures) 96 | 97 | tx_item = self._tx.get() 98 | if tx_item is _STOP_RUNNING_SENTINEL: 99 | break 100 | 101 | future, function = tx_item 102 | 103 | try: 104 | LOG.debug("executing %s", function) 105 | result = function() 106 | LOG.debug("operation %s completed", function) 107 | future.get_loop().call_soon_threadsafe(set_result, future, result) 108 | except BaseException as e: # noqa B036 109 | LOG.debug("returning exception %s", e) 110 | future.get_loop().call_soon_threadsafe(set_exception, future, e) 111 | 112 | async def _execute(self, fn, *args, **kwargs): 113 | """Queue a function with the given arguments for execution.""" 114 | if not self._running or not self._connection: 115 | raise ValueError("Connection closed") 116 | 117 | function = partial(fn, *args, **kwargs) 118 | future = asyncio.get_event_loop().create_future() 119 | 120 | self._tx.put_nowait((future, function)) 121 | 122 | return await future 123 | 124 | async def _connect(self) -> "Connection": 125 | """Connect to the actual sqlite database.""" 126 | if self._connection is None: 127 | try: 128 | future = asyncio.get_event_loop().create_future() 129 | self._tx.put_nowait((future, self._connector)) 130 | self._connection = await future 131 | except BaseException: 132 | self._stop_running() 133 | self._connection = None 134 | raise 135 | 136 | return self 137 | 138 | def __await__(self) -> Generator[Any, None, "Connection"]: 139 | self.start() 140 | return self._connect().__await__() 141 | 142 | async def __aenter__(self) -> "Connection": 143 | return await self 144 | 145 | async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: 146 | await self.close() 147 | 148 | @contextmanager 149 | async def cursor(self) -> Cursor: 150 | """Create an aiosqlite cursor wrapping a sqlite3 cursor object.""" 151 | return Cursor(self, await self._execute(self._conn.cursor)) 152 | 153 | async def commit(self) -> None: 154 | """Commit the current transaction.""" 155 | await self._execute(self._conn.commit) 156 | 157 | async def rollback(self) -> None: 158 | """Roll back the current transaction.""" 159 | await self._execute(self._conn.rollback) 160 | 161 | async def close(self) -> None: 162 | """Complete queued queries/cursors and close the connection.""" 163 | 164 | if self._connection is None: 165 | return 166 | 167 | try: 168 | await self._execute(self._conn.close) 169 | except Exception: 170 | LOG.info("exception occurred while closing connection") 171 | raise 172 | finally: 173 | self._stop_running() 174 | self._connection = None 175 | 176 | @contextmanager 177 | async def execute( 178 | self, sql: str, parameters: Optional[Iterable[Any]] = None 179 | ) -> Cursor: 180 | """Helper to create a cursor and execute the given query.""" 181 | if parameters is None: 182 | parameters = [] 183 | cursor = await self._execute(self._conn.execute, sql, parameters) 184 | return Cursor(self, cursor) 185 | 186 | @contextmanager 187 | async def execute_insert( 188 | self, sql: str, parameters: Optional[Iterable[Any]] = None 189 | ) -> Optional[sqlite3.Row]: 190 | """Helper to insert and get the last_insert_rowid.""" 191 | if parameters is None: 192 | parameters = [] 193 | return await self._execute(self._execute_insert, sql, parameters) 194 | 195 | @contextmanager 196 | async def execute_fetchall( 197 | self, sql: str, parameters: Optional[Iterable[Any]] = None 198 | ) -> Iterable[sqlite3.Row]: 199 | """Helper to execute a query and return all the data.""" 200 | if parameters is None: 201 | parameters = [] 202 | return await self._execute(self._execute_fetchall, sql, parameters) 203 | 204 | @contextmanager 205 | async def executemany( 206 | self, sql: str, parameters: Iterable[Iterable[Any]] 207 | ) -> Cursor: 208 | """Helper to create a cursor and execute the given multiquery.""" 209 | cursor = await self._execute(self._conn.executemany, sql, parameters) 210 | return Cursor(self, cursor) 211 | 212 | @contextmanager 213 | async def executescript(self, sql_script: str) -> Cursor: 214 | """Helper to create a cursor and execute a user script.""" 215 | cursor = await self._execute(self._conn.executescript, sql_script) 216 | return Cursor(self, cursor) 217 | 218 | async def interrupt(self) -> None: 219 | """Interrupt pending queries.""" 220 | return self._conn.interrupt() 221 | 222 | async def create_function( 223 | self, name: str, num_params: int, func: Callable, deterministic: bool = False 224 | ) -> None: 225 | """ 226 | Create user-defined function that can be later used 227 | within SQL statements. Must be run within the same thread 228 | that query executions take place so instead of executing directly 229 | against the connection, we defer this to `run` function. 230 | 231 | If ``deterministic`` is true, the created function is marked as deterministic, 232 | which allows SQLite to perform additional optimizations. This flag is supported 233 | by SQLite 3.8.3 or higher, ``NotSupportedError`` will be raised if used with 234 | older versions. 235 | """ 236 | await self._execute( 237 | self._conn.create_function, 238 | name, 239 | num_params, 240 | func, 241 | deterministic=deterministic, 242 | ) 243 | 244 | @property 245 | def in_transaction(self) -> bool: 246 | return self._conn.in_transaction 247 | 248 | @property 249 | def isolation_level(self) -> Optional[str]: 250 | return self._conn.isolation_level 251 | 252 | @isolation_level.setter 253 | def isolation_level(self, value: IsolationLevel) -> None: 254 | self._conn.isolation_level = value 255 | 256 | @property 257 | def row_factory(self) -> Optional[type]: 258 | return self._conn.row_factory 259 | 260 | @row_factory.setter 261 | def row_factory(self, factory: Optional[type]) -> None: 262 | self._conn.row_factory = factory 263 | 264 | @property 265 | def text_factory(self) -> Callable[[bytes], Any]: 266 | return self._conn.text_factory 267 | 268 | @text_factory.setter 269 | def text_factory(self, factory: Callable[[bytes], Any]) -> None: 270 | self._conn.text_factory = factory 271 | 272 | @property 273 | def total_changes(self) -> int: 274 | return self._conn.total_changes 275 | 276 | async def enable_load_extension(self, value: bool) -> None: 277 | await self._execute(self._conn.enable_load_extension, value) # type: ignore 278 | 279 | async def load_extension(self, path: str): 280 | await self._execute(self._conn.load_extension, path) # type: ignore 281 | 282 | async def set_progress_handler( 283 | self, handler: Callable[[], Optional[int]], n: int 284 | ) -> None: 285 | await self._execute(self._conn.set_progress_handler, handler, n) 286 | 287 | async def set_trace_callback(self, handler: Callable) -> None: 288 | await self._execute(self._conn.set_trace_callback, handler) 289 | 290 | async def iterdump(self) -> AsyncIterator[str]: 291 | """ 292 | Return an async iterator to dump the database in SQL text format. 293 | 294 | Example:: 295 | 296 | async for line in db.iterdump(): 297 | ... 298 | 299 | """ 300 | dump_queue: Queue = Queue() 301 | 302 | def dumper(): 303 | try: 304 | for line in self._conn.iterdump(): 305 | dump_queue.put_nowait(line) 306 | dump_queue.put_nowait(None) 307 | 308 | except Exception: 309 | LOG.exception("exception while dumping db") 310 | dump_queue.put_nowait(None) 311 | raise 312 | 313 | fut = self._execute(dumper) 314 | task = asyncio.ensure_future(fut) 315 | 316 | while True: 317 | try: 318 | line: Optional[str] = dump_queue.get_nowait() 319 | if line is None: 320 | break 321 | yield line 322 | 323 | except Empty: 324 | if task.done(): 325 | LOG.warning("iterdump completed unexpectedly") 326 | break 327 | 328 | await asyncio.sleep(0.01) 329 | 330 | await task 331 | 332 | async def backup( 333 | self, 334 | target: Union["Connection", sqlite3.Connection], 335 | *, 336 | pages: int = 0, 337 | progress: Optional[Callable[[int, int, int], None]] = None, 338 | name: str = "main", 339 | sleep: float = 0.250, 340 | ) -> None: 341 | """ 342 | Make a backup of the current database to the target database. 343 | 344 | Takes either a standard sqlite3 or aiosqlite Connection object as the target. 345 | """ 346 | if isinstance(target, Connection): 347 | target = target._conn 348 | 349 | await self._execute( 350 | self._conn.backup, 351 | target, 352 | pages=pages, 353 | progress=progress, 354 | name=name, 355 | sleep=sleep, 356 | ) 357 | 358 | 359 | def connect( 360 | database: Union[str, Path], 361 | *, 362 | iter_chunk_size=64, 363 | loop: Optional[asyncio.AbstractEventLoop] = None, 364 | **kwargs: Any, 365 | ) -> Connection: 366 | """Create and return a connection proxy to the sqlite database.""" 367 | 368 | if loop is not None: 369 | warn( 370 | "aiosqlite.connect() no longer uses the `loop` parameter", 371 | DeprecationWarning, 372 | ) 373 | 374 | def connector() -> sqlite3.Connection: 375 | if isinstance(database, str): 376 | loc = database 377 | elif isinstance(database, bytes): 378 | loc = database.decode("utf-8") 379 | else: 380 | loc = str(database) 381 | 382 | return sqlite3.connect(loc, **kwargs) 383 | 384 | return Connection(connector, iter_chunk_size) 385 | -------------------------------------------------------------------------------- /aiosqlite/cursor.py: -------------------------------------------------------------------------------- 1 | # Copyright Amethyst Reese 2 | # Licensed under the MIT license 3 | 4 | import sqlite3 5 | from collections.abc import AsyncIterator, Iterable 6 | from typing import Any, Callable, Optional, TYPE_CHECKING 7 | 8 | if TYPE_CHECKING: 9 | from .core import Connection 10 | 11 | 12 | class Cursor: 13 | def __init__(self, conn: "Connection", cursor: sqlite3.Cursor) -> None: 14 | self.iter_chunk_size = conn._iter_chunk_size 15 | self._conn = conn 16 | self._cursor = cursor 17 | 18 | def __aiter__(self) -> AsyncIterator[sqlite3.Row]: 19 | """The cursor proxy is also an async iterator.""" 20 | return self._fetch_chunked() 21 | 22 | async def _fetch_chunked(self): 23 | while True: 24 | rows = await self.fetchmany(self.iter_chunk_size) 25 | if not rows: 26 | return 27 | for row in rows: 28 | yield row 29 | 30 | async def _execute(self, fn, *args, **kwargs): 31 | """Execute the given function on the shared connection's thread.""" 32 | return await self._conn._execute(fn, *args, **kwargs) 33 | 34 | async def execute( 35 | self, sql: str, parameters: Optional[Iterable[Any]] = None 36 | ) -> "Cursor": 37 | """Execute the given query.""" 38 | if parameters is None: 39 | parameters = [] 40 | await self._execute(self._cursor.execute, sql, parameters) 41 | return self 42 | 43 | async def executemany( 44 | self, sql: str, parameters: Iterable[Iterable[Any]] 45 | ) -> "Cursor": 46 | """Execute the given multiquery.""" 47 | await self._execute(self._cursor.executemany, sql, parameters) 48 | return self 49 | 50 | async def executescript(self, sql_script: str) -> "Cursor": 51 | """Execute a user script.""" 52 | await self._execute(self._cursor.executescript, sql_script) 53 | return self 54 | 55 | async def fetchone(self) -> Optional[sqlite3.Row]: 56 | """Fetch a single row.""" 57 | return await self._execute(self._cursor.fetchone) 58 | 59 | async def fetchmany(self, size: Optional[int] = None) -> Iterable[sqlite3.Row]: 60 | """Fetch up to `cursor.arraysize` number of rows.""" 61 | args: tuple[int, ...] = () 62 | if size is not None: 63 | args = (size,) 64 | return await self._execute(self._cursor.fetchmany, *args) 65 | 66 | async def fetchall(self) -> Iterable[sqlite3.Row]: 67 | """Fetch all remaining rows.""" 68 | return await self._execute(self._cursor.fetchall) 69 | 70 | async def close(self) -> None: 71 | """Close the cursor.""" 72 | await self._execute(self._cursor.close) 73 | 74 | @property 75 | def rowcount(self) -> int: 76 | return self._cursor.rowcount 77 | 78 | @property 79 | def lastrowid(self) -> Optional[int]: 80 | return self._cursor.lastrowid 81 | 82 | @property 83 | def arraysize(self) -> int: 84 | return self._cursor.arraysize 85 | 86 | @arraysize.setter 87 | def arraysize(self, value: int) -> None: 88 | self._cursor.arraysize = value 89 | 90 | @property 91 | def description(self) -> tuple[tuple[str, None, None, None, None, None, None], ...]: 92 | return self._cursor.description 93 | 94 | @property 95 | def row_factory(self) -> Optional[Callable[[sqlite3.Cursor, sqlite3.Row], object]]: 96 | return self._cursor.row_factory 97 | 98 | @row_factory.setter 99 | def row_factory(self, factory: Optional[type]) -> None: 100 | self._cursor.row_factory = factory 101 | 102 | @property 103 | def connection(self) -> sqlite3.Connection: 104 | return self._cursor.connection 105 | 106 | async def __aenter__(self): 107 | return self 108 | 109 | async def __aexit__(self, exc_type, exc_val, exc_tb): 110 | await self.close() 111 | -------------------------------------------------------------------------------- /aiosqlite/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omnilib/aiosqlite/895fd9183b43cecce89310d8ef00f9f632b9afe9/aiosqlite/py.typed -------------------------------------------------------------------------------- /aiosqlite/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright Amethyst Reese 2 | # Licensed under the MIT license 3 | 4 | from .smoke import SmokeTest 5 | -------------------------------------------------------------------------------- /aiosqlite/tests/__main__.py: -------------------------------------------------------------------------------- 1 | # Copyright Amethyst Reese 2 | # Licensed under the MIT license 3 | 4 | import unittest 5 | 6 | if __name__ == "__main__": 7 | unittest.main(module="aiosqlite.tests", verbosity=2) 8 | -------------------------------------------------------------------------------- /aiosqlite/tests/helpers.py: -------------------------------------------------------------------------------- 1 | # Copyright Amethyst Reese 2 | # Licensed under the MIT license 3 | 4 | import logging 5 | import sys 6 | 7 | 8 | def setup_logger(): 9 | log = logging.getLogger("") 10 | log.setLevel(logging.INFO) 11 | 12 | logging.addLevelName(logging.ERROR, "E") 13 | logging.addLevelName(logging.WARNING, "W") 14 | logging.addLevelName(logging.INFO, "I") 15 | logging.addLevelName(logging.DEBUG, "V") 16 | 17 | date_fmt = r"%H:%M:%S" 18 | verbose_fmt = ( 19 | "%(asctime)s,%(msecs)d %(levelname)s " 20 | "%(module)s:%(funcName)s():%(lineno)d " 21 | "%(message)s" 22 | ) 23 | 24 | handler = logging.StreamHandler(sys.stdout) 25 | handler.setLevel(logging.INFO) 26 | handler.setFormatter(logging.Formatter(verbose_fmt, date_fmt)) 27 | log.addHandler(handler) 28 | 29 | return log 30 | -------------------------------------------------------------------------------- /aiosqlite/tests/perf.py: -------------------------------------------------------------------------------- 1 | # Copyright Amethyst Reese 2 | # Licensed under the MIT license 3 | 4 | """ 5 | Simple perf tests for aiosqlite and the asyncio run loop. 6 | """ 7 | import string 8 | import tempfile 9 | import time 10 | 11 | from unittest import IsolatedAsyncioTestCase as TestCase 12 | 13 | import aiosqlite 14 | from .smoke import setup_logger 15 | 16 | TEST_DB = ":memory:" 17 | TARGET = 2.0 18 | RESULTS = {} 19 | 20 | 21 | def timed(fn, name=None): 22 | """ 23 | Decorator for perf testing a block of async code. 24 | 25 | Expects the wrapped function to return an async generator. 26 | The generator should do setup, then yield when ready to start perf testing. 27 | The decorator will then pump the generator repeatedly until the target 28 | time has been reached, then close the generator and print perf results. 29 | """ 30 | 31 | name = name or fn.__name__ 32 | 33 | async def wrapper(*args, **kwargs): 34 | gen = fn(*args, **kwargs) 35 | 36 | await gen.asend(None) 37 | count = 0 38 | before = time.time() 39 | 40 | while True: 41 | count += 1 42 | value = time.time() - before < TARGET 43 | try: 44 | if value: 45 | await gen.asend(value) 46 | else: 47 | await gen.aclose() 48 | break 49 | 50 | except StopAsyncIteration: 51 | break 52 | 53 | except Exception as e: 54 | print(f"exception occurred: {e}") 55 | return 56 | 57 | duration = time.time() - before 58 | 59 | RESULTS[name] = (count, duration) 60 | 61 | return wrapper 62 | 63 | 64 | class PerfTest(TestCase): 65 | @classmethod 66 | def setUpClass(cls): 67 | print(f"Running perf tests for at least {TARGET:.1f}s each...") 68 | setup_logger() 69 | 70 | @classmethod 71 | def tearDownClass(cls): 72 | print(f"\n{'Perf Test':<25} Iterations Duration {'Rate':>11}") 73 | for name in sorted(RESULTS): 74 | count, duration = RESULTS[name] 75 | rate = count / duration 76 | name = name.replace("test_", "") 77 | print(f"{name:<25} {count:>10} {duration:>7.1f}s {rate:>9.1f}/s") 78 | 79 | @timed 80 | async def test_connection_memory(self): 81 | while True: 82 | yield 83 | async with aiosqlite.connect(TEST_DB): 84 | pass 85 | 86 | @timed 87 | async def test_connection_file(self): 88 | with tempfile.NamedTemporaryFile(delete=False) as tf: 89 | path = tf.name 90 | tf.close() 91 | 92 | async with aiosqlite.connect(path) as db: 93 | await db.execute( 94 | "create table perf (i integer primary key asc, k integer)" 95 | ) 96 | await db.execute("insert into perf (k) values (2), (3)") 97 | await db.commit() 98 | 99 | while True: 100 | yield 101 | async with aiosqlite.connect(path): 102 | pass 103 | 104 | @timed 105 | async def test_atomics(self): 106 | async with aiosqlite.connect(TEST_DB) as db: 107 | await db.execute("create table perf (i integer primary key asc, k integer)") 108 | await db.execute("insert into perf (k) values (2), (3)") 109 | await db.commit() 110 | 111 | while True: 112 | yield 113 | async with db.execute("select last_insert_rowid()") as cursor: 114 | await cursor.fetchone() 115 | 116 | @timed 117 | async def test_inserts(self): 118 | async with aiosqlite.connect(TEST_DB) as db: 119 | await db.execute("create table perf (i integer primary key asc, k integer)") 120 | await db.commit() 121 | 122 | while True: 123 | yield 124 | await db.execute("insert into perf (k) values (1), (2), (3)") 125 | await db.commit() 126 | 127 | @timed 128 | async def test_insert_ids(self): 129 | async with aiosqlite.connect(TEST_DB) as db: 130 | await db.execute("create table perf (i integer primary key asc, k integer)") 131 | await db.commit() 132 | 133 | while True: 134 | yield 135 | cursor = await db.execute("insert into perf (k) values (1)") 136 | await cursor.execute("select last_insert_rowid()") 137 | await cursor.fetchone() 138 | await db.commit() 139 | 140 | @timed 141 | async def test_insert_macro_ids(self): 142 | async with aiosqlite.connect(TEST_DB) as db: 143 | await db.execute("create table perf (i integer primary key asc, k integer)") 144 | await db.commit() 145 | 146 | while True: 147 | yield 148 | await db.execute_insert("insert into perf (k) values (1)") 149 | await db.commit() 150 | 151 | @timed 152 | async def test_select(self): 153 | async with aiosqlite.connect(TEST_DB) as db: 154 | await db.execute("create table perf (i integer primary key asc, k integer)") 155 | for i in range(100): 156 | await db.execute("insert into perf (k) values (%d)" % (i,)) 157 | await db.commit() 158 | 159 | while True: 160 | yield 161 | cursor = await db.execute("select i, k from perf") 162 | assert len(await cursor.fetchall()) == 100 163 | 164 | @timed 165 | async def test_select_macro(self): 166 | async with aiosqlite.connect(TEST_DB) as db: 167 | await db.execute("create table perf (i integer primary key asc, k integer)") 168 | for i in range(100): 169 | await db.execute("insert into perf (k) values (%d)" % (i,)) 170 | await db.commit() 171 | 172 | while True: 173 | yield 174 | assert len(await db.execute_fetchall("select i, k from perf")) == 100 175 | 176 | async def test_iterable_cursor_perf(self): 177 | async with aiosqlite.connect(TEST_DB) as db: 178 | await db.execute( 179 | "create table ic_perf (" 180 | "i integer primary key asc, k integer, a integer, b integer, c char(16))" 181 | ) 182 | for batch in range(128): # add 128k rows 183 | r_start = batch * 1024 184 | await db.executemany( 185 | "insert into ic_perf (k, a, b, c) values(?, 1, 2, ?)", 186 | [ 187 | *[ 188 | (i, string.ascii_lowercase) 189 | for i in range(r_start, r_start + 1024) 190 | ] 191 | ], 192 | ) 193 | await db.commit() 194 | 195 | async def test_perf(chunk_size: int): 196 | while True: 197 | async with db.execute("SELECT * FROM ic_perf") as cursor: 198 | cursor.iter_chunk_size = chunk_size 199 | async for _ in cursor: 200 | yield 201 | 202 | for chunk_size in [2**i for i in range(4, 11)]: 203 | await timed(test_perf, f"iterable_cursor @ {chunk_size}")(chunk_size) 204 | -------------------------------------------------------------------------------- /aiosqlite/tests/smoke.py: -------------------------------------------------------------------------------- 1 | # Copyright Amethyst Reese 2 | # Licensed under the MIT license 3 | 4 | import asyncio 5 | import sqlite3 6 | from pathlib import Path 7 | from sqlite3 import OperationalError 8 | from tempfile import TemporaryDirectory 9 | from threading import Thread 10 | from unittest import IsolatedAsyncioTestCase, SkipTest 11 | from unittest.mock import patch 12 | 13 | import aiosqlite 14 | from .helpers import setup_logger 15 | 16 | 17 | class SmokeTest(IsolatedAsyncioTestCase): 18 | @classmethod 19 | def setUpClass(cls): 20 | setup_logger() 21 | 22 | def setUp(self): 23 | td = TemporaryDirectory() 24 | self.addCleanup(td.cleanup) 25 | self.db = Path(td.name).resolve() / "test.db" 26 | 27 | async def test_connection_await(self): 28 | db = await aiosqlite.connect(self.db) 29 | self.assertIsInstance(db, aiosqlite.Connection) 30 | 31 | async with db.execute("select 1, 2") as cursor: 32 | rows = await cursor.fetchall() 33 | self.assertEqual(rows, [(1, 2)]) 34 | 35 | await db.close() 36 | 37 | async def test_connection_context(self): 38 | async with aiosqlite.connect(self.db) as db: 39 | self.assertIsInstance(db, aiosqlite.Connection) 40 | 41 | async with db.execute("select 1, 2") as cursor: 42 | rows = await cursor.fetchall() 43 | self.assertEqual(rows, [(1, 2)]) 44 | 45 | async def test_connection_locations(self): 46 | TEST_DB = self.db.as_posix() 47 | 48 | class Fake: # pylint: disable=too-few-public-methods 49 | def __str__(self): 50 | return TEST_DB 51 | 52 | locs = (Path(TEST_DB), TEST_DB, TEST_DB.encode(), Fake()) 53 | 54 | async with aiosqlite.connect(locs[0]) as db: 55 | await db.execute("create table foo (i integer, k integer)") 56 | await db.execute("insert into foo (i, k) values (1, 5)") 57 | await db.commit() 58 | 59 | cursor = await db.execute("select * from foo") 60 | rows = await cursor.fetchall() 61 | 62 | for loc in locs: 63 | async with aiosqlite.connect(loc) as db: 64 | cursor = await db.execute("select * from foo") 65 | self.assertEqual(await cursor.fetchall(), rows) 66 | 67 | async def test_multiple_connections(self): 68 | async with aiosqlite.connect(self.db) as db: 69 | await db.execute( 70 | "create table multiple_connections " 71 | "(i integer primary key asc, k integer)" 72 | ) 73 | 74 | async def do_one_conn(i): 75 | async with aiosqlite.connect(self.db) as db: 76 | await db.execute("insert into multiple_connections (k) values (?)", [i]) 77 | await db.commit() 78 | 79 | await asyncio.gather(*[do_one_conn(i) for i in range(10)]) 80 | 81 | async with aiosqlite.connect(self.db) as db: 82 | cursor = await db.execute("select * from multiple_connections") 83 | rows = await cursor.fetchall() 84 | 85 | assert len(rows) == 10 86 | 87 | async def test_multiple_queries(self): 88 | async with aiosqlite.connect(self.db) as db: 89 | await db.execute( 90 | "create table multiple_queries " 91 | "(i integer primary key asc, k integer)" 92 | ) 93 | 94 | await asyncio.gather( 95 | *[ 96 | db.execute("insert into multiple_queries (k) values (?)", [i]) 97 | for i in range(10) 98 | ] 99 | ) 100 | 101 | await db.commit() 102 | 103 | async with aiosqlite.connect(self.db) as db: 104 | cursor = await db.execute("select * from multiple_queries") 105 | rows = await cursor.fetchall() 106 | 107 | assert len(rows) == 10 108 | 109 | async def test_iterable_cursor(self): 110 | async with aiosqlite.connect(self.db) as db: 111 | cursor = await db.cursor() 112 | await cursor.execute( 113 | "create table iterable_cursor " "(i integer primary key asc, k integer)" 114 | ) 115 | await cursor.executemany( 116 | "insert into iterable_cursor (k) values (?)", [[i] for i in range(10)] 117 | ) 118 | await db.commit() 119 | 120 | async with aiosqlite.connect(self.db) as db: 121 | cursor = await db.execute("select * from iterable_cursor") 122 | rows = [] 123 | async for row in cursor: 124 | rows.append(row) 125 | 126 | assert len(rows) == 10 127 | 128 | async def test_multi_loop_usage(self): 129 | results = {} 130 | 131 | def runner(k, conn): 132 | async def query(): 133 | async with conn.execute("select * from foo") as cursor: 134 | rows = await cursor.fetchall() 135 | self.assertEqual(len(rows), 2) 136 | return rows 137 | 138 | with self.subTest(k): 139 | loop = asyncio.new_event_loop() 140 | rows = loop.run_until_complete(query()) 141 | loop.close() 142 | results[k] = rows 143 | 144 | async with aiosqlite.connect(":memory:") as db: 145 | await db.execute("create table foo (id int, name varchar)") 146 | await db.execute( 147 | "insert into foo values (?, ?), (?, ?)", (1, "Sally", 2, "Janet") 148 | ) 149 | await db.commit() 150 | 151 | threads = [Thread(target=runner, args=(k, db)) for k in range(4)] 152 | for thread in threads: 153 | thread.start() 154 | for thread in threads: 155 | thread.join() 156 | 157 | self.assertEqual(len(results), 4) 158 | for rows in results.values(): 159 | self.assertEqual(len(rows), 2) 160 | 161 | async def test_context_cursor(self): 162 | async with aiosqlite.connect(self.db) as db: 163 | async with db.cursor() as cursor: 164 | await cursor.execute( 165 | "create table context_cursor " 166 | "(i integer primary key asc, k integer)" 167 | ) 168 | await cursor.executemany( 169 | "insert into context_cursor (k) values (?)", 170 | [[i] for i in range(10)], 171 | ) 172 | await db.commit() 173 | 174 | async with aiosqlite.connect(self.db) as db: 175 | async with db.execute("select * from context_cursor") as cursor: 176 | rows = [] 177 | async for row in cursor: 178 | rows.append(row) 179 | 180 | assert len(rows) == 10 181 | 182 | async def test_cursor_return_self(self): 183 | async with aiosqlite.connect(self.db) as db: 184 | cursor = await db.cursor() 185 | 186 | result = await cursor.execute( 187 | "create table test_cursor_return_self (i integer, k integer)" 188 | ) 189 | self.assertEqual(result, cursor, "cursor execute returns itself") 190 | 191 | result = await cursor.executemany( 192 | "insert into test_cursor_return_self values (?, ?)", [(1, 1), (2, 2)] 193 | ) 194 | self.assertEqual(result, cursor) 195 | 196 | result = await cursor.executescript( 197 | "insert into test_cursor_return_self values (3, 3);" 198 | "insert into test_cursor_return_self values (4, 4);" 199 | "insert into test_cursor_return_self values (5, 5);" 200 | ) 201 | self.assertEqual(result, cursor) 202 | 203 | async def test_connection_properties(self): 204 | async with aiosqlite.connect(self.db) as db: 205 | self.assertEqual(db.total_changes, 0) 206 | 207 | async with db.cursor() as cursor: 208 | self.assertFalse(db.in_transaction) 209 | await cursor.execute( 210 | "create table test_properties " 211 | "(i integer primary key asc, k integer, d text)" 212 | ) 213 | await cursor.execute( 214 | "insert into test_properties (k, d) values (1, 'hi')" 215 | ) 216 | self.assertTrue(db.in_transaction) 217 | await db.commit() 218 | self.assertFalse(db.in_transaction) 219 | 220 | self.assertEqual(db.total_changes, 1) 221 | 222 | self.assertIsNone(db.row_factory) 223 | self.assertEqual(db.text_factory, str) 224 | 225 | async with db.cursor() as cursor: 226 | await cursor.execute("select * from test_properties") 227 | row = await cursor.fetchone() 228 | self.assertIsInstance(row, tuple) 229 | self.assertEqual(row, (1, 1, "hi")) 230 | with self.assertRaises(TypeError): 231 | _ = row["k"] 232 | 233 | async with db.cursor() as cursor: 234 | cursor.row_factory = aiosqlite.Row 235 | self.assertEqual(cursor.row_factory, aiosqlite.Row) 236 | await cursor.execute("select * from test_properties") 237 | row = await cursor.fetchone() 238 | self.assertIsInstance(row, aiosqlite.Row) 239 | self.assertEqual(row[1], 1) 240 | self.assertEqual(row[2], "hi") 241 | self.assertEqual(row["k"], 1) 242 | self.assertEqual(row["d"], "hi") 243 | 244 | db.row_factory = aiosqlite.Row 245 | db.text_factory = bytes 246 | self.assertEqual(db.row_factory, aiosqlite.Row) 247 | self.assertEqual(db.text_factory, bytes) 248 | 249 | async with db.cursor() as cursor: 250 | await cursor.execute("select * from test_properties") 251 | row = await cursor.fetchone() 252 | self.assertIsInstance(row, aiosqlite.Row) 253 | self.assertEqual(row[1], 1) 254 | self.assertEqual(row[2], b"hi") 255 | self.assertEqual(row["k"], 1) 256 | self.assertEqual(row["d"], b"hi") 257 | 258 | async def test_fetch_all(self): 259 | async with aiosqlite.connect(self.db) as db: 260 | await db.execute( 261 | "create table test_fetch_all (i integer primary key asc, k integer)" 262 | ) 263 | await db.execute( 264 | "insert into test_fetch_all (k) values (10), (24), (16), (32)" 265 | ) 266 | await db.commit() 267 | 268 | async with aiosqlite.connect(self.db) as db: 269 | cursor = await db.execute("select k from test_fetch_all where k < 30") 270 | rows = await cursor.fetchall() 271 | self.assertEqual(rows, [(10,), (24,), (16,)]) 272 | 273 | async def test_enable_load_extension(self): 274 | """Assert that after enabling extension loading, they can be loaded""" 275 | async with aiosqlite.connect(self.db) as db: 276 | try: 277 | await db.enable_load_extension(True) 278 | await db.load_extension("test") 279 | except OperationalError as e: 280 | assert "not authorized" not in e.args 281 | except AttributeError as e: 282 | raise SkipTest( 283 | "python was not compiled with sqlite3 " 284 | "extension support, so we can't test it" 285 | ) from e 286 | 287 | async def test_set_progress_handler(self): 288 | """ 289 | Assert that after setting a progress handler returning 1, DB operations are aborted 290 | """ 291 | async with aiosqlite.connect(self.db) as db: 292 | await db.set_progress_handler(lambda: 1, 1) 293 | with self.assertRaises(OperationalError): 294 | await db.execute( 295 | "create table test_progress_handler (i integer primary key asc, k integer)" 296 | ) 297 | 298 | async def test_create_function(self): 299 | """Assert that after creating a custom function, it can be used""" 300 | 301 | def no_arg(): 302 | return "no arg" 303 | 304 | def one_arg(num): 305 | return num * 2 306 | 307 | async with aiosqlite.connect(self.db) as db: 308 | await db.create_function("no_arg", 0, no_arg) 309 | await db.create_function("one_arg", 1, one_arg) 310 | 311 | async with db.execute("SELECT no_arg();") as res: 312 | row = await res.fetchone() 313 | self.assertEqual(row[0], "no arg") 314 | 315 | async with db.execute("SELECT one_arg(10);") as res: 316 | row = await res.fetchone() 317 | self.assertEqual(row[0], 20) 318 | 319 | async def test_create_function_deterministic(self): 320 | """Assert that after creating a deterministic custom function, it can be used. 321 | 322 | https://sqlite.org/deterministic.html 323 | """ 324 | 325 | def one_arg(num): 326 | return num * 2 327 | 328 | async with aiosqlite.connect(self.db) as db: 329 | await db.create_function("one_arg", 1, one_arg, deterministic=True) 330 | await db.execute("create table foo (id int, bar int)") 331 | 332 | # Non-deterministic functions cannot be used in indexes 333 | await db.execute("create index t on foo(one_arg(bar))") 334 | 335 | async def test_set_trace_callback(self): 336 | statements = [] 337 | 338 | def callback(statement: str): 339 | statements.append(statement) 340 | 341 | async with aiosqlite.connect(self.db) as db: 342 | await db.set_trace_callback(callback) 343 | 344 | await db.execute("select 10") 345 | self.assertIn("select 10", statements) 346 | 347 | async def test_connect_error(self): 348 | bad_db = Path("/something/that/shouldnt/exist.db") 349 | with self.assertRaisesRegex(OperationalError, "unable to open database"): 350 | async with aiosqlite.connect(bad_db) as db: 351 | self.assertIsNone(db) # should never be reached 352 | 353 | with self.assertRaisesRegex(OperationalError, "unable to open database"): 354 | await aiosqlite.connect(bad_db) 355 | 356 | async def test_connect_base_exception(self): 357 | # Check if connect task is cancelled, thread is properly closed. 358 | def _raise_cancelled_error(*_, **__): 359 | raise asyncio.CancelledError("I changed my mind") 360 | 361 | connection = aiosqlite.Connection(lambda: sqlite3.connect(":memory:"), 64) 362 | with ( 363 | patch.object(sqlite3, "connect", side_effect=_raise_cancelled_error), 364 | self.assertRaisesRegex(asyncio.CancelledError, "I changed my mind"), 365 | ): 366 | async with connection: 367 | ... 368 | # Terminate the thread here if the test fails to have a clear error. 369 | if connection._running: 370 | connection._stop_running() 371 | raise AssertionError("connection thread was not stopped") 372 | 373 | async def test_iterdump(self): 374 | async with aiosqlite.connect(":memory:") as db: 375 | await db.execute("create table foo (i integer, k charvar(250))") 376 | await db.executemany( 377 | "insert into foo values (?, ?)", [(1, "hello"), (2, "world")] 378 | ) 379 | 380 | lines = [line async for line in db.iterdump()] 381 | self.assertEqual( 382 | lines, 383 | [ 384 | "BEGIN TRANSACTION;", 385 | "CREATE TABLE foo (i integer, k charvar(250));", 386 | "INSERT INTO \"foo\" VALUES(1,'hello');", 387 | "INSERT INTO \"foo\" VALUES(2,'world');", 388 | "COMMIT;", 389 | ], 390 | ) 391 | 392 | async def test_cursor_on_closed_connection(self): 393 | db = await aiosqlite.connect(self.db) 394 | 395 | cursor = await db.execute("select 1, 2") 396 | await db.close() 397 | with self.assertRaisesRegex(ValueError, "Connection closed"): 398 | await cursor.fetchall() 399 | with self.assertRaisesRegex(ValueError, "Connection closed"): 400 | await cursor.fetchall() 401 | 402 | async def test_cursor_on_closed_connection_loop(self): 403 | db = await aiosqlite.connect(self.db) 404 | 405 | cursor = await db.execute("select 1, 2") 406 | tasks = [] 407 | for i in range(100): 408 | if i == 50: 409 | tasks.append(asyncio.ensure_future(db.close())) 410 | tasks.append(asyncio.ensure_future(cursor.fetchall())) 411 | for task in tasks: 412 | try: 413 | await task 414 | except sqlite3.ProgrammingError: 415 | pass 416 | 417 | async def test_close_twice(self): 418 | db = await aiosqlite.connect(self.db) 419 | 420 | await db.close() 421 | 422 | # no error 423 | await db.close() 424 | 425 | async def test_backup_aiosqlite(self): 426 | def progress(a, b, c): 427 | print(a, b, c) 428 | 429 | async with ( 430 | aiosqlite.connect(":memory:") as db1, 431 | aiosqlite.connect(":memory:") as db2, 432 | ): 433 | await db1.execute("create table foo (i integer, k charvar(250))") 434 | await db1.executemany( 435 | "insert into foo values (?, ?)", [(1, "hello"), (2, "world")] 436 | ) 437 | await db1.commit() 438 | 439 | with self.assertRaisesRegex(OperationalError, "no such table: foo"): 440 | await db2.execute("select * from foo") 441 | 442 | await db1.backup(db2, progress=progress) 443 | 444 | async with db2.execute("select * from foo") as cursor: 445 | rows = await cursor.fetchall() 446 | self.assertEqual(rows, [(1, "hello"), (2, "world")]) 447 | 448 | async def test_backup_sqlite(self): 449 | async with aiosqlite.connect(":memory:") as db1: 450 | with sqlite3.connect(":memory:") as db2: 451 | await db1.execute("create table foo (i integer, k charvar(250))") 452 | await db1.executemany( 453 | "insert into foo values (?, ?)", [(1, "hello"), (2, "world")] 454 | ) 455 | await db1.commit() 456 | 457 | with self.assertRaisesRegex(OperationalError, "no such table: foo"): 458 | db2.execute("select * from foo") 459 | 460 | await db1.backup(db2) 461 | 462 | cursor = db2.execute("select * from foo") 463 | rows = cursor.fetchall() 464 | self.assertEqual(rows, [(1, "hello"), (2, "world")]) 465 | -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | div.omnilib { 2 | margin-top: 24px; 3 | } 4 | 5 | div.omnilib-badges { 6 | margin-top: 12px; 7 | margin-bottom: 10px; 8 | } 9 | 10 | div.omnilib-badges a { 11 | color: #bbb; 12 | text-decoration: none; 13 | font-size: 24px; 14 | border: none; 15 | } 16 | 17 | div.omnilib-badges a:hover { 18 | color: #777; 19 | border: none; 20 | } -------------------------------------------------------------------------------- /docs/_templates/badges.html: -------------------------------------------------------------------------------- 1 |
2 | Star 4 |
5 | 6 | -------------------------------------------------------------------------------- /docs/_templates/omnilib.html: -------------------------------------------------------------------------------- 1 |
2 |

The Omnilib Project

3 | 4 | Omnilib is a group of MIT licensed software libraries developed under a 5 | common, inclusive Code of Conduct. 6 | We are committed to providing a welcoming and open space for all contributors who adhere to these rules. 7 | 8 |
9 |
10 | 11 | 12 | 13 |   14 | 15 | 16 | 17 |   18 | 19 | 20 | 21 | 22 |
23 | 24 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | 2 | API Reference 3 | ============= 4 | 5 | .. module:: aiosqlite 6 | 7 | Connection 8 | ---------- 9 | 10 | .. autofunction:: connect 11 | 12 | .. autoclass:: Connection 13 | :special-members: __aenter__, __aexit__, __await__ 14 | 15 | Cursors 16 | ------- 17 | 18 | .. autoclass:: aiosqlite.cursor.Cursor 19 | :special-members: __aiter__, __anext__, __aenter__, __aexit__ 20 | 21 | Errors 22 | ------ 23 | 24 | .. autoexception:: Warning 25 | :members: 26 | 27 | .. autoexception:: Error 28 | :members: 29 | 30 | .. autoexception:: DatabaseError 31 | :members: 32 | 33 | .. autoexception:: IntegrityError 34 | :members: 35 | 36 | .. autoexception:: ProgrammingError 37 | :members: 38 | 39 | .. autoexception:: OperationalError 40 | :members: 41 | 42 | .. autoexception:: NotSupportedError 43 | :members: 44 | 45 | Advanced 46 | -------- 47 | 48 | .. autofunction:: register_adapter 49 | 50 | .. autofunction:: register_converter 51 | 52 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | .. mdinclude:: ../CHANGELOG.md 5 | :start-line: 2 -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | import datetime 21 | import os 22 | 23 | project = "aiosqlite" 24 | copyright = f"{datetime.date.today().year}, Amethyst Reese" 25 | author = "Amethyst Reese" 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | "sphinx.ext.autodoc", 35 | "sphinx.ext.intersphinx", 36 | "sphinx_mdinclude", 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ["_templates"] 41 | 42 | # List of patterns, relative to source directory, that match files and 43 | # directories to ignore when looking for source files. 44 | # This pattern also affects html_static_path and html_extra_path. 45 | exclude_patterns = [] 46 | 47 | autodoc_default_options = { 48 | "show-inheritance": True, 49 | "members": True, 50 | "undoc-members": True, 51 | } 52 | autodoc_member_order = "groupwise" 53 | autodoc_typehints = "description" 54 | 55 | highlight_language = "text" 56 | intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} 57 | master_doc = "index" 58 | 59 | # Set canonical URL from the Read the Docs Domain 60 | html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "") 61 | 62 | # -- Options for HTML output ------------------------------------------------- 63 | 64 | # The theme to use for HTML and HTML Help pages. See the documentation for 65 | # a list of builtin themes. 66 | # 67 | html_theme = "alabaster" 68 | html_theme_options = { 69 | "description": "Sqlite for Python AsyncIO", 70 | "fixed_sidebar": True, 71 | "badge_branch": "master", 72 | "github_button": False, 73 | "github_user": "omnilib", 74 | "github_repo": "aiosqlite", 75 | "show_powered_by": False, 76 | "sidebar_collapse": False, 77 | "extra_nav_links": { 78 | "Report Issues": "https://github.com/omnilib/aiosqlite/issues", 79 | }, 80 | } 81 | 82 | html_sidebars = { 83 | "**": [ 84 | "about.html", 85 | "badges.html", 86 | "navigation.html", 87 | "relations.html", 88 | "searchbox.html", 89 | "omnilib.html", 90 | ], 91 | } 92 | 93 | # Add any paths that contain custom static files (such as style sheets) here, 94 | # relative to this directory. They are copied after the builtin static files, 95 | # so a file named "default.css" will overwrite the builtin "default.css". 96 | html_static_path = ["_static"] 97 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | .. mdinclude:: ../CONTRIBUTING.md 5 | :start-line: 2 -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. aiosqlite documentation master file, created by 2 | sphinx-quickstart on Sun Apr 26 19:57:46 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 7 | .. include:: ../README.rst 8 | 9 | 10 | .. toctree:: 11 | :hidden: 12 | :maxdepth: 2 13 | 14 | api 15 | 16 | .. toctree:: 17 | :hidden: 18 | :maxdepth: 1 19 | 20 | changelog 21 | contributing 22 | 23 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | PKG:=aiosqlite 2 | EXTRAS:=dev,docs 3 | 4 | UV:=$(shell uv --version) 5 | ifdef UV 6 | VENV:=uv venv 7 | PIP:=uv pip 8 | else 9 | VENV:=python -m venv 10 | PIP:=python -m pip 11 | endif 12 | 13 | install: 14 | $(PIP) install -Ue .[$(EXTRAS)] 15 | 16 | .venv: 17 | $(VENV) .venv 18 | 19 | venv: .venv 20 | source .venv/bin/activate && make install 21 | echo 'run `source .venv/bin/activate` to activate virtualenv' 22 | 23 | test: 24 | python -m coverage run -m $(PKG).tests 25 | python -m coverage report 26 | python -m mypy -p $(PKG) 27 | 28 | lint: 29 | python -m flake8 $(PKG) 30 | python -m ufmt check $(PKG) 31 | 32 | format: 33 | python -m ufmt format $(PKG) 34 | 35 | perf: 36 | python -m unittest -v $(PKG).tests.perf 37 | 38 | .PHONY: html 39 | html: .venv README.rst docs/*.rst docs/conf.py 40 | .venv/bin/sphinx-build -an -b html docs html 41 | 42 | .PHONY: clean 43 | clean: 44 | rm -rf build dist html README MANIFEST *.egg-info 45 | 46 | .PHONY: distclean 47 | distclean: clean 48 | rm -rf .venv 49 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.8,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "aiosqlite" 7 | readme = "README.rst" 8 | license = {file="LICENSE"} 9 | dynamic = ["version", "description"] 10 | authors = [ 11 | {name="Amethyst Reese", email="amethyst@n7.gg"}, 12 | ] 13 | classifiers = [ 14 | "Development Status :: 5 - Production/Stable", 15 | "Framework :: AsyncIO", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: MIT License", 18 | "Topic :: Software Development :: Libraries", 19 | ] 20 | requires-python = ">=3.9" 21 | dependencies = [ 22 | "typing_extensions >= 4.0", 23 | ] 24 | 25 | [project.optional-dependencies] 26 | dev = [ 27 | "attribution==1.8.0", 28 | "black==25.1.0", 29 | "build>=1.2", 30 | "coverage[toml]==7.8.0", 31 | "flake8==7.2.0", 32 | "flake8-bugbear==24.12.12", 33 | "flit==3.12.0", 34 | "mypy==1.15.0", 35 | "ufmt==2.8.0", 36 | "usort==1.0.8.post1", 37 | ] 38 | docs = [ 39 | "sphinx==8.1.3", 40 | "sphinx-mdinclude==0.6.2", 41 | ] 42 | 43 | [project.urls] 44 | Documentation = "https://aiosqlite.omnilib.dev" 45 | Github = "https://github.com/omnilib/aiosqlite" 46 | 47 | [tool.flit.sdist] 48 | exclude = [ 49 | ".github/", 50 | ] 51 | 52 | [tool.attribution] 53 | name = "aiosqlite" 54 | package = "aiosqlite" 55 | ignored_authors = ["dependabot"] 56 | signed_tags = true 57 | version_file = true 58 | 59 | [tool.coverage.run] 60 | branch = true 61 | include = ["aiosqlite/*"] 62 | omit = ["aiosqlite/tests/*"] 63 | 64 | [tool.coverage.report] 65 | fail_under = 75 66 | precision = 1 67 | show_missing = true 68 | skip_covered = true 69 | 70 | [tool.mypy] 71 | ignore_missing_imports = true 72 | python_version = "3.9" 73 | 74 | [[tool.mypy.overrides]] 75 | module = "aiosqlite.tests.perf" 76 | follow_imports = "silent" 77 | --------------------------------------------------------------------------------