├── .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 | [](https://pypi.org/project/cx-Oracle-async/)
3 | [](https://github.com/GoodManWEN/cx_Oracle_async/blob/master/LICENSE)
4 | [](https://pypi.org/project/cx-Oracle-async/)
5 | [](https://github.com/GoodManWEN/cx_Oracle_async/actions?query=workflow:Publish)
6 | [](https://github.com/GoodManWEN/cx_Oracle_async/actions?query=workflow:Build)
7 | [](https://readthedocs.org/projects/cx-oracle-async/)
8 | [](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()
--------------------------------------------------------------------------------