├── .github ├── ISSUE_TEMPLATE │ ├── blank-template.md │ └── bug_report.md └── workflows │ ├── python-package.yml │ └── python-publish.yml ├── .gitignore ├── .readthedocs.yml ├── LICENSE ├── README.md ├── cx_Oracle_async ├── AQ.py ├── __init__.py ├── connections.py ├── context.py ├── cursors.py ├── pools.py └── utils.py ├── docs ├── Makefile ├── README.md ├── doc.rwt ├── make.bat ├── requirements.txt ├── serve.bat └── source │ ├── _static │ └── BLANK │ ├── _templates │ └── sidebarlinks.html │ ├── api_manual │ ├── connections.rst │ ├── cursors.rst │ ├── moduleinterface.rst │ └── sessionpool.rst │ ├── conf.py │ ├── images │ └── BLANK │ ├── index.rst │ ├── license.rst │ └── user_guide │ ├── advancedfeatures.rst │ ├── quickstart.rst │ └── sqlexecution.rst ├── misc ├── performance_test_asynchronous.py └── performance_test_synchronous.py ├── requirements.txt ├── setup.py └── tests ├── test_aq.py ├── test_behavior.py ├── test_concurrency.py ├── test_connection.py ├── test_drop.py ├── test_import.py ├── test_ping.py ├── test_rollback.py └── test_stress.py /.github/ISSUE_TEMPLATE/blank-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Blank template 3 | about: Start from a blank template. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Exception traceback** 20 | The full error traceback info shown when exception raised. 21 | 22 | **Platform:** 23 | - OS: [e.g. Ubuntu 18.04] 24 | - Oracle version: [e.g. 19c] 25 | - Python version [e.g. 3.9.1] 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Build 5 | 6 | on: 7 | push: 8 | branches: 9 | - 'main' 10 | - '[0-9].[0-9]+' # matches to backport branches, e.g. 3.6 11 | - dev 12 | pull_request: 13 | branches: 14 | - 'main' 15 | - '[0-9].[0-9]+' 16 | schedule: 17 | - cron: '0 9 1 * *' # Runs at 09:00 UTC on the 1st of every month 18 | 19 | jobs: 20 | build: 21 | 22 | runs-on: ${{ matrix.os }} 23 | strategy: 24 | matrix: 25 | os: [ubuntu-latest] 26 | python-version: ['3.7' , '3.8' , '3.9'] 27 | 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v2 # You need to checkout first, then you can use your repo in following actions. 31 | with: 32 | repository: GoodManWEN/oracle-client-action 33 | - name: Setup Oracledb client 34 | uses: GoodManWEN/oracle-client-action@main 35 | - name: Setup Oracledb 11gR2 server 36 | uses: GoodManWENNumber2/oracle-11g-server-action@v1.0 37 | with: 38 | host port: 1521 39 | oracle version: '1.0.0' 40 | - name: Checkout 41 | uses: actions/checkout@v2 42 | - name: Set up Python ${{ matrix.python-version }} 43 | uses: actions/setup-python@v2 44 | with: 45 | python-version: ${{ matrix.python-version }} 46 | - name: Install dependencies 47 | run: | # sleep to make sure Oracle server is fully loaded 48 | sleep 60 49 | python -m pip install --upgrade pip 50 | python -m pip install flake8 pytest pytest-asyncio async_timeout 51 | pip install -r requirements.txt 52 | - name: Lint with flake8 53 | run: | 54 | # stop the build if there are Python syntax errors or undefined names 55 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 56 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 57 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 58 | - name: Test with pytest 59 | run: | 60 | sleep 30 61 | pytest 62 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Publish 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.8' 21 | - name: Get latest release version number 22 | id: get_version 23 | uses: battila7/get-version-action@v2 24 | - name: Install dependencies 25 | run: | 26 | echo ${{ steps.get_version.outputs.version-without-v }} >> tagname 27 | python -m pip install --upgrade pip 28 | pip install setuptools wheel twine requests beautifulsoup4 lxml 29 | - name: Build and publish 30 | env: 31 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 32 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 33 | run: | 34 | python setup.py sdist bdist_wheel 35 | twine upload dist/* 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Spinx 132 | /docs/build -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Build documentation in the docs/ directory with Sphinx 9 | sphinx: 10 | configuration: docs/source/conf.py 11 | 12 | # Optionally build your docs in additional formats such as PDF 13 | formats: 14 | - pdf 15 | - htmlzip 16 | - epub 17 | 18 | # Optionally set the version of Python and requirements required to build your docs 19 | python: 20 | version: 3.7 21 | install: 22 | - requirements: docs/requirements.txt 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 GoodManWEN 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cx_Oracle_async 2 | [![fury](https://img.shields.io/pypi/v/cx-Oracle-async.svg)](https://pypi.org/project/cx-Oracle-async/) 3 | [![licence](https://img.shields.io/github/license/GoodManWEN/cx_Oracle_async)](https://github.com/GoodManWEN/cx_Oracle_async/blob/master/LICENSE) 4 | [![pyversions](https://img.shields.io/pypi/pyversions/cx-Oracle-async.svg)](https://pypi.org/project/cx-Oracle-async/) 5 | [![Publish](https://github.com/GoodManWEN/cx_Oracle_async/workflows/Publish/badge.svg)](https://github.com/GoodManWEN/cx_Oracle_async/actions?query=workflow:Publish) 6 | [![Build](https://github.com/GoodManWEN/cx_Oracle_async/workflows/Build/badge.svg)](https://github.com/GoodManWEN/cx_Oracle_async/actions?query=workflow:Build) 7 | [![Docs](https://readthedocs.org/projects/cx-oracle-async/badge/?version=latest)](https://readthedocs.org/projects/cx-oracle-async/) 8 | [![Visitors](https://visitor-badge.glitch.me/badge?page_id=goodmanwen.cx_Oracle_async&style=flat-square&color=0088cc)](https://github.com/GoodManWEN/cx_Oracle_async/) 9 | 10 | A very simple asynchronous wrapper that allows you to get access to the Oracle database in asyncio programs. 11 | 12 | Easy to use , buy may not the best practice for efficiency concern. 13 | 14 | ## About development release 15 | In parallel with the main branch, we have worked on a development version based on [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy)'s thread pool management facilities, please refer to [Development notes](https://github.com/GoodManWEN/cx_Oracle_async/blob/dev/misc/Development_notes.md) for details. 16 | 17 | ## Requirements 18 | - [cx_Oracle >= 8.1.0](https://github.com/oracle/python-cx_Oracle) (Take into consideration that author of cx_Oracle said he's trying to implement asyncio support , APIs maybe change in future version. Switch to 8.1.0 if there's something wrong makes it not gonna work.) 19 | - [ThreadPoolExecutorPlus >= 0.2.0](https://github.com/GoodManWEN/ThreadPoolExecutorPlus) 20 | 21 | ## Install 22 | 23 | pip install cx_Oracle_async 24 | 25 | ## Feature 26 | - Nearly all the same as aiomysql in asynchronous operational approach , with limited cx_Oracle feature support. 27 | - No automaticly date format transition built-in. 28 | - AQ feature added , check [docs here](https://cx_oracle_async.readthedocs.io/en/latest/user_guide/advancedfeatures.html#oracle-advanced-queuing-aq) for further information. 29 | - You can modify some of the connection properties simply like you're using cx_Oracle. 30 | - You can do basic insert / select / delete etc. 31 | - If you're connecting to database which is on a different machine from python process , you need to install oracle client module in order to use this library. Check [cx-Oracle's installation guide](https://cx-oracle.readthedocs.io/en/latest/user_guide/installation.html) for further information. 32 | 33 | ## Documentation 34 | 35 | [https://cx_oracle_async.readthedocs.io](https://cx_oracle_async.readthedocs.io) 36 | 37 | ## Performance 38 | query type | asynchronous multithreading | synchronous multithreading | synchronous single thread 39 | -|-|-|- 40 | fast single line query | 6259.80 q/s | 28906.93 q/s | 14805.61 q/s 41 | single line insertion | 1341.88 q/s | 1898 q/s | 1685.17 q/s 42 | 43 | */\* Test platform: \*/*
44 | *AMD Ryzen 3700x*
45 | *Windows 10 LTSC*
46 | *Oracle 19c*
47 | *You can find performance test codes [here](https://github.com/GoodManWEN/cx_Oracle_async/blob/main/misc).* 48 | 49 | ## Examples 50 | Before running examples , make sure you've already installed a [Oracle Client](https://cx-oracle-async.readthedocs.io/en/latest/user_guide/quickstart.html#install-oracle-client) on your machine. 51 | ```Python 52 | # basic_usages.py 53 | import asyncio 54 | import cx_Oracle_async 55 | 56 | async def main(): 57 | oracle_pool = await cx_Oracle_async.create_pool( 58 | host='localhost', 59 | port='1521', 60 | user='user', 61 | password='password', 62 | service_name='orcl', 63 | min = 2, 64 | max = 4, 65 | ) 66 | 67 | async with oracle_pool.acquire() as connection: 68 | async with connection.cursor() as cursor: 69 | await cursor.execute("SELECT * FROM V$SESSION") 70 | print(await cursor.fetchall()) 71 | 72 | await oracle_pool.close() 73 | 74 | if __name__ == '__main__': 75 | asyncio.run(main()) 76 | ``` 77 | -------------------------------------------------------------------------------- /cx_Oracle_async/AQ.py: -------------------------------------------------------------------------------- 1 | from cx_Oracle import MessageProperties , DEQ_NO_WAIT 2 | from ThreadPoolExecutorPlus import ThreadPoolExecutor 3 | from asyncio import Lock as aioLock 4 | from collections.abc import Iterable 5 | from typing import Union , TYPE_CHECKING 6 | from collections import deque 7 | if TYPE_CHECKING: 8 | from .connections import AsyncConnectionWrapper 9 | from cx_Oracle import Queue 10 | from asyncio.windows_events import ProactorEventLoop 11 | 12 | 13 | class AsyncQueueWrapper: 14 | 15 | def __init__(self , queue: 'Queue', loop: 'ProactorEventLoop' , thread_pool: ThreadPoolExecutor , conn : 'AsyncConnectionWrapper'): 16 | self._queue = queue 17 | self._loop = loop 18 | self._thread_pool = thread_pool 19 | self._conn = conn 20 | self._deqlock = aioLock() 21 | 22 | async def enqOne(self , *args , **kwargs): 23 | return await self._loop.run_in_executor(self._thread_pool , self._queue.enqOne , *args , **kwargs) 24 | 25 | async def enqMany(self , *args , **kwargs): 26 | return await self._loop.run_in_executor(self._thread_pool , self._queue.enqMany , *args , **kwargs) 27 | 28 | async def deqOne(self , *args , **kwargs): 29 | async with self._deqlock: 30 | return await self._loop.run_in_executor(self._thread_pool , self._queue.deqOne , *args , **kwargs) 31 | 32 | def deqMany(self , maxMessages: int = -1): 33 | return DeqManyWrapper(self._loop , self._thread_pool , self._queue , self._deqlock , maxMessages) 34 | 35 | def _decode(self , _object: MessageProperties): 36 | return _object.payload.decode(self._conn.encoding) 37 | 38 | def unpack(self , _object: Union[MessageProperties , list]): 39 | if isinstance(_object , Iterable): 40 | return list(map(self._decode , _object)) 41 | else: 42 | return self._decode(_object) 43 | 44 | @property 45 | def pack(self): 46 | return self._conn.msgproperties 47 | 48 | @property 49 | def enqOptions(self): 50 | return self._queue.enqOptions 51 | 52 | @property 53 | def deqOptions(self): 54 | return self._queue.deqOptions 55 | 56 | 57 | class DeqManyWrapper: 58 | 59 | def __init__(self , loop : 'ProactorEventLoop' , thread_pool : ThreadPoolExecutor, queue : 'Queue' , deqlock: aioLock , maxMessages : int): 60 | self._loop = loop 61 | self._thread_pool = thread_pool 62 | self._queue = queue 63 | self._count = 0 64 | self._max_messages = maxMessages 65 | self._deqlock = deqlock 66 | self._buffer = deque() 67 | self._soft_max = ((1 << 16) - 1) 68 | self._max_limit = maxMessages if maxMessages > -1 else self._soft_max 69 | self._max_limit = self._max_limit if self._max_limit <= self._soft_max else self._soft_max 70 | self._deqcount = 0 71 | self._closed = False 72 | 73 | @property 74 | def _fetch_num(self): 75 | return self._soft_max if self._max_messages < 0 else min(self._max_limit , self._max_messages - self._deqcount) 76 | 77 | def __await__(self): 78 | if self._closed: 79 | raise RuntimeError('Current query has closed , you cannot activate it twice.') 80 | yield from self._deqlock.acquire().__await__() 81 | try: 82 | ret = yield from self._loop.run_in_executor(self._thread_pool , self._queue.deqMany , self._fetch_num).__await__() 83 | except Exception as exc: 84 | raise exc 85 | finally: 86 | self._deqlock.release() 87 | self._closed = True 88 | return ret 89 | 90 | def __aiter__(self): 91 | return self 92 | 93 | async def __anext__(self): 94 | if self._closed: 95 | raise RuntimeError('Current query has closed , you cannot activate it twice.') 96 | 97 | if self._max_messages == 0: 98 | self._closed = True 99 | raise StopAsyncIteration 100 | 101 | # Fetch off 102 | if self._max_messages > 0 and self._deqcount >= self._max_messages: 103 | if self._buffer: 104 | return self._buffer.popleft() 105 | self._closed = True 106 | raise StopAsyncIteration 107 | 108 | # Fetch on 109 | if self._buffer: 110 | return self._buffer.popleft() 111 | _tmp = self._queue.deqOptions.wait 112 | async with self._deqlock: 113 | try: 114 | self._queue.deqOptions.wait = DEQ_NO_WAIT 115 | data = await self._loop.run_in_executor(self._thread_pool , self._queue.deqMany , self._fetch_num) 116 | except Exception as exc: 117 | raise exc 118 | finally: 119 | self._queue.deqOptions.wait = _tmp 120 | 121 | if data: 122 | self._buffer.extend(data) 123 | self._deqcount += len(data) 124 | return self._buffer.popleft() 125 | 126 | # No data return , close iteration. 127 | self._closed = True 128 | raise StopAsyncIteration -------------------------------------------------------------------------------- /cx_Oracle_async/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '' 2 | 3 | from .utils import create_pool , makedsn , DEQ_NO_WAIT , DEQ_WAIT_FOREVER 4 | 5 | __all__ = ( 6 | 'create_pool', 7 | 'makedsn', 8 | 'DEQ_NO_WAIT', 9 | 'DEQ_WAIT_FOREVER' 10 | ) 11 | -------------------------------------------------------------------------------- /cx_Oracle_async/connections.py: -------------------------------------------------------------------------------- 1 | from .context import AbstractContextManager as BaseManager 2 | from .cursors import AsyncCursorWrapper , AsyncCursorWrapper_context 3 | from .AQ import AsyncQueueWrapper 4 | from cx_Oracle import Connection , SessionPool 5 | from ThreadPoolExecutorPlus import ThreadPoolExecutor 6 | from typing import TYPE_CHECKING 7 | if TYPE_CHECKING: 8 | from asyncio.windows_events import ProactorEventLoop 9 | from .pools import AsyncPoolWrapper 10 | 11 | class AsyncConnectionWrapper_context(BaseManager): 12 | 13 | def __init__(self , coro): 14 | super().__init__(coro) 15 | 16 | async def __aexit__(self, exc_type, exc, tb): 17 | await self._obj.release() 18 | self._obj = None 19 | 20 | 21 | class AsyncConnectionWrapper: 22 | 23 | def __init__(self , conn: Connection, loop: 'ProactorEventLoop', thread_pool: ThreadPoolExecutor, pool: SessionPool, pool_wrapper:'AsyncPoolWrapper'): 24 | self._conn = conn 25 | self._loop = loop 26 | self._pool = pool 27 | self._pool_wrapper = pool_wrapper 28 | self._thread_pool = thread_pool 29 | 30 | 31 | def cursor(self): 32 | coro = self._loop.run_in_executor(self._thread_pool , self._cursor) 33 | return AsyncCursorWrapper_context(coro) 34 | 35 | def _cursor(self): 36 | return AsyncCursorWrapper(self._conn.cursor() , self._loop , self._thread_pool) 37 | 38 | def msgproperties(self , *args , **kwargs): 39 | return self._conn.msgproperties(*args , **kwargs) 40 | 41 | @property 42 | def encoding(self): 43 | return self._conn.encoding 44 | 45 | @property 46 | def dsn(self): 47 | return self._conn.dsn 48 | 49 | @property 50 | def module(self): 51 | return self._conn.module 52 | 53 | @module.setter 54 | def module(self , arg): 55 | self._conn.module = arg 56 | 57 | @property 58 | def action(self): 59 | return self._conn.action 60 | 61 | @action.setter 62 | def action(self , arg): 63 | self._conn.action = arg 64 | 65 | @property 66 | def client_identifier(self): 67 | return self._conn.client_identifier 68 | 69 | @client_identifier.setter 70 | def client_identifier(self , arg): 71 | self._conn.client_identifier = arg 72 | 73 | @property 74 | def clientinfo(self): 75 | return self._conn.clientinfo 76 | 77 | @clientinfo.setter 78 | def clientinfo(self , arg): 79 | self._conn.clientinfo = arg 80 | 81 | async def queue(self , *args , **kwargs): 82 | return AsyncQueueWrapper(self._conn.queue(*args , **kwargs) , self._loop , self._thread_pool , self) 83 | 84 | async def gettype(self , *args , **kwargs): 85 | ''' 86 | Uses the original cx_Oracle object without wrapper 87 | ''' 88 | return await self._loop.run_in_executor(self._thread_pool , self._conn.gettype , *args , **kwargs) 89 | 90 | async def commit(self): 91 | return await self._loop.run_in_executor(self._thread_pool , self._conn.commit) 92 | 93 | async def release(self): 94 | self._pool_wrapper._ofree(self) 95 | return await self._loop.run_in_executor(self._thread_pool , self._pool.release , self._conn) 96 | 97 | async def cancel(self): 98 | return await self._loop.run_in_executor(self._thread_pool , self._conn.cancel) 99 | 100 | async def ping(self): 101 | return await self._loop.run_in_executor(self._thread_pool , self._conn.ping) 102 | 103 | async def rollback(self): 104 | return await self._loop.run_in_executor(self._thread_pool , self._conn.rollback) -------------------------------------------------------------------------------- /cx_Oracle_async/context.py: -------------------------------------------------------------------------------- 1 | from types import CoroutineType 2 | 3 | class AbstractContextManager: 4 | 5 | def __init__(self , coro : CoroutineType): 6 | self._coro = coro 7 | self._obj = None 8 | 9 | # def __next__(self): 10 | # return self.send(None) 11 | 12 | def __iter__(self): 13 | return self._coro.__await__() 14 | 15 | def __await__(self): 16 | return self._coro.__await__() 17 | 18 | async def __aenter__(self): 19 | self._obj = await self._coro 20 | return self._obj 21 | 22 | async def __aexit__(self, exc_type, exc, tb): 23 | ... -------------------------------------------------------------------------------- /cx_Oracle_async/cursors.py: -------------------------------------------------------------------------------- 1 | from .context import AbstractContextManager as BaseManager 2 | from ThreadPoolExecutorPlus import ThreadPoolExecutor 3 | from types import CoroutineType 4 | from cx_Oracle import Cursor 5 | from typing import TYPE_CHECKING 6 | if TYPE_CHECKING: 7 | from asyncio.windows_events import ProactorEventLoop 8 | 9 | 10 | class AsyncCursorWrapper_context(BaseManager): 11 | 12 | def __init__(self , coro : CoroutineType): 13 | super().__init__(coro) 14 | 15 | 16 | class AsyncCursorWrapper: 17 | 18 | def __init__(self , cursor : Cursor, loop : 'ProactorEventLoop' , thread_pool : ThreadPoolExecutor): 19 | self._cursor = cursor 20 | self._loop = loop 21 | self._thread_pool = thread_pool 22 | 23 | async def execute(self , sql , *args , **kwargs): 24 | if kwargs: 25 | return await self._loop.run_in_executor( 26 | self._thread_pool , 27 | lambda : self._cursor.execute(sql , *args , **kwargs) 28 | ) 29 | return await self._loop.run_in_executor(self._thread_pool , self._cursor.execute , sql , *args , **kwargs) 30 | 31 | async def executemany(self , sql , *args , **kwargs): 32 | return await self._loop.run_in_executor(self._thread_pool , self._cursor.executemany , sql , *args , **kwargs) 33 | 34 | async def fetchone(self): 35 | return await self._loop.run_in_executor(self._thread_pool , self._cursor.fetchone) 36 | 37 | async def fetchall(self): 38 | # block mainly happens when fetch triggered. 39 | return await self._loop.run_in_executor(self._thread_pool , self._cursor.fetchall) 40 | 41 | async def var(self, args): 42 | return await self._loop.run_in_executor(self._thread_pool , self._cursor.var, args) 43 | 44 | async def callproc(self, *args , **kwargs): 45 | return await self._loop.run_in_executor(self._thread_pool , self._cursor.callproc, *args , **kwargs) 46 | -------------------------------------------------------------------------------- /cx_Oracle_async/pools.py: -------------------------------------------------------------------------------- 1 | from .context import AbstractContextManager as BaseManager 2 | from .connections import AsyncConnectionWrapper , AsyncConnectionWrapper_context 3 | from ThreadPoolExecutorPlus import ThreadPoolExecutor 4 | from cx_Oracle import Connection , SessionPool 5 | from weakref import WeakSet 6 | from types import CoroutineType 7 | import asyncio 8 | import platform 9 | import os 10 | from typing import TYPE_CHECKING 11 | if TYPE_CHECKING: 12 | from asyncio.windows_events import ProactorEventLoop 13 | 14 | pltfm = platform.system() 15 | if pltfm == 'Windows': 16 | DEFAULT_MAXIMUM_WORKER_NUM = (os.cpu_count() or 1) * 16 17 | DEFAULT_MAXIMUM_WORKER_TIMES = 2 18 | elif pltfm == 'Linux' or pltfm == 'Darwin': 19 | DEFAULT_MAXIMUM_WORKER_NUM = (os.cpu_count() or 1) * 32 20 | DEFAULT_MAXIMUM_WORKER_TIMES = 3 21 | 22 | 23 | class AsyncPoolWrapper_context(BaseManager): 24 | 25 | def __init__(self , coro : CoroutineType): 26 | super().__init__(coro) 27 | 28 | async def __aexit__(self, exc_type, exc, tb): 29 | await self._obj.close(force = True) 30 | self._obj = None 31 | 32 | 33 | class AsyncPoolWrapper: 34 | 35 | def __init__(self , pool : SessionPool, loop : 'ProactorEventLoop' = None): 36 | if loop == None: 37 | loop = asyncio.get_running_loop() 38 | self._thread_pool = ThreadPoolExecutor(max_workers = max(DEFAULT_MAXIMUM_WORKER_NUM , pool.max << DEFAULT_MAXIMUM_WORKER_TIMES)) 39 | self._thread_pool.set_daemon_opts(min_workers = max(4 , pool.min << 1)) 40 | self._loop = loop 41 | self._pool = pool 42 | self._occupied = WeakSet() 43 | 44 | def acquire(self): 45 | coro = self._loop.run_in_executor(self._thread_pool , self._acquire) 46 | return AsyncConnectionWrapper_context(coro) 47 | 48 | def _acquire(self): 49 | wrapper = AsyncConnectionWrapper(self._pool.acquire() , self._loop , self._thread_pool , self._pool , self) 50 | self._occupied.add(wrapper) 51 | return wrapper 52 | 53 | def _ofree(self , obj: AsyncConnectionWrapper): 54 | ''' 55 | A performance optimization tip: 56 | 57 | When there's no exception raised , `try` way perform 58 | 20%-30% faster than `if` way. 59 | 60 | If there do have a exception , `try` way will be 61 | 100% slower than `if` way , it takes about 500ns 62 | to recover the stack. 63 | 64 | So in this perticular situation when there's far more 65 | chance no exception raised rather than exception raised, 66 | use `try` provides better performance. 67 | ''' 68 | try: 69 | self._occupied.remove(obj) 70 | except: 71 | pass 72 | # if obj in self._occupied: 73 | # self._occupied.remove(obj) 74 | 75 | async def release(self , conn: AsyncConnectionWrapper): 76 | self._ofree(conn) 77 | return await self._loop.run_in_executor(self._thread_pool , self._pool.release , conn._conn) 78 | 79 | async def drop(self , conn: Connection): 80 | return await self._loop.run_in_executor(self._thread_pool , self._pool.drop , conn) 81 | 82 | async def close(self , force: bool = False , interrupt: bool = False): 83 | ''' 84 | WARNING: option `interrupt` will force cancel all running connections before close 85 | the pool. This may cause fetching thread no response forever in some legacy version 86 | of oracle database such as 11 or lower. 87 | 88 | Do make sure this option works fine with your working enviornment. 89 | ''' 90 | while self._occupied: 91 | wrapper = self._occupied.pop() 92 | if interrupt: 93 | await self._loop.run_in_executor(self._thread_pool , wrapper._conn.cancel) 94 | 95 | return await self._loop.run_in_executor(self._thread_pool , self._pool.close , force) -------------------------------------------------------------------------------- /cx_Oracle_async/utils.py: -------------------------------------------------------------------------------- 1 | from .pools import AsyncPoolWrapper , AsyncPoolWrapper_context 2 | from ThreadPoolExecutorPlus import ThreadPoolExecutor 3 | import cx_Oracle as cxor 4 | import asyncio 5 | from typing import TYPE_CHECKING 6 | if TYPE_CHECKING: 7 | from asyncio.windows_events import ProactorEventLoop 8 | 9 | 10 | makedsn = cxor.makedsn 11 | DEQ_NO_WAIT = cxor.DEQ_NO_WAIT 12 | DEQ_WAIT_FOREVER = cxor.DEQ_WAIT_FOREVER 13 | 14 | async def _create_pool( 15 | host: str =None, 16 | port: str =None, 17 | service_name: str =None, 18 | sid: str =None, 19 | loop: 'ProactorEventLoop' =None, 20 | dsn: str =None, 21 | **kwargs 22 | ): 23 | if loop == None: 24 | loop = asyncio.get_running_loop() 25 | 26 | if dsn == None: 27 | if service_name != None: 28 | dsn = makedsn(host = host, port = port, sid = sid , service_name = service_name) 29 | else: 30 | dsn = makedsn(host = host, port = port, sid = sid) 31 | pool = cxor.SessionPool(dsn=dsn, **kwargs) 32 | pool = AsyncPoolWrapper(pool) 33 | return pool 34 | 35 | def create_pool( 36 | user: str =None, 37 | password: str =None, 38 | dsn: str =None, 39 | min: int =2, 40 | max: int =4, 41 | increment=1, 42 | connectiontype=cxor.Connection, 43 | threaded=True, 44 | getmode=cxor.SPOOL_ATTRVAL_WAIT, 45 | events=False, 46 | homogeneous=True, 47 | externalauth=False, 48 | encoding='UTF-8', 49 | edition=None, 50 | timeout=0, 51 | waitTimeout=0, 52 | maxLifetimeSession=0, 53 | sessionCallback=None, 54 | maxSessionsPerShard=0, 55 | host: str =None, 56 | port: str =None, 57 | service_name: str =None, 58 | sid: str =None, 59 | loop: 'ProactorEventLoop' =None, 60 | ): 61 | coro = _create_pool( 62 | user=user, 63 | password=password, 64 | dsn=dsn, 65 | min=min, 66 | max=max, 67 | increment=increment, 68 | connectiontype=connectiontype, 69 | threaded=threaded, 70 | getmode=getmode, 71 | events=events, 72 | homogeneous=homogeneous, 73 | externalauth=externalauth, 74 | encoding=encoding, 75 | edition=edition, 76 | timeout=timeout, 77 | waitTimeout=waitTimeout, 78 | maxLifetimeSession=maxLifetimeSession, 79 | sessionCallback=sessionCallback, 80 | maxSessionsPerShard=maxSessionsPerShard, 81 | host=host, 82 | port=port, 83 | service_name=service_name, 84 | sid=sid, 85 | loop=loop, 86 | ) 87 | return AsyncPoolWrapper_context(coro) -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | The generated cx_Oracle_async documentation is at https://cx_oracle_async.readthedocs.io/ 2 | 3 | This directory contains the documentation source. It is written using reST 4 | (re-Structured Text) format source files which are processed using Sphinx and 5 | turned into HTML, PDF or ePub documents. If you wish to build these yourself, 6 | you need to install Sphinx. Sphinx is available on many Linux distributions as a 7 | pre-built package. You can also install Sphinx on all platforms using the Python 8 | package manager "pip". For more information on Sphinx, please visit this page: 9 | 10 | http://www.sphinx-doc.org 11 | 12 | Once Sphinx is installed, the supplied Makefile can be used to build the 13 | different targets. 14 | -------------------------------------------------------------------------------- /docs/doc.rwt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodManWEN/cx_Oracle_async/3d2f88a758ad49d1f05bc7fcbcc7ab9b74ae15b2/docs/doc.rwt -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx 2 | sphinx-autobuild 3 | recommonmark 4 | sphinx_nameko_theme 5 | sphinx_markdown_tables 6 | cx_Oracle_async 7 | -------------------------------------------------------------------------------- /docs/serve.bat: -------------------------------------------------------------------------------- 1 | sphinx-autobuild source build/html -------------------------------------------------------------------------------- /docs/source/_static/BLANK: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodManWEN/cx_Oracle_async/3d2f88a758ad49d1f05bc7fcbcc7ab9b74ae15b2/docs/source/_static/BLANK -------------------------------------------------------------------------------- /docs/source/_templates/sidebarlinks.html: -------------------------------------------------------------------------------- 1 |

Useful Links

2 | -------------------------------------------------------------------------------- /docs/source/api_manual/connections.rst: -------------------------------------------------------------------------------- 1 | .. _connobj: 2 | 3 | ***************************** 4 | AsyncConnectionWrapper Object 5 | ***************************** 6 | 7 | Wraps a cx_Oracle.Connection object. 8 | 9 | .. method:: AsyncConnectionWrapper.cursor() 10 | 11 | Return a new :ref:`async cursor wrapper object ` using the connection. 12 | 13 | This is a asynchronous method. 14 | 15 | .. method:: AsyncConnectionWrapper.msgproperties(payload, correlation, delay, exceptionq, \ 16 | expiration, priority) 17 | 18 | Returns an object specifying the properties of messages used in advanced 19 | queuing. 20 | 21 | This is a synchronous method. 22 | 23 | .. versionadded:: 0.2.0 24 | 25 | .. note:: 26 | 27 | This method is an extension to the cx_Oracle's DB API definition. 28 | 29 | .. method:: AsyncConnectionWrapper.queue(name, payloadType=None) 30 | 31 | Creates a queue which is used to enqueue and dequeue 32 | messages in Advanced Queueing. 33 | 34 | The name parameter is expected to be a string identifying the queue in 35 | which messages are to be enqueued or dequeued. 36 | 37 | This is a asynchronous method. 38 | 39 | .. versionadded:: 0.2.0 40 | 41 | .. note:: 42 | 43 | This method is an extension to the cx_Oracle's DB API definition. 44 | 45 | .. method:: AsyncConnectionWrapper.gettype(name) 46 | 47 | Return a ``cx_Oracle.type object`` given its name. This can then be 48 | used to create objects which can be bound to cursors created by this 49 | connection. 50 | 51 | This is a asynchronous method. 52 | 53 | .. versionadded:: 0.2.0 54 | 55 | .. note:: 56 | 57 | This method is an extension to the cx_Oracle's DB API definition. 58 | 59 | .. method:: AsyncConnectionWrapper.commit() 60 | 61 | Commit any pending transactions to the database. 62 | 63 | This is a asynchronous method. 64 | 65 | .. method:: AsyncConnectionWrapper.release() 66 | 67 | Equals to cx_Oracle.SessionPool.release(connection) , by using this equivalent 68 | you don't need to operate with another ``AsyncPoolWrapper`` object. 69 | 70 | This is a asynchronous method. 71 | 72 | .. method:: AsyncConnectionWrapper.cancel() 73 | 74 | Break a long-running transaction. 75 | 76 | This is a asynchronous method. 77 | 78 | .. note:: 79 | 80 | This method is an extension to the cx_Oracle's DB API definition. 81 | 82 | .. method:: AsyncConnectionWrapper.rollback() 83 | 84 | Rollback any pending transactions. 85 | 86 | This is a asynchronous method. 87 | 88 | .. method:: AsyncConnectionWrapper.ping() 89 | 90 | Ping the server which can be used to test if the connection is still 91 | active. 92 | 93 | .. note:: 94 | 95 | This method is an extension to the cx_Oracle's DB API definition. 96 | 97 | .. attribute:: AsyncConnectionWrapper.encoding 98 | 99 | This read-only attribute returns the IANA character set name of the 100 | character set in use by the Oracle client for regular strings. 101 | 102 | .. note:: 103 | 104 | This attribute is an extension to the cx_Oracle's DB API definition. 105 | 106 | .. attribute:: AsyncConnectionWrapper.dsn 107 | 108 | This read-only attribute returns the TNS entry of the database to which a 109 | connection has been established. 110 | 111 | .. attribute:: AsyncConnectionWrapper.module 112 | 113 | This write-only attribute sets the module column in the v$session table. 114 | The maximum length for this string is 48 and if you exceed this length you 115 | will get ORA-24960. 116 | 117 | .. note: 118 | 119 | This attribute is an extension to the cx_Oracle's DB API definition. 120 | 121 | .. attribute:: AsyncConnectionWrapper.action 122 | 123 | This write-only attribute sets the action column in the v$session table. It 124 | is a string attribute and cannot be set to None -- use the empty string 125 | instead. 126 | 127 | .. note:: 128 | 129 | This attribute is an extension to the cx_Oracle's DB API definition. 130 | 131 | .. attribute:: AsyncConnectionWrapper.client_identifier 132 | 133 | This write-only attribute sets the client_identifier column in the 134 | v$session table. 135 | 136 | .. note:: 137 | 138 | This attribute is an extension to the cx_Oracle's DB API definition. 139 | 140 | .. attribute:: AsyncConnectionWrapper.clientinfo 141 | 142 | This write-only attribute sets the client_info column in the v$session 143 | table. 144 | 145 | .. note:: 146 | 147 | This attribute is an extension to the cx_Oracle's DB API definition. -------------------------------------------------------------------------------- /docs/source/api_manual/cursors.rst: -------------------------------------------------------------------------------- 1 | .. _cursorobj: 2 | 3 | ************************* 4 | AsyncCursorWrapper Object 5 | ************************* 6 | 7 | .. method:: AsyncCursorWrapper.execute(statement, [parameters], \*\*keywordParameters) 8 | 9 | Execute a statement against the database. 10 | 11 | Parameters may be passed as a dictionary or sequence or as keyword 12 | parameters. If the parameters are a dictionary, the values will be bound by 13 | name and if the parameters are a sequence the values will be bound by 14 | position. Note that if the values are bound by position, the order of the 15 | variables is from left to right as they are encountered in the statement 16 | and SQL statements are processed differently than PL/SQL statements. For 17 | this reason, it is generally recommended to bind parameters by name instead 18 | of by position. 19 | 20 | Parameters passed as a dictionary are name and value pairs. The name maps 21 | to the bind variable name used by the statement and the value maps to the 22 | Python value you wish bound to that bind variable. 23 | 24 | A reference to the statement will be retained by the cursor. If None or the 25 | same string object is passed in again, the cursor will execute that 26 | statement again without performing a prepare or rebinding and redefining. 27 | This is most effective for algorithms where the same statement is used, but 28 | different parameters are bound to it (many times). Note that parameters 29 | that are not passed in during subsequent executions will retain the value 30 | passed in during the last execution that contained them. 31 | 32 | If the statement is a query, the cursor is returned as a convenience to the 33 | caller (so it can be used directly as an iterator over the rows in the 34 | cursor); otherwise, ``None`` is returned. 35 | 36 | This is a asynchronous method. 37 | 38 | .. method:: AsyncCursorWrapper.executemany(statement, parameters, batcherrors=False, \ 39 | arraydmlrowcounts=False) 40 | 41 | Prepare a statement for execution against a database and then execute it 42 | against all parameter mappings or sequences found in the sequence 43 | parameters. 44 | 45 | The statement is managed in the same way as the :meth:`~AsyncCursorWrapper.execute()` 46 | method manages it. If the size of the buffers allocated for any of the 47 | parameters exceeds 2 GB, you will receive the error "DPI-1015: array size 48 | of is too large", where varies with the size of each element being 49 | allocated in the buffer. If you receive this error, decrease the number of 50 | elements in the sequence parameters. 51 | 52 | This is a asynchronous method. 53 | 54 | .. method:: AsyncCursorWrapper.fetchone() 55 | 56 | Fetch the next row of a query result set, returning a single tuple or None 57 | when no more data is available. 58 | 59 | An exception is raised if the previous call to :meth:`~Cursor.execute()` 60 | did not produce any result set or no call was issued yet. 61 | 62 | See :ref:`sqlexecution` for further example. 63 | 64 | This is a asynchronous method. 65 | 66 | .. method:: AsyncCursorWrapper.fetchall() 67 | 68 | Fetch all (remaining) rows of a query result, returning them as a list of 69 | tuples. An empty list is returned if no more rows are available. Note that 70 | the cursor's arraysize attribute can affect the performance of this 71 | operation, as internally reads from the database are done in batches 72 | corresponding to the arraysize. 73 | 74 | An exception is raised if the previous call to :meth:`~Cursor.execute()` 75 | did not produce any result set or no call was issued yet. 76 | 77 | See :ref:`sqlexecution` for further example. 78 | 79 | This is a asynchronous method. 80 | 81 | .. method:: AsyncCursorWrapper.var(dataType, [size, arraysize, inconverter, outconverter, \ 82 | typename, encodingErrors]) 83 | 84 | Create a variable with the specified characteristics. This method was 85 | designed for use with PL/SQL in/out variables where the length or type 86 | cannot be determined automatically from the Python object passed in or for 87 | use in input and output type handlers defined on cursors or connections. 88 | 89 | Only accept positional argument. 90 | 91 | This is a asynchronous method. 92 | 93 | .. method:: AsyncCursorWrapper.close() 94 | 95 | Close the cursor now, rather than whenever __del__ is called. The cursor 96 | will be unusable from this point forward; an Error exception will be raised 97 | if any operation is attempted with the cursor. -------------------------------------------------------------------------------- /docs/source/api_manual/moduleinterface.rst: -------------------------------------------------------------------------------- 1 | .. module:: cx_Oracle_async 2 | 3 | .. _moduleinterface: 4 | 5 | **************** 6 | Module Interface 7 | **************** 8 | 9 | .. data:: DEQ_NO_WAIT 10 | 11 | This constant is used to specify that dequeue not wait for messages to be available for dequeuing. 12 | 13 | .. versionadded:: 0.2.0 14 | 15 | .. data:: DEQ_WAIT_FOREVER 16 | 17 | This constant is used to specify that dequeue should wait forever for messages to be available for dequeuing. This is the default value. 18 | 19 | .. versionadded:: 0.2.0 20 | 21 | .. function:: makedsn(host, port, sid=None, service_name=None) 22 | 23 | Return a string suitable for use as the dsn parameter for 24 | :meth:`~cx_Oracle_async.create_pool()`. This string is identical to the strings that 25 | are defined by the Oracle names server or defined in the tnsnames.ora file. 26 | 27 | .. note:: 28 | 29 | This method is an extension to the cx_Oracle's DB API definition. 30 | 31 | .. function:: create_pool(user=None, password=None, dsn=None, min=2, max=4, \ 32 | increment=1, connectiontype=cx_Oracle.Connection, threaded=True, \ 33 | getmode=cx_Oracle.SPOOL_ATTRVAL_NOWAIT, events=False, \ 34 | homogeneous=True, externalauth=False, encoding='UTF-8', \ 35 | edition=None, timeout=0, waitTimeout=0, maxLifetimeSession=0, \ 36 | sessionCallback=None, maxSessionsPerShard=0) 37 | 38 | Create and return a :ref:`AsyncPoolWrapper object ` which wraps a cx_Oracle.SessionPool. 39 | 40 | Different from the original library , ``threaded`` is set to ``True`` by default , and encoding is set to ``UTF-8``. 41 | 42 | .. note:: 43 | 44 | This method is an extension to cx_Oracle's DB API definition. 45 | 46 | -------------------------------------------------------------------------------- /docs/source/api_manual/sessionpool.rst: -------------------------------------------------------------------------------- 1 | .. _sessionpool: 2 | 3 | *********************** 4 | AsyncPoolWrapper Object 5 | *********************** 6 | 7 | Wraps a cx_Oracle.SessionPool object. 8 | 9 | .. note:: 10 | 11 | This object is an extension to cx_Oracle's DB API. 12 | 13 | .. method:: AsyncPoolWrapper.acquire() 14 | 15 | Acquire a connection from the session pool and return a 16 | :ref:`async connection wrapper object `. 17 | 18 | .. method:: AsyncPoolWrapper.drop(AsyncConnectionWrapper) 19 | 20 | Drop the connection from the pool which is useful if the connection is no 21 | longer usable (such as when the session is killed). 22 | 23 | 24 | .. method:: AsyncPoolWrapper.release(AsyncConnectionWrapper) 25 | 26 | Release the connection back to the pool now, rather than whenever __del__ 27 | is called. The connection will be unusable from this point forward; an 28 | Error exception will be raised if any operation is attempted with the 29 | connection. Any cursors or LOBs created by the connection will also be 30 | marked unusable and an Error exception will be raised if any operation is 31 | attempted with them. 32 | 33 | .. method:: AsyncPoolWrapper.close(force=False , interrupt=False) 34 | 35 | If any connections have been acquired and not released back to the pool 36 | this method will fail unless the force parameter is set to True. 37 | 38 | If interrupt is set to true , the session pool will close now, 39 | rather than when the last reference to it is released , 40 | Which makes it unusable for further work. 41 | 42 | The interrupt feature is not supported in legacy oracle versions. 43 | -------------------------------------------------------------------------------- /docs/source/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 | # import sphinx_rtd_theme 17 | import pkg_resources 18 | import sphinx_nameko_theme 19 | import datetime 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'cx_Oracle_async' 23 | copyright = '{}, WEN'.format(datetime.date.today().year) 24 | author = 'WEN' 25 | 26 | # The version info for the project you're documenting, acts as replacement for 27 | # |version| and |release|, also used in various other places throughout the 28 | # built documents. 29 | # 30 | # The short X.Y version. 31 | try: 32 | version = pkg_resources.get_distribution('cx_Oracle_async').version 33 | # The full version, including alpha/beta/rc tags. 34 | release = version 35 | except: 36 | version = '0.0.1' 37 | release = '0.0.1' 38 | 39 | # -- General configuration --------------------------------------------------- 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ['_templates'] 43 | 44 | # The language for content autogenerated by Sphinx. Refer to documentation 45 | # for a list of supported languages. 46 | # 47 | # This is also used if you do content translation via gettext catalogs. 48 | # Usually you set "language" from the command line for these cases. 49 | language = 'en_US' 50 | 51 | # Add any Sphinx extension module names here, as strings. They can be 52 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 53 | # ones. 54 | extensions = [ 55 | 'recommonmark', 56 | 'sphinx_markdown_tables', 57 | # "sphinx_rtd_theme", 58 | ] 59 | 60 | # List of patterns, relative to source directory, that match files and 61 | # directories to ignore when looking for source files. 62 | # This pattern also affects html_static_path and html_extra_path. 63 | exclude_patterns = [] 64 | 65 | # -- Options for HTML output ------------------------------------------------- 66 | 67 | # The theme to use for HTML and HTML Help pages. See the documentation for 68 | # a list of builtin themes. 69 | # 70 | # html_theme = 'sphinx_rtd_theme' 71 | html_theme = 'nameko' 72 | html_theme_path = [sphinx_nameko_theme.get_html_theme_path()] 73 | html_sidebars = { 74 | '**': ['localtoc.html', 'relations.html', 'sourcelink.html', 75 | 'sidebarlinks.html', 'searchbox.html'] 76 | } 77 | 78 | # 将代码框高亮调整为绿色背景风格 79 | # 在sphinx_rtd_theme中生效,nameko中无效 80 | pygments_style = 'sphinx' 81 | 82 | # Add any paths that contain custom static files (such as style sheets) here, 83 | # relative to this directory. They are copied after the builtin static files, 84 | # so a file named "default.css" will overwrite the builtin "default.css". 85 | html_static_path = ['_static'] 86 | 87 | source_suffix = ['.rst', '.md'] 88 | today_fmt = '%B %d, %Y' 89 | html_last_updated_fmt = '%b %d, %Y' 90 | htmlhelp_basename = 'cx_Oracle_async' 91 | numfig = True -------------------------------------------------------------------------------- /docs/source/images/BLANK: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoodManWEN/cx_Oracle_async/3d2f88a758ad49d1f05bc7fcbcc7ab9b74ae15b2/docs/source/images/BLANK -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to cx_Oracle_async's documentation! 2 | =========================================== 3 | **cx_Oracle_async** is a very simple asynchronous wrapper based on cx_Oracle, that allows you to get access to the Oracle database in asyncio programs. Which is easy to use , buy may not the best practice for efficiency concern. 4 | 5 | **cx_Oracle_async** is distributed under an open-source :ref:`license ` 6 | (the MIT license). 7 | 8 | User Guide 9 | ========== 10 | 11 | .. toctree:: 12 | :maxdepth: 3 13 | 14 | user_guide/quickstart.rst 15 | user_guide/sqlexecution.rst 16 | user_guide/advancedfeatures.rst 17 | 18 | 19 | API Manual 20 | ========== 21 | 22 | .. toctree:: 23 | :maxdepth: 3 24 | 25 | api_manual/moduleinterface.rst 26 | api_manual/sessionpool.rst 27 | api_manual/connections.rst 28 | api_manual/cursors.rst 29 | 30 | Indices and tables 31 | ================== 32 | 33 | * :ref:`genindex` 34 | * :ref:`modindex` 35 | * :ref:`search` 36 | -------------------------------------------------------------------------------- /docs/source/license.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | .. _license: 4 | 5 | ******* 6 | License 7 | ******* 8 | 9 | MIT License 10 | 11 | Copyright (c) 2020 GoodManWEN 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy 14 | of this software and associated documentation files (the "Software"), to deal 15 | in the Software without restriction, including without limitation the rights 16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 | copies of the Software, and to permit persons to whom the Software is 18 | furnished to do so, subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in all 21 | copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 | SOFTWARE. 30 | -------------------------------------------------------------------------------- /docs/source/user_guide/advancedfeatures.rst: -------------------------------------------------------------------------------- 1 | .. _advancedfeatures: 2 | 3 | ***************** 4 | Advanced Features 5 | ***************** 6 | 7 | Oracle Advanced Queuing(AQ) 8 | =========================== 9 | 10 | We made a very simple implement of Advanced Queue as it's Oracledb's exclusive feature. Here's a rough draft with basic examples shows you how to use it . 11 | 12 | 13 | Basic 14 | ----- 15 | 16 | Before running python codes below , you need to create queues in the database via console mode ,use tools such as SQL*Plus , run the following SQL: 17 | 18 | Create a queue for basic test: 19 | 20 | .. code-block:: sql 21 | 22 | BEGIN 23 | DBMS_AQADM.CREATE_QUEUE_TABLE('MY_QUEUE_TABLE', 'RAW'); 24 | DBMS_AQADM.CREATE_QUEUE('DEMO_RAW_QUEUE', 'MY_QUEUE_TABLE'); 25 | DBMS_AQADM.START_QUEUE('DEMO_RAW_QUEUE'); 26 | END; 27 | 28 | 29 | Create your own data type: 30 | 31 | .. code-block:: sql 32 | 33 | CREATE OR REPLACE TYPE udt_book AS OBJECT ( 34 | Title VARCHAR2(100), 35 | Authors VARCHAR2(100), 36 | Price NUMBER(5,2) 37 | ); 38 | 39 | Create a queue to contain modified data type: 40 | 41 | .. code-block:: sql 42 | 43 | BEGIN 44 | DBMS_AQADM.CREATE_QUEUE_TABLE('BOOK_QUEUE_TAB', 'UDT_BOOK'); 45 | DBMS_AQADM.CREATE_QUEUE('DEMO_BOOK_QUEUE', 'BOOK_QUEUE_TAB'); 46 | DBMS_AQADM.START_QUEUE('DEMO_BOOK_QUEUE'); 47 | END; 48 | 49 | Then you can get access to AQ through the following code: 50 | 51 | .. code-block:: python 52 | 53 | import cx_Oracle_async 54 | import asyncio 55 | 56 | async def features(pool): 57 | async with oracle_pool.acquire() as conn: 58 | # Basic put 59 | queue = await conn.queue("DEMO_RAW_QUEUE") 60 | PAYLOAD_DATA = [ 61 | "The first message", 62 | "The second message", 63 | "The third message" 64 | ] 65 | for data in PAYLOAD_DATA: 66 | await queue.enqOne(conn.msgproperties(payload=data)) 67 | await conn.commit() 68 | 69 | # Basic get 70 | queue = await conn.queue("DEMO_RAW_QUEUE") 71 | for _ in range(len(PAYLOAD_DATA)): 72 | msg = await queue.deqOne() 73 | print(msg.payload.decode(conn.encoding)) 74 | await conn.commit() 75 | 76 | async def main(): 77 | dsn = cx_Oracle_async.makedsn( 78 | host = 'localhost', 79 | port = '1521', 80 | service_name='orcl' 81 | ) 82 | async with cx_Oracle_async.create_pool(user = '' , password = '' , dsn = dsn) as pool: 83 | await features(pool) 84 | 85 | asyncio.run(main()) 86 | 87 | You can define a queue contains your own type of data: 88 | 89 | .. code-block:: python 90 | 91 | async def features(pool): 92 | async with pool.acquire() as conn: 93 | booksType = await conn.gettype("UDT_BOOK") 94 | book = booksType.newobject() 95 | book.TITLE = "Quick Brown Fox" 96 | book.AUTHORS = "The Dog" 97 | book.PRICE = 123 98 | 99 | # Put and get modified data 100 | queue = await conn.queue("DEMO_BOOK_QUEUE", booksType) 101 | await queue.enqOne(conn.msgproperties(payload=book)) 102 | msg = await queue.deqOne() 103 | print(msg.payload.TITLE) 104 | await conn.commit() 105 | 106 | Put many and get many: 107 | 108 | .. code-block:: python 109 | 110 | async def features(pool): 111 | async with pool.acquire() as conn: 112 | # Put many 113 | messages = [ 114 | "1", 115 | "2", 116 | "3", 117 | "4", 118 | "5", 119 | "6" 120 | ] 121 | queue = await conn.queue("DEMO_RAW_QUEUE") 122 | await queue.enqMany(conn.msgproperties(payload=m) for m in messages) 123 | await conn.commit() 124 | 125 | # Get many 126 | async for m in queue.deqMany(maxMessages=5): 127 | print(m.payload.decode(conn.encoding)) 128 | await queue.deqOne() # clean 129 | await conn.commit() 130 | 131 | 132 | As syntactic sugar ,there're some equivalent replacements perticularly in this async library designed for convenient use. ``Queue.pack(m)`` is equal to ``Connection.msgproperties(payload=m)`` , ``Queue.unpack(m)`` is equal to ``m.payload.decode(conn.encoding)`` so that you don't have to operate severl different type of objects. 133 | 134 | Further more , ``Queue.unpack(m)`` will automatically detect whether the input is a single object or a iterable, 135 | 136 | .. code-block:: python 137 | 138 | async def features(pool): 139 | async with pool.acquire() as conn: 140 | queue = await conn.queue("DEMO_RAW_QUEUE") 141 | async for _ in queue.deqMany():... # Clear queue 142 | message = "Hello World" 143 | 144 | # Queue.pack(m) is equal to Connection.msgproperties(payload=m) 145 | await queue.enqOne(queue.pack(message)) 146 | await queue.enqOne(conn.msgproperties(payload=message)) 147 | await conn.commit() 148 | 149 | # Queue.unpack(m) is equal to m.payload.decode(conn.encoding) 150 | ret1 = queue.unpack(await queue.deqOne()) 151 | ret2 = (await queue.deqOne()).payload.decode(conn.encoding) 152 | await conn.commit() 153 | assert ret1 == ret2 == message 154 | 155 | # Queue.unpack(m) will do automatically treatment 156 | # depends on whether input a single object or a iterable. 157 | await queue.enqMany(queue.pack(m) for m in map(str , range(10))) 158 | await conn.commit() 159 | # This returns a list but not a single object. 160 | ret1 = queue.unpack(await queue.deqMany()) 161 | await conn.commit() 162 | 163 | ret2 = [] 164 | await queue.enqMany(queue.pack(m) for m in map(str , range(10))) 165 | await conn.commit() 166 | async for m in queue.deqMany(): 167 | # This returns a single object since one input eachtime. 168 | ret2.append(queue.unpack(m)) 169 | assert ret1 == ret2 170 | 171 | 172 | It is noteworthy that since we were not implement this library asynchronous in a very basic level ,yet it's just a wrapper of synchronous functions via threads , that makes it not gonna work if you are doing two different things in a single connection at a time. For example in the following situation the code will **NOT** work: 173 | 174 | .. code-block:: python 175 | 176 | from cx_Oracle_async import makedsn , create_pool 177 | import asyncio 178 | 179 | async def coro_to_get_from_queue(conn , queue , oracle_pool): 180 | print(f"coroutine start fetching") 181 | ret = (await queue.deqOne()).payload.decode(conn.encoding) 182 | print(f"coroutine returned , {ret=}") 183 | await conn.commit() 184 | 185 | async def main(): 186 | loop = asyncio.get_running_loop() 187 | dsn = makedsn( 188 | host = 'localhost', 189 | port = '1521', 190 | service_name='orcl' 191 | ) 192 | async with create_pool(user = '' , password = '' , dsn = dsn) as oracle_pool: 193 | async with oracle_pool.acquire() as conn: 194 | queue = await conn.queue("DEMO_RAW_QUEUE") 195 | loop.create_task(coro_to_get_from_queue(conn , queue , oracle_pool)) 196 | 197 | await asyncio.sleep(1) 198 | 199 | data = 'Hello World' 200 | print(f"mainthread put some thing in queue ,{data=}") 201 | await queue.enqOne(conn.msgproperties(payload=data)) 202 | await conn.commit() 203 | print(f"mainthread put some thing done") 204 | 205 | await asyncio.sleep(1) 206 | print('Process terminated.') 207 | 208 | asyncio.run(main()) 209 | 210 | As we planned , there should be a fetching thread(coroutine) start fetcing , this action will block since the queue is empty , and will return until there's something put into the queue. Then after one second sleep , the main thread will put 'Hello World' into AQ and that will trigger the blocked fetching thread , and then the whole program terminated. 211 | 212 | However we will find the program blocking forever in real practice. That's because since ``queue.deqOptions.wait`` equals to ``cx_Oracle.DEQ_WAIT_FOREVER`` thus while there's nothing in the queue , the query will block **AND** this will take over the control of connection thread , which makes it impossible for the following code to put anything into the queue using the same thread, thus makes it a deadlock. 213 | 214 | If you would like to achieve the same result , you should do that in **ANOTHER** connection thread. Simply modify the code as follow: 215 | 216 | .. code-block:: python 217 | 218 | from cx_Oracle_async import makedsn , create_pool 219 | import asyncio 220 | from async_timeout import timeout 221 | 222 | async def coro_to_get_from_queue(conn , queue , oracle_pool): 223 | try: 224 | async with timeout(2): 225 | print(f"coroutine start fetching") 226 | ret = (await queue.deqOne()).payload.decode(conn.encoding) 227 | print(f"coroutine returned , {ret=}") 228 | await conn.commit() 229 | except asyncio.TimeoutError: 230 | print('two seconds passed , timeout triggered.') 231 | async with oracle_pool.acquire() as conn2: 232 | queue2 = await conn2.queue("DEMO_RAW_QUEUE") 233 | data = 'Hello World' 234 | print(f"another connection put some thing in queue ,{data=}") 235 | await queue2.enqOne(conn2.msgproperties(payload=data)) 236 | await conn2.commit() 237 | print(f"another connection put some thing done") 238 | 239 | async def main(): 240 | loop = asyncio.get_running_loop() 241 | dsn = makedsn( 242 | host = 'localhost', 243 | port = '1521', 244 | service_name='orcl' 245 | ) 246 | async with create_pool(user = '' , password = '' , dsn = dsn) as oracle_pool: 247 | async with oracle_pool.acquire() as conn: 248 | queue = await conn.queue("DEMO_RAW_QUEUE") 249 | loop.create_task(coro_to_get_from_queue(conn , queue , oracle_pool)) 250 | 251 | await asyncio.sleep(1) 252 | 253 | cursor = await conn.cursor() 254 | await cursor.execute(f"SELECT COUNT(*) FROM DEPT") 255 | fetch_result = await cursor.fetchall() 256 | print(f"main thread continue , {fetch_result=}") 257 | 258 | await asyncio.sleep(1) 259 | print('Process terminated.') 260 | 261 | asyncio.run(main()) 262 | 263 | Special Explanation for queue.deqMany() 264 | --------------------------------------- 265 | 266 | Queue.deqMany has a little bit complexity in usage , here're some further instructions. 267 | 268 | There're two ways of calling deqMany , you can use it as a normal asynchronous call , **OR** you can use it as a asynchronous generator. For example: 269 | 270 | .. code-block:: python 271 | 272 | from cx_Oracle_async import makedsn , create_pool 273 | import asyncio 274 | import random 275 | 276 | async def main(): 277 | loop = asyncio.get_running_loop() 278 | dsn = makedsn( 279 | host = 'localhost', 280 | port = '1521', 281 | service_name='orcl' 282 | ) 283 | async with create_pool(user = '' , password = '' , dsn = dsn) as oracle_pool: 284 | async with oracle_pool.acquire() as conn: 285 | 286 | # Init and clear a queue 287 | queue = await conn.queue("DEMO_RAW_QUEUE") 288 | async for _ in queue.deqMany():... 289 | 290 | # Fill up 291 | await queue.enqMany(queue.pack(m) for m in map(str , range(10))) 292 | await conn.commit() 293 | 294 | # The First way , use it as a normal asynchronous call , 295 | # This method use the original cx_Oracle.Queue.deqMany , so its 296 | # your choice if you're looking for efficiency concern. The 297 | # sub thread will block until all results returned. 298 | 299 | ret = await queue.deqMany(maxMessages = 10) 300 | await conn.commit() 301 | assert list(map(queue.unpack , ret)) == list(map(str , range(10))) 302 | 303 | # The second way , you can call deqMany as a asynchronous generator. 304 | # This is a self implemented method which yield queue.deqMany() with 305 | # queue.deqOptions = DEQ_NO_WAIT until it reaches the message limit or 306 | # there's nothing in the queue. The benifits is you will get immediate 307 | # response. 308 | 309 | await queue.enqMany(queue.pack(m) for m in map(str , range(10))) 310 | await conn.commit() 311 | 312 | ret = [] 313 | async for m in queue.deqMany(maxMessages = 10): 314 | ret.append(queue.unpack(m)) 315 | await conn.commit() 316 | assert ret == list(map(str , range(10))) 317 | 318 | asyncio.run(main()) 319 | 320 | It is worth mentioning that , the two means act differently when there's a empty queue. 321 | 322 | If you are using the ``await`` mode , for example ``ret = await queue.deqMany()`` if do have something there in the queue , this method will quickly return , while if there's nothing in the queue , the method will block until there's something new come into the queue , this will sometime make it a deadlock in main threadloop under improper use. So do please make sure you're clear about what you're doing. 323 | 324 | Of course you can change deqOptions into non-blocking mode like ``queue.deqOptions.wait = cx_Oracle_async.DEQ_NO_WAIT`` to aviod it. 325 | 326 | On the other hand , If you are using the ``async with`` mode , it will never block your main thread , which means it will not be affected by ``Queue.deqOptions`` , no matter what setting ``Queue.deqOptions`` is , it will return immediately eventhough there's nothing in the queue. 327 | 328 | So taking into consideration that when argument maxMessages equals to -1 (default value), it means unlimit fetch untill the queue is empty (where if youre using ``await`` mode , the "unlimit" do have a soft upper bound of 65535 , and no limit with ``async for`` mode cause its assembled from multiple querys). It's convenient to clear the whole queue with the following code: 329 | 330 | .. code-block:: python 331 | 332 | queue = await ... 333 | messages = list(map(str , range(random.randint(0,10000)))) 334 | await queue.enqMany(queue.pack(m) for m in messages) 335 | await conn.commit() 336 | 337 | # You are not clear about how large the queue size is 338 | # (there's also chance it's empty) 339 | # and want to take out all stuffs in it if its not empty. 340 | ret = [] 341 | async for m in queue.deqMany(): 342 | ret.append(queue.unpack(m)) 343 | print(ret) 344 | 345 | # Do something keep on. 346 | ... -------------------------------------------------------------------------------- /docs/source/user_guide/quickstart.rst: -------------------------------------------------------------------------------- 1 | .. _quickstart: 2 | 3 | *********** 4 | Quick Start 5 | *********** 6 | 7 | Install cx_Oracle_async 8 | ======================= 9 | 10 | Supports python3.7 or later. 11 | 12 | - Install from PyPI: 13 | 14 | .. code-block:: 15 | 16 | pip instal cx_Oracle_async 17 | 18 | Install Oracle Client 19 | ===================== 20 | 21 | If you're connecting to database which is on a different machine from python process , you need to setup a oracle instanct client module before you use this library. Check \ `cx-Oracle's installation guide `_ for further information. 22 | 23 | Basic Usage 24 | =========== 25 | 26 | All usage in **cx_Oracle_async** based on the session pool, **cx_Oracle_async** does not provide means you can setup a simple connection to database without pool manager. 27 | 28 | Here's a basic example: 29 | 30 | .. code-block:: python 31 | 32 | import cx_Oracle_async 33 | import asyncio 34 | 35 | async def main(): 36 | oracle_pool = await cx_Oracle_async.create_pool( 37 | host='localhost', 38 | port='1521', 39 | user='user', 40 | password='password', 41 | service_name='orcl', 42 | min = 2, 43 | max = 4, 44 | ) 45 | 46 | async with oracle_pool.acquire() as connection: 47 | async with connection.cursor() as cursor: 48 | await cursor.execute("SELECT * FROM V$SESSION") 49 | print(await cursor.fetchall()) 50 | 51 | await oracle_pool.close() 52 | 53 | asyncio.run(main()) 54 | 55 | Or you may prefer to use ``makedsn`` style to manage your token and server destinations: 56 | 57 | .. code-block:: python 58 | 59 | async def main(): 60 | dsn = cx_Oracle_async.makedsn('localhost' , '1521' , service_name = 'orcl') 61 | oracle_pool = await cx_Oracle_async.create_pool( 62 | 'username' , 63 | 'password' , 64 | dsn, 65 | ) 66 | ... 67 | 68 | You can use both context manager / non-context manager way to access your :meth:`SessionPool` , :meth:`Connection` , :meth:`Cursor` object , they will act the same in results. 69 | 70 | .. code-block:: python 71 | 72 | from cx_Oracle_async import makedsn , create_pool 73 | import asyncio 74 | 75 | async def main(): 76 | dsn = makedsn('localhost' , '1521' , service_name = 'orcl') 77 | pool_1 = await create_pool('username' , 'password' , dsn) 78 | 79 | async with create_pool('username' , 'password' , dsn) as pool_2: 80 | assert type(pool_1) == type(pool_2) 81 | 82 | conn_1 = await pool_2.acquire() 83 | async with pool_2.acquire() as conn_2: 84 | assert type(conn_1) == type(conn_2) 85 | 86 | cursor = await conn.cursor() 87 | await cursor.execute("SELECT * FROM V$SESSION") 88 | 89 | await pool_1.close() 90 | 91 | asyncio.run(main()) 92 | 93 | Closing SessionPools 94 | -------------------- 95 | 96 | You can hardly run into close problem in normal use with the help of a context manager , however , if you're using some kind of nested code structure , ``SessionPool.close()`` may get ``cx_Oracle.DatabaseError: ORA-24422`` which indicates there's still some connection remaining activate when ``close`` triggered. In this perticular situation , you may need to use ``SessionPool.close(force = True)`` to ignore those error. 97 | 98 | .. code-block:: python 99 | 100 | import cx_Oracle_async 101 | import asyncio 102 | 103 | async def solitary_fetching_thread(pool): 104 | # Simulation of a long duration query. 105 | async with pool.acquire() as conn: 106 | async with conn.cursor() as cursor: 107 | await cursor.execute("BEGIN DBMS_LOCK.SLEEP(10); END;") 108 | 109 | async def main(): 110 | dsn = cx_Oracle_async.makedsn('localhost' , '1521' , service_name = 'orcl') 111 | pool = await cx_Oracle_async.create_pool( 112 | 'username' , 113 | 'password' , 114 | dsn, 115 | ) 116 | 117 | loop = asyncio.get_running_loop() 118 | loop.create_task(solitary_fetching_thread(pool)) 119 | 120 | await asyncio.sleep(2) 121 | # If you're not using force == True (which is False by default) 122 | # you'll get a exception of ORA-24422. 123 | await pool.close(force = True) 124 | 125 | asyncio.run(main()) 126 | 127 | It is noteworthy that although ``force = True`` is set , main thread loop of ``pool.close()`` will not continue untill all connection finished its query anyhow. In latest version of Oracle database (e.g. Oracle DB 19c) , you can use ``interrupt = True`` to let every activate connection in sessionpool cancel its current qurey in order to get a quick return. However , **DO NOT** use this feature if you're using a legacy version of Orace DB such as Oracle DB 11g , force cancel feature may cause connection no response and create a deadlock in your mainloop. 128 | 129 | .. code-block:: python 130 | 131 | import cx_Oracle_async 132 | import asyncio 133 | from async_timeout import timeout 134 | 135 | async def solitary_fetching_thread(pool): 136 | # Simulation of a long duration query. 137 | async with pool.acquire() as conn: 138 | async with conn.cursor() as cursor: 139 | await cursor.execute("BEGIN DBMS_LOCK.SLEEP(10); END;") 140 | 141 | async def main(): 142 | dsn = cx_Oracle_async.makedsn('localhost' , '1521' , service_name = 'orcl') 143 | pool = await cx_Oracle_async.create_pool( 144 | 'username' , 145 | 'password' , 146 | dsn, 147 | ) 148 | 149 | loop = asyncio.get_running_loop() 150 | loop.create_task(solitary_fetching_thread(pool)) 151 | 152 | await asyncio.sleep(2) 153 | async with timeout(2): 154 | # This will not cause a asyncio.TimeoutError exception 155 | # cause the long duration query is canceled. 156 | await pool.close(force = True , interrupt = True) 157 | 158 | asyncio.run(main()) 159 | -------------------------------------------------------------------------------- /docs/source/user_guide/sqlexecution.rst: -------------------------------------------------------------------------------- 1 | .. _sqlexecution: 2 | 3 | ************* 4 | SQL Execution 5 | ************* 6 | 7 | As the primary way a Python application communicates with Oracle Database , you can get pretty much the same experience on SQL statements execution as the original cx_Oracle library , with feature of bind variables supported as well. 8 | 9 | SQL Queries 10 | =========== 11 | 12 | Both ``execute`` and ``executemany`` are supported. 13 | 14 | .. code-block:: python 15 | 16 | import cx_Oracle_async 17 | import asyncio 18 | 19 | async def main(): 20 | dsn = cx_Oracle_async.makedsn('localhost' , '1521' , service_name = 'orcl') 21 | pool = await cx_Oracle_async.create_pool('username', 'password', dsn) 22 | async with pool.acquire() as conn: 23 | async with conn.cursor() as cursor: 24 | 25 | # single fetch 26 | await cursor.execute( 27 | "SELECT DNAME FROM SCOTT.DEPT WHERE DEPTNO = :a" , 28 | (10 , ) 29 | ) 30 | print(await cursor.fetchone()) 31 | 32 | # multiple insert 33 | sql = "INSERT INTO SCOTT.DEPT(deptno , dname) VALUES (:a , :b)" 34 | sql_data = [ 35 | [60 , "Hello"], 36 | [70 , "World"], 37 | ] 38 | await cursor.executemany(sql , sql_data) 39 | await conn.commit() 40 | 41 | # multiple fetch 42 | await cursor.execute( 43 | "SELECT * FROM SCOTT.DEPT WHERE DEPTNO < :maximum" , 44 | maximum = 100 45 | ) 46 | print(await cursor.fetchall()) 47 | 48 | await pool.close() 49 | 50 | asyncio.run(main()) -------------------------------------------------------------------------------- /misc/performance_test_asynchronous.py: -------------------------------------------------------------------------------- 1 | class Counter: 2 | 3 | def __init__(self): 4 | self.success = 0 5 | self.fail = 0 6 | 7 | async def single_thread_fetching(oracle_pool , counter): 8 | SQL = 'SELECT DEPTNO FROM "SCOTT"."DEPT" WHERE DEPTNO = 10' 9 | async with oracle_pool.acquire() as connection: 10 | async with connection.cursor() as cursor: 11 | while True: 12 | await cursor.execute(SQL) 13 | r = await cursor.fetchone() 14 | if r and r[0] == 10: 15 | counter.success += 1 16 | else: 17 | counter.fail += 1 18 | 19 | async def report_thread(counter , oracle_pool): 20 | st_time = time.time() 21 | while True: 22 | await asyncio.sleep(2) 23 | time_passed = time.time() - st_time 24 | print(f"Report every 2 secs : [success] {counter.success} \t[fail] {counter.fail} \t[qps] {'%.2f' % round(counter.success / time_passed , 2)} \t[time_passed] {'%.2f' % round(time_passed,2)}s") 25 | 26 | async def main(): 27 | loop = asyncio.get_running_loop() 28 | oracle_pool = await cx_Oracle_async.create_pool( 29 | host='localhost', 30 | port='1521', 31 | user='system', 32 | password='123456', 33 | service_name='orcl', 34 | loop=loop, 35 | min = 2, 36 | max = THREAD_NUM, 37 | ) 38 | counter = Counter() 39 | loop = asyncio.get_running_loop() 40 | for _ in range(THREAD_NUM): 41 | loop.create_task(single_thread_fetching(oracle_pool , counter)) 42 | loop.create_task(report_thread(counter , oracle_pool)) 43 | while True: 44 | await asyncio.sleep(3600) 45 | 46 | if __name__ == '__main__': 47 | import asyncio 48 | import cx_Oracle_async 49 | import time 50 | import os 51 | THREAD_NUM = (os.cpu_count() or 1) << 2 52 | asyncio.run(main()) -------------------------------------------------------------------------------- /misc/performance_test_synchronous.py: -------------------------------------------------------------------------------- 1 | from concurrent.futures import ThreadPoolExecutor 2 | import cx_Oracle 3 | import os 4 | import time 5 | 6 | class Counter: 7 | 8 | def __init__(self , num): 9 | self.success = [0 for _ in range(num)] 10 | self.fail = [0 for _ in range(num)] 11 | 12 | def single_thread(pool , counter , _id): 13 | SQL = 'SELECT DEPTNO FROM "SCOTT"."DEPT" WHERE DEPTNO = 10' 14 | connection = pool.acquire() 15 | cursor = connection.cursor() 16 | while True: 17 | cursor.execute(SQL) 18 | r = cursor.fetchone() 19 | if r and r[0] == 10: 20 | counter.success[_id] += 1 21 | else: 22 | counter.fail[_id] += 1 23 | # break 24 | 25 | THREAD_NUM = (os.cpu_count() or 1) << 1 26 | dsn = cx_Oracle.makedsn(host = 'localhost' , port = '1521' , service_name = 'orcl') 27 | pool = cx_Oracle.SessionPool("system", "123456", dsn , min =1 , max = 4 , increment = 1 , threaded = True , encoding = 'UTF-8') 28 | counter = Counter(THREAD_NUM) 29 | with ThreadPoolExecutor(max_workers = THREAD_NUM << 5) as executor: 30 | for _ in range(THREAD_NUM): 31 | executor.submit(single_thread , pool , counter , _) 32 | st_time = time.time() 33 | while True: 34 | time.sleep(2) 35 | time_passed = time.time() - st_time 36 | print(f"Report every 2 secs : [success] {sum(counter.success)} \t[fail] {sum(counter.fail)} \t[qps] {'%.2f' % round(sum(counter.success) / time_passed , 2)} \t[time_passed] {'%.2f' % round(time_passed,2)}s") -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cx-Oracle>=8.1.0 2 | ThreadPoolExecutorPlus>=0.2.0 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from requests import get as rget 3 | from bs4 import BeautifulSoup 4 | import logging , sys 5 | # init logger 6 | logger = logging.getLogger(__name__) 7 | logger.setLevel(logging.INFO) 8 | sh = logging.StreamHandler(stream=sys.stdout) 9 | format = logging.Formatter("%(message)s")#("%(asctime)s - %(message)s") 10 | sh.setFormatter(format) 11 | logger.addHandler(sh) 12 | 13 | # 14 | def get_install_requires(filename): 15 | with open(filename,'r') as f: 16 | lines = f.readlines() 17 | return [x.strip() for x in lines] 18 | 19 | # 20 | url = 'https://github.com/GoodManWEN/cx_Oracle_async' 21 | release = f'{url}/releases/latest' 22 | headers = { 23 | "User-Agent": "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36", 24 | "Connection": "keep-alive", 25 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", 26 | "Accept-Language": "zh-CN,zh;q=0.8" 27 | } 28 | 29 | html = BeautifulSoup(rget(url , headers).text ,'lxml') 30 | description = html.find('meta' ,{'name':'description'}).get('content') 31 | for kw in (' - GitHub', ' - GoodManWEN'): 32 | if ' - GitHub' in description: 33 | description = description[:description.index(' - GitHub')] 34 | html = BeautifulSoup(rget(release , headers).text ,'lxml') 35 | logger.info(f"description: {description}") 36 | 37 | # 38 | with open('tagname','r',encoding='utf-8') as f: 39 | version = f.read() 40 | if ':' in version: 41 | version = version[:version.index(':')].strip() 42 | version = version.strip() 43 | logger.info(f"version: {version}") 44 | 45 | # 46 | with open('README.md','r',encoding='utf-8') as f: 47 | long_description_lines = f.readlines() 48 | 49 | long_description_lines_copy = long_description_lines[:] 50 | long_description_lines_copy.insert(0,'r"""\n') 51 | long_description_lines_copy.append('"""\n') 52 | 53 | # update __init__ docs 54 | with open('cx_Oracle_async/__init__.py','r',encoding='utf-8') as f: 55 | init_content = f.readlines() 56 | 57 | for line in init_content: 58 | if line == "__version__ = ''\n": 59 | long_description_lines_copy.append(f"__version__ = '{version}'\n") 60 | else: 61 | long_description_lines_copy.append(line) 62 | 63 | with open('cx_Oracle_async/__init__.py','w',encoding='utf-8') as f: 64 | f.writelines(long_description_lines_copy) 65 | 66 | setup( 67 | name="cx_Oracle_async", 68 | version=version, 69 | author="WEN", 70 | description=description, 71 | long_description=''.join(long_description_lines), 72 | long_description_content_type="text/markdown", 73 | url="https://github.com/GoodManWEN/cx_Oracle_async", 74 | packages = find_packages(), 75 | install_requires = get_install_requires('requirements.txt'), 76 | classifiers=[ 77 | 'Programming Language :: Python :: 3', 78 | 'Programming Language :: Python :: 3.7', 79 | 'Programming Language :: Python :: 3.8', 80 | 'Programming Language :: Python :: 3.9', 81 | 'License :: OSI Approved :: MIT License', 82 | 'Operating System :: POSIX :: Linux', 83 | 'Operating System :: Microsoft :: Windows', 84 | 'Framework :: AsyncIO', 85 | ], 86 | python_requires='>=3.7', 87 | keywords=["oracle" , "cx_Oracle" , "asyncio" ,"cx_Oracle_async"] 88 | ) 89 | -------------------------------------------------------------------------------- /tests/test_aq.py: -------------------------------------------------------------------------------- 1 | import os , sys 2 | sys.path.append(os.getcwd()) 3 | import pytest 4 | import asyncio 5 | import time 6 | from cx_Oracle_async import * 7 | import cx_Oracle 8 | 9 | async def modify_deqopts(queue , val): 10 | queue.deqOptions.wait = val 11 | 12 | async def fetch_from_queue_no_wait(oracle_pool , loop): 13 | async with oracle_pool.acquire() as conn: 14 | queue = await conn.queue("DEMO_RAW_QUEUE") 15 | loop.create_task(modify_deqopts(queue , DEQ_NO_WAIT)) 16 | ret = await queue.deqOne() 17 | if ret: 18 | ret = ret.payload.decode(conn.encoding) 19 | await conn.commit() 20 | return ret 21 | 22 | async def fetch_from_queue_wait_forever(oracle_pool , loop): 23 | async with oracle_pool.acquire() as conn: 24 | queue = await conn.queue("DEMO_RAW_QUEUE") 25 | loop.create_task(modify_deqopts(queue , DEQ_WAIT_FOREVER)) 26 | ret = await queue.deqOne() 27 | if ret: 28 | ret = ret.payload.decode(conn.encoding) 29 | return ret 30 | 31 | async def put_into_queue(oracle_pool , loop , queue_name): 32 | await asyncio.sleep(2) 33 | async with oracle_pool.acquire() as conn: 34 | queue = await conn.queue(queue_name) 35 | await queue.enqOne(conn.msgproperties(payload='Hello World')) 36 | await conn.commit() 37 | 38 | @pytest.mark.asyncio 39 | async def test_multiquery(): 40 | loop = asyncio.get_running_loop() 41 | dsn = makedsn('localhost','1521',sid='xe') 42 | INAQ = 0.5 43 | async with create_pool(user='system',password='oracle',dsn=dsn,max=4) as oracle_pool: 44 | async with oracle_pool.acquire() as conn: 45 | async with conn.cursor() as cursor: 46 | try: 47 | await cursor.execute(""" 48 | BEGIN 49 | DBMS_AQADM.STOP_QUEUE(queue_name => 'DEMO_RAW_QUEUE'); 50 | DBMS_AQADM.DROP_QUEUE(queue_name => 'DEMO_RAW_QUEUE'); 51 | DBMS_AQADM.DROP_QUEUE_TABLE(queue_table => 'MY_QUEUE_TABLE'); 52 | END; 53 | 54 | """) 55 | except Exception as e: 56 | ... 57 | 58 | await cursor.execute(""" 59 | BEGIN 60 | DBMS_AQADM.CREATE_QUEUE_TABLE('MY_QUEUE_TABLE', 'RAW'); 61 | DBMS_AQADM.CREATE_QUEUE('DEMO_RAW_QUEUE', 'MY_QUEUE_TABLE'); 62 | DBMS_AQADM.START_QUEUE('DEMO_RAW_QUEUE'); 63 | END; 64 | """) 65 | 66 | try: 67 | await cursor.execute("DROP TYPE udt_book FORCE") 68 | except Exception as e: 69 | ... 70 | 71 | await cursor.execute(""" 72 | CREATE OR REPLACE TYPE udt_book AS OBJECT ( 73 | Title VARCHAR2(100), 74 | Authors VARCHAR2(100), 75 | Price NUMBER(5,2) 76 | ); 77 | """) 78 | 79 | try: 80 | await cursor.execute(""" 81 | BEGIN 82 | DBMS_AQADM.STOP_QUEUE(queue_name => 'DEMO_BOOK_QUEUE2'); 83 | DBMS_AQADM.DROP_QUEUE(queue_name => 'DEMO_BOOK_QUEUE2'); 84 | DBMS_AQADM.DROP_QUEUE_TABLE(queue_table => 'BOOK_QUEUE_TAB2'); 85 | END; 86 | 87 | """) 88 | except Exception as e: 89 | ... 90 | 91 | await cursor.execute(""" 92 | BEGIN 93 | DBMS_AQADM.CREATE_QUEUE_TABLE('BOOK_QUEUE_TAB2', 'UDT_BOOK'); 94 | DBMS_AQADM.CREATE_QUEUE('DEMO_BOOK_QUEUE2', 'BOOK_QUEUE_TAB2'); 95 | DBMS_AQADM.START_QUEUE('DEMO_BOOK_QUEUE2'); 96 | END; 97 | """) 98 | 99 | # test put 100 | queue = await conn.queue("DEMO_RAW_QUEUE") 101 | PAYLOAD_DATA = [ 102 | "The first message", 103 | "The second message", 104 | ] 105 | for data in PAYLOAD_DATA: 106 | await queue.enqOne(conn.msgproperties(payload=data)) 107 | await conn.commit() 108 | 109 | # test get 110 | queue = await conn.queue("DEMO_RAW_QUEUE") 111 | msg = await queue.deqOne() 112 | assert msg.payload.decode(conn.encoding) == "The first message" 113 | msg = await queue.deqOne() 114 | assert msg.payload.decode(conn.encoding) == "The second message" 115 | await conn.commit() 116 | 117 | # Define your own type of data 118 | booksType = await conn.gettype("UDT_BOOK") 119 | book = booksType.newobject() 120 | book.TITLE = "Quick Brown Fox" 121 | book.AUTHORS = "The Dog" 122 | book.PRICE = 123 123 | 124 | # Put and get modified data 125 | queue = await conn.queue("DEMO_BOOK_QUEUE2", booksType) 126 | await queue.enqOne(conn.msgproperties(payload=book)) 127 | await conn.commit() 128 | msg = await queue.deqOne() 129 | await conn.commit() 130 | assert msg.payload.TITLE == "Quick Brown Fox" 131 | 132 | # Put many 133 | messages = [ 134 | "1", 135 | "2", 136 | "3", 137 | "4", 138 | "5", 139 | "6" 140 | ] 141 | queue = await conn.queue("DEMO_RAW_QUEUE") 142 | await queue.enqMany(conn.msgproperties(payload=m) for m in messages) 143 | await conn.commit() 144 | 145 | # Get many 146 | res = [] 147 | async for m in queue.deqMany(maxMessages=5): 148 | res.append(m.payload.decode(conn.encoding)) 149 | await queue.deqOne() # clean 150 | await conn.commit() 151 | assert res == list(map(str,range(1,6))) 152 | 153 | # Get nowait 154 | st_time = time.time() 155 | async for m in queue.deqMany(maxMessages=5): 156 | res.append(m.payload.decode(conn.encoding)) 157 | ed_time = time.time() 158 | assert (ed_time - st_time) <= 0.5 159 | 160 | # test aq options 161 | st_time = time.time() 162 | _task = loop.create_task(fetch_from_queue_no_wait(oracle_pool , loop)) 163 | result = await _task 164 | ed_time = time.time() 165 | assert result == None 166 | assert (ed_time - st_time) <= INAQ 167 | 168 | # 169 | st_time = time.time() 170 | _task = loop.create_task(fetch_from_queue_wait_forever(oracle_pool , loop)) 171 | loop.create_task(put_into_queue(oracle_pool , loop , "DEMO_RAW_QUEUE")) 172 | result = await _task 173 | ed_time = time.time() 174 | assert result == "Hello World" 175 | assert (2 - INAQ) <= (ed_time - st_time) <= (2 + INAQ) 176 | 177 | @pytest.mark.asyncio 178 | async def test_block_behavior(): 179 | loop = asyncio.get_running_loop() 180 | dsn = makedsn('localhost','1521',sid='xe') 181 | INAQ = 0.5 182 | async with create_pool(user='system',password='oracle',dsn=dsn,max=4) as oracle_pool: 183 | async with oracle_pool.acquire() as conn: 184 | async with conn.cursor() as cursor: 185 | try: 186 | await cursor.execute(""" 187 | BEGIN 188 | DBMS_AQADM.STOP_QUEUE(queue_name => 'DEMO_RAW_QUEUE3'); 189 | DBMS_AQADM.DROP_QUEUE(queue_name => 'DEMO_RAW_QUEUE3'); 190 | DBMS_AQADM.DROP_QUEUE_TABLE(queue_table => 'MY_QUEUE_TABLE3'); 191 | END; 192 | 193 | """) 194 | except Exception as e: 195 | ... 196 | 197 | await cursor.execute(""" 198 | BEGIN 199 | DBMS_AQADM.CREATE_QUEUE_TABLE('MY_QUEUE_TABLE3', 'RAW'); 200 | DBMS_AQADM.CREATE_QUEUE('DEMO_RAW_QUEUE3', 'MY_QUEUE_TABLE3'); 201 | DBMS_AQADM.START_QUEUE('DEMO_RAW_QUEUE3'); 202 | END; 203 | """) 204 | 205 | queue = await conn.queue("DEMO_RAW_QUEUE3") 206 | queue.deqOptions.wait = DEQ_WAIT_FOREVER 207 | messages = list(map(str,range(6))) 208 | await queue.enqMany(queue.pack(m) for m in messages) 209 | await conn.commit() 210 | 211 | ret = [] 212 | async for m in queue.deqMany(): 213 | ret.append(queue.unpack(m)) 214 | await conn.commit() 215 | assert ret == messages 216 | 217 | # Return immediate with empty queue 218 | st_time = time.time() 219 | async for m in queue.deqMany(): 220 | ... 221 | ed_time = time.time() 222 | assert (ed_time - st_time) <= INAQ 223 | 224 | messages = list(map(str,range(6))) 225 | await queue.enqMany(queue.pack(m) for m in messages) 226 | await conn.commit() 227 | 228 | ret = await queue.deqMany(5) 229 | assert list(map(queue.unpack , ret)) == list(map(str,range(5))) 230 | # return all 231 | ret = await queue.deqMany(65536) 232 | assert list(map(queue.unpack , ret)) == ['5',] 233 | await conn.commit() 234 | 235 | # test block 236 | loop.create_task(put_into_queue(oracle_pool , loop , "DEMO_RAW_QUEUE3")) 237 | st_time = time.time() 238 | ret = await queue.deqMany(-1) 239 | ed_time = time.time() 240 | assert queue.unpack(ret) == ["Hello World",] 241 | assert (2 - INAQ) <= (ed_time - st_time) <= (2 + INAQ) 242 | 243 | await queue.enqOne(queue.pack('Hello World')) 244 | await conn.commit() 245 | obj = queue.deqMany(65535) 246 | ret = await obj 247 | try: 248 | ret = await obj 249 | raise TypeError() 250 | except Exception as exc: 251 | assert isinstance(exc , RuntimeError) 252 | 253 | obj = queue.deqMany(65535) 254 | async for _ in obj:... 255 | try: 256 | async for _ in obj:... 257 | raise TypeError() 258 | except Exception as exc: 259 | assert isinstance(exc , RuntimeError) -------------------------------------------------------------------------------- /tests/test_behavior.py: -------------------------------------------------------------------------------- 1 | import os , sys 2 | sys.path.append(os.getcwd()) 3 | import pytest 4 | import asyncio 5 | from cx_Oracle_async import * 6 | 7 | @pytest.mark.asyncio 8 | async def test_new_table(): 9 | dsn = makedsn( 10 | host = 'localhost', 11 | port = '1521', 12 | sid = 'xe' 13 | ) 14 | oracle_pool = await create_pool( 15 | user = 'system', 16 | password = 'oracle', 17 | dsn = dsn 18 | ) 19 | 20 | async with oracle_pool.acquire() as connection: 21 | async with connection.cursor() as cursor: 22 | # check if dept exesits 23 | await cursor.execute("SELECT COUNT(*) FROM USER_TABLES WHERE TABLE_NAME = UPPER(:a)" , ('DEPT' , )) 24 | ret = await cursor.fetchone() 25 | assert ret 26 | if ret[0] > 0: 27 | await cursor.execute("DROP TABLE DEPT") 28 | 29 | sql = f""" 30 | CREATE TABLE DEPT 31 | (DEPTNO NUMBER(2) CONSTRAINT PK_DEPT PRIMARY KEY, 32 | DNAME VARCHAR2(14), 33 | LOC VARCHAR2(13) 34 | ) 35 | """ 36 | await cursor.execute(sql) 37 | 38 | # Single Insertion 39 | sql = "INSERT INTO DEPT(DEPTNO , DNAME , LOC) VALUES (:a , :b , :c)" 40 | await cursor.execute(sql , (10 ,'ACCOUNTING','NEW YORK')) 41 | await connection.commit() 42 | 43 | # bind vars 44 | sql = "SELECT DNAME FROM DEPT WHERE DEPTNO = :dno" 45 | await cursor.execute(sql , dno = 10) 46 | ret = await cursor.fetchone() 47 | assert ret 48 | assert ret[0] == 'ACCOUNTING' 49 | 50 | # Multiple Insertion 51 | sql = "INSERT INTO DEPT(DEPTNO , DNAME , LOC) VALUES (:a , :b , :c)" 52 | data = [ 53 | (30,'SALES','CHICAGO'), 54 | (40,'OPERATIONS','BOSTON'), 55 | ] 56 | await cursor.executemany(sql , data) 57 | await connection.commit() 58 | 59 | # Check 60 | await cursor.execute("SELECT DNAME , LOC FROM DEPT WHERE DEPTNO BETWEEN :a AND :b" , (10 , 30)) 61 | ret = await cursor.fetchall() 62 | assert len(ret) == 2 63 | assert ret[0][0] == 'ACCOUNTING' 64 | assert ret[1][1] == 'CHICAGO' 65 | 66 | await oracle_pool.close() 67 | 68 | @pytest.mark.asyncio 69 | async def test_usage(): 70 | dsn = makedsn( 71 | host = 'localhost', 72 | port = '1521', 73 | sid = 'xe' 74 | ) 75 | oracle_pool1 = await create_pool( 76 | user = 'system', 77 | password = 'oracle', 78 | dsn = dsn 79 | ) 80 | async with create_pool(user = 'system',password = 'oracle',dsn = dsn) as oracle_pool2: 81 | assert type(oracle_pool1) is type(oracle_pool2) 82 | 83 | conn1 = await oracle_pool1.acquire() 84 | async with oracle_pool1.acquire() as conn2: 85 | assert type(conn1) is type(conn2) 86 | 87 | cursor1 = await conn1.cursor() 88 | async with conn1.cursor() as cursor2: 89 | assert type(cursor1) is type(cursor2) 90 | 91 | conn = await oracle_pool1.acquire() 92 | await conn.release() 93 | conn = await oracle_pool1.acquire() 94 | await oracle_pool1.release(conn) 95 | 96 | # test weakref 97 | conn = await oracle_pool1.acquire() 98 | conn = await oracle_pool1.acquire() 99 | conn = await oracle_pool1.acquire() 100 | assert len(oracle_pool1._occupied) == 2 101 | -------------------------------------------------------------------------------- /tests/test_concurrency.py: -------------------------------------------------------------------------------- 1 | import os , sys 2 | sys.path.append(os.getcwd()) 3 | import pytest 4 | import asyncio 5 | import time 6 | from cx_Oracle_async import * 7 | 8 | @pytest.mark.asyncio 9 | async def test_multiquery(): 10 | 11 | CQUANT = 16 12 | INAC = 0.5 13 | SLEEP_TIME = 5 14 | 15 | async def single_thread(oracle_pool , counter): 16 | async with oracle_pool.acquire() as connection: 17 | async with connection.cursor() as cursor: 18 | await cursor.execute('BEGIN DBMS_LOCK.SLEEP(:a); END;' , (SLEEP_TIME, )) 19 | counter[0] += 1 20 | 21 | 22 | dsn = makedsn('localhost','1521',sid='xe') 23 | async with create_pool(user='system',password='oracle',dsn=dsn,max=CQUANT) as oracle_pool: 24 | 25 | # under limit test 26 | counter = [0 , ] 27 | st_time = time.time() 28 | await asyncio.gather(*(single_thread(oracle_pool , counter) for _ in range(CQUANT))) 29 | ed_time = time.time() 30 | assert counter[0] == CQUANT 31 | assert (SLEEP_TIME - INAC) <= (ed_time - st_time) <= (CQUANT * SLEEP_TIME // 2 + INAC) 32 | 33 | # overflow test 34 | counter = [0 , ] 35 | st_time = time.time() 36 | await asyncio.gather(*(single_thread(oracle_pool , counter) for _ in range(CQUANT + 1))) 37 | ed_time = time.time() 38 | assert counter[0] == CQUANT + 1 39 | assert (SLEEP_TIME * 2 - INAC) <= (ed_time - st_time) <= (SLEEP_TIME * 3 + INAC) -------------------------------------------------------------------------------- /tests/test_connection.py: -------------------------------------------------------------------------------- 1 | import os , sys 2 | sys.path.append(os.getcwd()) 3 | import pytest 4 | import asyncio 5 | from cx_Oracle_async import * 6 | import time 7 | 8 | @pytest.mark.asyncio 9 | async def test_different_connect_ways(): 10 | dsn = makedsn( 11 | host = 'localhost', 12 | port = '1521', 13 | sid = 'xe' 14 | ) 15 | oracle_pool = await create_pool( 16 | user = 'system', 17 | password = 'oracle', 18 | dsn = dsn 19 | ) 20 | ret = await oracle_pool.close() 21 | assert ret == None 22 | 23 | oracle_pool = await create_pool( 24 | host = 'localhost', 25 | port = '1521', 26 | user = 'system', 27 | password = 'oracle', 28 | sid = 'xe', 29 | min = 2 , 30 | max = 4 31 | ) 32 | 33 | async with oracle_pool.acquire() as connection: 34 | async with connection.cursor() as cursor: 35 | pass 36 | 37 | ret = await oracle_pool.close() 38 | assert ret == None 39 | 40 | @pytest.mark.asyncio 41 | async def test_properties(): 42 | INAQ = 0.5 43 | dsn = makedsn( 44 | host = 'localhost', 45 | port = '1521', 46 | sid = 'xe' 47 | ) 48 | oracle_pool = await create_pool( 49 | user = 'system', 50 | password = 'oracle', 51 | dsn = dsn 52 | ) 53 | 54 | async with oracle_pool.acquire() as conn: 55 | st_time = time.time() 56 | conn.module = 'hello world' 57 | conn.action = 'test_action' 58 | conn.client_identifier = 'test_identifier' 59 | conn.clientinfo = 'test_info' 60 | ed_time = time.time() 61 | assert (ed_time - st_time) <= INAQ 62 | 63 | async with conn.cursor() as cursor: 64 | await cursor.execute("SELECT SID, MODULE ,ACTION ,CLIENT_IDENTIFIER , CLIENT_INFO FROM V$SESSION WHERE MODULE='hello world'") 65 | r = await cursor.fetchall() 66 | assert len(r) == 1 67 | _ , _module , _action , _ciden , _cinfo = r[0] 68 | assert _module == 'hello world' 69 | assert _action == 'test_action' 70 | assert _ciden == 'test_identifier' 71 | assert _cinfo == 'test_info' 72 | 73 | # test no update 74 | st_time = time.time() 75 | conn.module = 'hello world2' 76 | conn.action = 'test_action2' 77 | conn.client_identifier = 'test_identifier2' 78 | conn.clientinfo = 'test_info2' 79 | ed_time = time.time() 80 | assert (ed_time - st_time) <= INAQ 81 | 82 | conn2 = await oracle_pool.acquire() 83 | async with conn2.cursor() as cursor: 84 | await cursor.execute("SELECT SID, MODULE ,ACTION ,CLIENT_IDENTIFIER , CLIENT_INFO FROM V$SESSION WHERE MODULE='hello world2'") 85 | r = await cursor.fetchall() 86 | assert len(r) == 0 -------------------------------------------------------------------------------- /tests/test_drop.py: -------------------------------------------------------------------------------- 1 | import os , sys 2 | sys.path.append(os.getcwd()) 3 | import pytest 4 | import asyncio 5 | import time 6 | from async_timeout import timeout 7 | from cx_Oracle_async import * 8 | import cx_Oracle 9 | import threading 10 | 11 | async def create_long_query(oracle_pool): 12 | async with oracle_pool.acquire() as conn: 13 | cursor = await conn.cursor() 14 | try: 15 | await cursor.execute("BEGIN DBMS_LOCK.SLEEP(:a); END;",(10,)) 16 | except Exception as e: 17 | assert isinstance(e , cx_Oracle.OperationalError) 18 | 19 | def create_long_query_sync(oracle_pool): 20 | ''' 21 | use sync function in order to avoid pytest loop never stop bug. 22 | ''' 23 | try: 24 | conn = oracle_pool._pool.acquire() 25 | cursor = conn.cursor() 26 | cursor.execute("BEGIN DBMS_LOCK.SLEEP(:a); END;",(10,)) 27 | except: 28 | ... 29 | 30 | @pytest.mark.asyncio 31 | async def test_force_close(): 32 | loop = asyncio.get_running_loop() 33 | dsn = makedsn('localhost','1521',sid='xe') 34 | INAQ = 0.5 35 | oracle_pool = await create_pool(user='system',password='oracle',dsn=dsn) 36 | loop.create_task(create_long_query(oracle_pool)) 37 | st_time = time.time() 38 | await asyncio.sleep(2) 39 | await oracle_pool.close(force = True , interrupt = False) 40 | ed_time = time.time() 41 | assert (10 - INAQ) <= (ed_time - st_time) <= (10 + INAQ) 42 | 43 | # test occupy 44 | oracle_pool = await create_pool(user='system',password='oracle',dsn=dsn,max=4) 45 | conn1 = await oracle_pool.acquire() 46 | conn2 = await oracle_pool.acquire() 47 | assert len(oracle_pool._occupied) == 2 48 | conn3 = await oracle_pool.acquire() 49 | assert len(oracle_pool._occupied) == 3 50 | st_time = time.time() 51 | await asyncio.sleep(2) 52 | async with timeout(2): 53 | # no running task , return immediately 54 | await oracle_pool.close(force = True , interrupt = False) 55 | ed_time = time.time() 56 | assert (2 - INAQ) <= (ed_time - st_time) <= (2 + INAQ) 57 | 58 | # test interrupt 59 | oracle_pool = await create_pool(user='system',password='oracle',dsn=dsn,max=4) 60 | st_time = time.time() 61 | t = threading.Thread(target = create_long_query_sync , args = (oracle_pool,)) 62 | t.setDaemon(True) 63 | t.start() 64 | await asyncio.sleep(2) 65 | exception_flag = False 66 | try: 67 | async with timeout(2): 68 | # no response forever 69 | await oracle_pool.close(force = True , interrupt = True) 70 | except Exception as e: 71 | exception_flag = True 72 | assert isinstance(e , asyncio.TimeoutError) 73 | ed_time = time.time() 74 | assert exception_flag 75 | assert (4 - INAQ) <= (ed_time - st_time) <= (10 - INAQ) -------------------------------------------------------------------------------- /tests/test_import.py: -------------------------------------------------------------------------------- 1 | import os , sys 2 | sys.path.append(os.getcwd()) 3 | import pytest 4 | import asyncio 5 | from cx_Oracle_async import * 6 | 7 | @pytest.mark.asyncio 8 | async def test_import(): 9 | ... 10 | -------------------------------------------------------------------------------- /tests/test_ping.py: -------------------------------------------------------------------------------- 1 | import os , sys 2 | sys.path.append(os.getcwd()) 3 | import pytest 4 | import asyncio 5 | from cx_Oracle_async import * 6 | import cx_Oracle 7 | 8 | @pytest.mark.asyncio 9 | async def test_ping(): 10 | dsn = makedsn( 11 | host = 'localhost', 12 | port = '1521', 13 | sid = 'xe' 14 | ) 15 | async with create_pool(user = 'system',password = 'oracle',dsn = dsn) as oracle_pool: 16 | conn = await oracle_pool.acquire() 17 | r = await conn.ping() 18 | assert r == None 19 | 20 | await conn.release() 21 | exception_flag = False 22 | try: 23 | await conn.ping() 24 | except Exception as e: 25 | exception_flag = True 26 | assert isinstance(e , cx_Oracle.InterfaceError) 27 | assert exception_flag 28 | -------------------------------------------------------------------------------- /tests/test_rollback.py: -------------------------------------------------------------------------------- 1 | import os , sys 2 | sys.path.append(os.getcwd()) 3 | import pytest 4 | import asyncio 5 | from cx_Oracle_async import * 6 | import cx_Oracle 7 | 8 | @pytest.mark.asyncio 9 | async def test_ping(): 10 | dsn = makedsn( 11 | host = 'localhost', 12 | port = '1521', 13 | sid = 'xe' 14 | ) 15 | async with create_pool(user = 'system',password = 'oracle',dsn = dsn) as oracle_pool: 16 | async with oracle_pool.acquire() as conn: 17 | async with conn.cursor() as cursor: 18 | # check if dept exesits 19 | await cursor.execute("SELECT COUNT(*) FROM USER_TABLES WHERE TABLE_NAME = UPPER(:a)" , ('DEPT' , )) 20 | ret = await cursor.fetchone() 21 | assert ret 22 | if ret[0] > 0: 23 | await cursor.execute("DROP TABLE DEPT") 24 | 25 | sql = f""" 26 | CREATE TABLE DEPT 27 | (DEPTNO NUMBER(2) CONSTRAINT PK_DEPT PRIMARY KEY, 28 | DNAME VARCHAR2(14), 29 | LOC VARCHAR2(13) 30 | ) 31 | """ 32 | await cursor.execute(sql) 33 | 34 | await cursor.execute(f"INSERT INTO DEPT(DEPTNO) VALUES (:a)" , (10 , )) 35 | await conn.rollback() 36 | await cursor.execute(f"INSERT INTO DEPT(DEPTNO) VALUES (:a)" , (12 , )) 37 | await conn.commit() 38 | 39 | async with oracle_pool.acquire() as conn: 40 | async with conn.cursor() as cursor: 41 | await cursor.execute(f"SELECT * FROM DEPT") 42 | r = await cursor.fetchall() 43 | assert len(r) == 1 44 | assert r[0][0] == 12 -------------------------------------------------------------------------------- /tests/test_stress.py: -------------------------------------------------------------------------------- 1 | import os , sys 2 | sys.path.append(os.getcwd()) 3 | import pytest 4 | import asyncio 5 | import time 6 | from cx_Oracle_async import * 7 | 8 | class Status: 9 | 10 | def __init__(self): 11 | self.status = True 12 | 13 | async def single_fetch(oracle_pool , n_data = 70): 14 | async with oracle_pool.acquire() as conn: 15 | async with conn.cursor() as cursor: 16 | await cursor.execute(f"INSERT INTO DEPT(DEPTNO) VALUES (:a)" , (n_data , )) 17 | await conn.commit() 18 | 19 | async with oracle_pool.acquire() as conn: 20 | async with conn.cursor() as cursor: 21 | await cursor.execute(f"SELECT DEPTNO FROM DEPT WHERE DEPTNO = :a" , (n_data , )) 22 | ret = await cursor.fetchone() 23 | assert ret 24 | assert ret[0] == n_data 25 | await conn.commit() 26 | 27 | async with oracle_pool.acquire() as conn: 28 | async with conn.cursor() as cursor: 29 | await cursor.execute(f"DELETE FROM DEPT WHERE DEPTNO = :a" , (n_data , )) 30 | await conn.commit() 31 | 32 | async with oracle_pool.acquire() as conn: 33 | async with conn.cursor() as cursor: 34 | await cursor.execute(f"SELECT DEPTNO FROM DEPT WHERE DEPTNO = :a" , (n_data , )) 35 | ret = await cursor.fetchone() 36 | assert ret == None 37 | await conn.commit() 38 | 39 | async def single_thread(oracle_pool , n_data , status): 40 | while status.status: 41 | await single_fetch(oracle_pool , n_data) 42 | 43 | async def daemon_thread(status): 44 | await asyncio.sleep(60) 45 | status.status = False 46 | 47 | @pytest.mark.asyncio 48 | async def test_multiquery(): 49 | dsn = makedsn('localhost','1521',sid='xe') 50 | max_thread = min(max(int((os.cpu_count() or 1) // 2) , 2) , 8) 51 | start_number = 77 52 | oracle_pool = await create_pool(user='system',password='oracle',dsn=dsn,max=max_thread) 53 | async with oracle_pool.acquire() as conn: 54 | async with conn.cursor() as cursor: 55 | await cursor.execute("SELECT COUNT(*) FROM USER_TABLES WHERE TABLE_NAME = UPPER(:a)" , ('DEPT' , )) 56 | ret = await cursor.fetchone() 57 | assert ret 58 | if ret[0] <= 0: 59 | sql = f""" 60 | CREATE TABLE DEPT 61 | (DEPTNO NUMBER(2) CONSTRAINT PK_DEPT PRIMARY KEY, 62 | DNAME VARCHAR2(14), 63 | LOC VARCHAR2(13) 64 | ) 65 | """ 66 | try: 67 | await cursor.execute(sql) 68 | await cursor.execute(f"DELETE FROM DEPT WHERE DEPTNO >= :a" , (start_number , )) 69 | await conn.commit() 70 | except: 71 | pass 72 | else: 73 | await cursor.execute(f"DELETE FROM DEPT WHERE DEPTNO >= :a" , (start_number , )) 74 | await conn.commit() 75 | status = Status() 76 | loop = asyncio.get_running_loop() 77 | loop.create_task(daemon_thread(status)) 78 | await asyncio.gather(*(single_thread(oracle_pool , start_number + _ , status) for _ in range(max_thread * 2))) 79 | await oracle_pool.close() --------------------------------------------------------------------------------